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 amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
45 from network_dialog import NetworkDialog
46 from qrcodewidget import QRCodeWidget, QRDialog
47 from qrtextedit import QRTextEdit
49 from decimal import Decimal
57 if platform.system() == 'Windows':
58 MONOSPACE_FONT = 'Lucida Console'
59 elif platform.system() == 'Darwin':
60 MONOSPACE_FONT = 'Monaco'
62 MONOSPACE_FONT = 'monospace'
66 # status of payment requests
69 PR_SENT = 2 # sent but not propagated
70 PR_PAID = 3 # send and propagated
71 PR_ERROR = 4 # could not parse
74 from electrum import ELECTRUM_VERSION
89 class StatusBarButton(QPushButton):
90 def __init__(self, icon, tooltip, func):
91 QPushButton.__init__(self, icon, '')
92 self.setToolTip(tooltip)
94 self.setMaximumWidth(25)
95 self.clicked.connect(func)
97 self.setIconSize(QSize(25,25))
99 def keyPressEvent(self, e):
100 if e.key() == QtCore.Qt.Key_Return:
112 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
114 class ElectrumWindow(QMainWindow):
118 def __init__(self, config, network, gui_object):
119 QMainWindow.__init__(self)
122 self.network = network
123 self.gui_object = gui_object
124 self.tray = gui_object.tray
125 self.go_lite = gui_object.go_lite
128 self.create_status_bar()
129 self.need_update = threading.Event()
131 self.decimal_point = config.get('decimal_point', 5)
132 self.num_zeros = int(config.get('num_zeros',0))
135 set_language(config.get('language'))
137 self.funds_error = False
138 self.completions = QStringListModel()
140 self.tabs = tabs = QTabWidget(self)
141 self.column_widths = self.config.get("column_widths_2", default_column_widths )
142 tabs.addTab(self.create_history_tab(), _('History') )
143 tabs.addTab(self.create_send_tab(), _('Send') )
144 tabs.addTab(self.create_receive_tab(), _('Receive') )
145 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
146 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
147 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
148 tabs.addTab(self.create_console_tab(), _('Console') )
149 tabs.setMinimumSize(600, 400)
150 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
151 self.setCentralWidget(tabs)
153 g = self.config.get("winpos-qt",[100, 100, 840, 400])
154 self.setGeometry(g[0], g[1], g[2], g[3])
155 if self.config.get("is_maximized"):
158 self.setWindowIcon(QIcon(":icons/electrum.png"))
161 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
163 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
164 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
165 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
167 for i in range(tabs.count()):
168 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
170 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
171 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
172 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
173 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
174 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
176 self.history_list.setFocus(True)
180 self.network.register_callback('updated', lambda: self.need_update.set())
181 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
182 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
184 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
186 # set initial message
187 self.console.showMessage(self.network.banner)
190 self.payment_request = None
192 def update_account_selector(self):
194 accounts = self.wallet.get_account_names()
195 self.account_selector.clear()
196 if len(accounts) > 1:
197 self.account_selector.addItems([_("All accounts")] + accounts.values())
198 self.account_selector.setCurrentIndex(0)
199 self.account_selector.show()
201 self.account_selector.hide()
204 def load_wallet(self, wallet):
208 self.update_wallet_format()
210 self.invoices = self.wallet.storage.get('invoices', {})
211 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
212 self.current_account = self.wallet.storage.get("current_account", None)
213 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
214 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
215 self.setWindowTitle( title )
217 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
218 self.notify_transactions()
219 self.update_account_selector()
221 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
222 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
223 self.password_menu.setEnabled(not self.wallet.is_watching_only())
224 self.seed_menu.setEnabled(self.wallet.has_seed())
225 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
226 self.import_menu.setEnabled(self.wallet.can_import())
228 self.update_lock_icon()
229 self.update_buttons_on_seed()
230 self.update_console()
232 self.clear_receive_tab()
233 self.update_receive_tab()
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 run_hook('timer_actions')
438 def format_amount(self, x, is_diff=False, whitespaces=False):
439 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
442 def get_decimal_point(self):
443 return self.decimal_point
447 assert self.decimal_point in [5,8]
448 return "BTC" if self.decimal_point == 8 else "mBTC"
451 def update_status(self):
452 if self.network is None or not self.network.is_running():
454 icon = QIcon(":icons/status_disconnected.png")
456 elif self.network.is_connected():
457 if not self.wallet.up_to_date:
458 text = _("Synchronizing...")
459 icon = QIcon(":icons/status_waiting.png")
460 elif self.network.server_lag > 1:
461 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
462 icon = QIcon(":icons/status_lagging.png")
464 c, u = self.wallet.get_account_balance(self.current_account)
465 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
466 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
468 # append fiat balance and price from exchange rate plugin
470 run_hook('get_fiat_status_text', c+u, r)
475 self.tray.setToolTip(text)
476 icon = QIcon(":icons/status_connected.png")
478 text = _("Not connected")
479 icon = QIcon(":icons/status_disconnected.png")
481 self.balance_label.setText(text)
482 self.status_button.setIcon( icon )
485 def update_wallet(self):
487 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
488 self.update_history_tab()
489 self.update_receive_tab()
490 self.update_address_tab()
491 self.update_contacts_tab()
492 self.update_completions()
493 self.update_invoices_tab()
496 def create_history_tab(self):
497 self.history_list = l = MyTreeWidget(self)
499 for i,width in enumerate(self.column_widths['history']):
500 l.setColumnWidth(i, width)
501 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
502 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
503 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
505 l.customContextMenuRequested.connect(self.create_history_menu)
509 def create_history_menu(self, position):
510 self.history_list.selectedIndexes()
511 item = self.history_list.currentItem()
512 be = self.config.get('block_explorer', 'Blockchain.info')
513 if be == 'Blockchain.info':
514 block_explorer = 'https://blockchain.info/tx/'
515 elif be == 'Blockr.io':
516 block_explorer = 'https://blockr.io/tx/info/'
517 elif be == 'Insight.is':
518 block_explorer = 'http://live.insight.is/tx/'
520 tx_hash = str(item.data(0, Qt.UserRole).toString())
521 if not tx_hash: return
523 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
524 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
525 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
526 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
527 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
530 def show_transaction(self, tx):
531 import transaction_dialog
532 d = transaction_dialog.TxDialog(tx, self)
535 def tx_label_clicked(self, item, column):
536 if column==2 and item.isSelected():
538 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
539 self.history_list.editItem( item, column )
540 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
543 def tx_label_changed(self, item, column):
547 tx_hash = str(item.data(0, Qt.UserRole).toString())
548 tx = self.wallet.transactions.get(tx_hash)
549 text = unicode( item.text(2) )
550 self.wallet.set_label(tx_hash, text)
552 item.setForeground(2, QBrush(QColor('black')))
554 text = self.wallet.get_default_label(tx_hash)
555 item.setText(2, text)
556 item.setForeground(2, QBrush(QColor('gray')))
560 def edit_label(self, is_recv):
561 l = self.receive_list if is_recv else self.contacts_list
562 item = l.currentItem()
563 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
564 l.editItem( item, 1 )
565 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
569 def address_label_clicked(self, item, column, l, column_addr, column_label):
570 if column == column_label and item.isSelected():
571 is_editable = item.data(0, 32).toBool()
574 addr = unicode( item.text(column_addr) )
575 label = unicode( item.text(column_label) )
576 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
577 l.editItem( item, column )
578 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
581 def address_label_changed(self, item, column, l, column_addr, column_label):
582 if column == column_label:
583 addr = unicode( item.text(column_addr) )
584 text = unicode( item.text(column_label) )
585 is_editable = item.data(0, 32).toBool()
589 changed = self.wallet.set_label(addr, text)
591 self.update_history_tab()
592 self.update_completions()
594 self.current_item_changed(item)
596 run_hook('item_changed', item, column)
599 def current_item_changed(self, a):
600 run_hook('current_item_changed', a)
604 def update_history_tab(self):
606 self.history_list.clear()
607 for item in self.wallet.get_tx_history(self.current_account):
608 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
609 time_str = _("unknown")
612 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
614 time_str = _("error")
617 time_str = 'unverified'
618 icon = QIcon(":icons/unconfirmed.png")
621 icon = QIcon(":icons/unconfirmed.png")
623 icon = QIcon(":icons/clock%d.png"%conf)
625 icon = QIcon(":icons/confirmed.png")
627 if value is not None:
628 v_str = self.format_amount(value, True, whitespaces=True)
632 balance_str = self.format_amount(balance, whitespaces=True)
635 label, is_default_label = self.wallet.get_label(tx_hash)
637 label = _('Pruned transaction outputs')
638 is_default_label = False
640 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
641 item.setFont(2, QFont(MONOSPACE_FONT))
642 item.setFont(3, QFont(MONOSPACE_FONT))
643 item.setFont(4, QFont(MONOSPACE_FONT))
645 item.setForeground(3, QBrush(QColor("#BC1E1E")))
647 item.setData(0, Qt.UserRole, tx_hash)
648 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
650 item.setForeground(2, QBrush(QColor('grey')))
652 item.setIcon(0, icon)
653 self.history_list.insertTopLevelItem(0,item)
656 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
657 run_hook('history_tab_update')
660 def create_receive_tab(self):
662 grid = QGridLayout(w)
663 grid.setColumnMinimumWidth(3, 300)
664 grid.setColumnStretch(5, 1)
666 self.receive_address_e = QLineEdit()
667 self.receive_address_e.setReadOnly(True)
668 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
669 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
670 self.receive_address_e.textChanged.connect(self.update_receive_qr)
672 self.receive_message_e = QLineEdit()
673 grid.addWidget(QLabel(_('Message')), 1, 0)
674 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
675 self.receive_message_e.textChanged.connect(self.update_receive_qr)
677 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
678 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
679 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
680 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
682 self.save_request_button = QPushButton(_('Save'))
683 self.save_request_button.clicked.connect(self.save_payment_request)
684 grid.addWidget(self.save_request_button, 3, 1)
685 clear_button = QPushButton(_('New'))
686 clear_button.clicked.connect(self.clear_receive_tab)
687 grid.addWidget(clear_button, 3, 2)
688 grid.setRowStretch(4, 1)
690 self.receive_qr = QRCodeWidget()
691 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
693 grid.setRowStretch(5, 1)
695 self.receive_requests_label = QLabel(_('Saved Requests'))
696 self.receive_list = MyTreeWidget(self)
697 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
698 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
699 self.receive_list.itemClicked.connect(self.receive_item_changed)
700 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
701 self.receive_list.setColumnWidth(0, 340)
702 h = self.receive_list.header()
703 h.setStretchLastSection(False)
704 h.setResizeMode(1, QHeaderView.Stretch)
706 grid.addWidget(self.receive_requests_label, 6, 0)
707 grid.addWidget(self.receive_list, 7, 0, 1, 6)
710 def receive_item_changed(self, item):
713 addr = str(item.text(0))
714 amount, message = self.receive_requests[addr]
715 self.receive_address_e.setText(addr)
716 self.receive_message_e.setText(message)
717 self.receive_amount_e.setAmount(amount)
720 def receive_list_delete(self, item):
721 addr = str(item.text(0))
722 self.receive_requests.pop(addr)
723 self.update_receive_tab()
724 self.clear_receive_tab()
726 def receive_list_menu(self, position):
727 item = self.receive_list.itemAt(position)
729 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
730 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
732 def save_payment_request(self):
733 addr = str(self.receive_address_e.text())
734 amount = self.receive_amount_e.get_amount()
735 message = str(self.receive_message_e.text())
736 if not message and not amount:
737 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
739 self.receive_requests = self.wallet.storage.get('receive_requests',{})
740 self.receive_requests[addr] = (amount, message)
741 self.wallet.storage.put('receive_requests', self.receive_requests)
742 self.update_receive_tab()
744 def clear_receive_tab(self):
745 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():
752 self.receive_address_e.setText(addr)
753 self.receive_message_e.setText('')
754 self.receive_amount_e.setAmount(None)
756 def receive_at(self, addr):
757 if not bitcoin.is_address(addr):
759 self.tabs.setCurrentIndex(2)
760 self.receive_address_e.setText(addr)
762 def update_receive_tab(self):
763 self.receive_requests = self.wallet.storage.get('receive_requests',{})
764 b = len(self.receive_requests) > 0
765 self.receive_list.setVisible(b)
766 self.receive_requests_label.setVisible(b)
768 self.receive_list.clear()
769 for address, v in self.receive_requests.items():
771 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
772 self.receive_list.addTopLevelItem(item)
775 def update_receive_qr(self):
776 import urlparse, urllib
777 addr = str(self.receive_address_e.text())
778 amount = self.receive_amount_e.get_amount()
779 message = unicode(self.receive_message_e.text()).encode('utf8')
780 self.save_request_button.setEnabled((amount is not None) or (message != ""))
784 query.append('amount=%s'%format_satoshis(amount))
786 query.append('message=%s'%urllib.quote(message))
787 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
788 url = urlparse.urlunparse(p)
791 self.receive_qr.setData(url)
792 run_hook('update_receive_qr', addr, amount, message, url)
795 def create_send_tab(self):
798 self.send_grid = grid = QGridLayout(w)
800 grid.setColumnMinimumWidth(3,300)
801 grid.setColumnStretch(5,1)
802 grid.setRowStretch(8, 1)
804 from paytoedit import PayToEdit
805 self.amount_e = BTCAmountEdit(self.get_decimal_point)
806 self.payto_e = PayToEdit(self)
807 self.payto_help = HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)'))
808 grid.addWidget(QLabel(_('Pay to')), 1, 0)
809 grid.addWidget(self.payto_e, 1, 1, 1, 3)
810 grid.addWidget(self.payto_help, 1, 4)
812 completer = QCompleter()
813 completer.setCaseSensitivity(False)
814 self.payto_e.setCompleter(completer)
815 completer.setModel(self.completions)
817 self.message_e = MyLineEdit()
818 self.message_help = HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.'))
819 grid.addWidget(QLabel(_('Description')), 2, 0)
820 grid.addWidget(self.message_e, 2, 1, 1, 3)
821 grid.addWidget(self.message_help, 2, 4)
823 self.from_label = QLabel(_('From'))
824 grid.addWidget(self.from_label, 3, 0)
825 self.from_list = MyTreeWidget(self)
826 self.from_list.setColumnCount(2)
827 self.from_list.setColumnWidth(0, 350)
828 self.from_list.setColumnWidth(1, 50)
829 self.from_list.setHeaderHidden(True)
830 self.from_list.setMaximumHeight(80)
831 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
832 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
833 grid.addWidget(self.from_list, 3, 1, 1, 3)
834 self.set_pay_from([])
836 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
837 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') \
838 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
839 grid.addWidget(QLabel(_('Amount')), 4, 0)
840 grid.addWidget(self.amount_e, 4, 1, 1, 2)
841 grid.addWidget(self.amount_help, 4, 3)
843 self.fee_e = BTCAmountEdit(self.get_decimal_point)
844 grid.addWidget(QLabel(_('Fee')), 5, 0)
845 grid.addWidget(self.fee_e, 5, 1, 1, 2)
846 grid.addWidget(HelpButton(
847 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
848 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
849 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
851 self.send_button = EnterButton(_("Send"), self.do_send)
852 grid.addWidget(self.send_button, 6, 1)
854 b = EnterButton(_("Clear"), self.do_clear)
855 grid.addWidget(b, 6, 2)
857 self.payto_sig = QLabel('')
858 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
860 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
861 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
864 def entry_changed( is_fee ):
865 self.funds_error = False
867 if self.amount_e.is_shortcut:
868 self.amount_e.is_shortcut = False
869 sendable = self.get_sendable_balance()
870 # there is only one output because we are completely spending inputs
871 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
872 fee = self.wallet.estimated_fee(inputs, 1)
874 self.amount_e.setAmount(amount)
875 self.fee_e.setAmount(fee)
878 amount = self.amount_e.get_amount()
879 fee = self.fee_e.get_amount()
881 if not is_fee: fee = None
884 # assume that there will be 2 outputs (one for change)
885 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
887 self.fee_e.setAmount(fee)
890 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
894 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
895 self.funds_error = True
896 text = _( "Not enough funds" )
897 c, u = self.wallet.get_frozen_balance()
898 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
900 self.statusBar().showMessage(text)
901 self.amount_e.setPalette(palette)
902 self.fee_e.setPalette(palette)
904 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
905 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
907 run_hook('create_send_tab', grid)
910 def from_list_delete(self, item):
911 i = self.from_list.indexOfTopLevelItem(item)
913 self.redraw_from_list()
915 def from_list_menu(self, position):
916 item = self.from_list.itemAt(position)
918 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
919 menu.exec_(self.from_list.viewport().mapToGlobal(position))
921 def set_pay_from(self, domain = None):
922 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
923 self.redraw_from_list()
925 def redraw_from_list(self):
926 self.from_list.clear()
927 self.from_label.setHidden(len(self.pay_from) == 0)
928 self.from_list.setHidden(len(self.pay_from) == 0)
931 h = x.get('prevout_hash')
932 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
934 for item in self.pay_from:
935 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
937 def update_completions(self):
939 for addr,label in self.wallet.labels.items():
940 if addr in self.wallet.addressbook:
941 l.append( label + ' <' + addr + '>')
943 run_hook('update_completions', l)
944 self.completions.setStringList(l)
948 return lambda s, *args: s.do_protect(func, args)
951 def read_send_tab(self):
953 if self.payment_request and self.payment_request.has_expired():
954 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
957 label = unicode( self.message_e.text() )
959 if self.payment_request:
960 outputs = self.payment_request.get_outputs()
962 outputs = self.payto_e.get_outputs()
965 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
968 for addr, x in outputs:
969 if addr is None or not bitcoin.is_address(addr):
970 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
973 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
976 amount = sum(map(lambda x:x[1], outputs))
978 fee = self.fee_e.get_amount()
980 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
983 confirm_amount = self.config.get('confirm_amount', 100000000)
984 if amount >= confirm_amount:
985 o = '\n'.join(map(lambda x:x[0], outputs))
986 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
989 confirm_fee = self.config.get('confirm_fee', 100000)
990 if fee >= confirm_fee:
991 if not self.question(_("The fee for this transaction seems unusually high.\nAre you really sure you want to pay %(fee)s in fees?")%{ 'fee' : self.format_amount(fee) + ' '+ self.base_unit()}):
994 coins = self.get_coins()
995 return outputs, fee, label, coins
999 r = self.read_send_tab()
1002 outputs, fee, label, coins = r
1003 self.send_tx(outputs, fee, label, coins)
1007 def send_tx(self, outputs, fee, label, coins, password):
1008 self.send_button.setDisabled(True)
1010 # first, create an unsigned tx
1012 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1014 except Exception as e:
1015 traceback.print_exc(file=sys.stdout)
1016 self.show_message(str(e))
1017 self.send_button.setDisabled(False)
1020 # call hook to see if plugin needs gui interaction
1021 run_hook('send_tx', tx)
1027 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
1028 self.wallet.sign_transaction(tx, keypairs, password)
1029 return tx, fee, label
1031 def sign_done(tx, fee, label):
1033 self.show_message(tx.error)
1034 self.send_button.setDisabled(False)
1036 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1037 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1038 self.send_button.setDisabled(False)
1041 self.wallet.set_label(tx.hash(), label)
1043 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1044 self.show_transaction(tx)
1046 self.send_button.setDisabled(False)
1049 self.broadcast_transaction(tx)
1051 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1052 self.waiting_dialog.start()
1056 def broadcast_transaction(self, tx):
1058 def broadcast_thread():
1059 pr = self.payment_request
1061 return self.wallet.sendtx(tx)
1063 if pr.has_expired():
1064 self.payment_request = None
1065 return False, _("Payment request has expired")
1067 status, msg = self.wallet.sendtx(tx)
1071 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1072 self.wallet.storage.put('invoices', self.invoices)
1073 self.update_invoices_tab()
1074 self.payment_request = None
1075 refund_address = self.wallet.addresses()[0]
1076 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1082 def broadcast_done(status, msg):
1084 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1087 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1088 self.send_button.setDisabled(False)
1090 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1091 self.waiting_dialog.start()
1095 def prepare_for_payment_request(self):
1096 self.tabs.setCurrentIndex(1)
1097 self.payto_e.is_pr = True
1098 for e in [self.payto_e, self.amount_e, self.message_e]:
1100 for h in [self.payto_help, self.amount_help, self.message_help]:
1102 self.payto_e.setText(_("please wait..."))
1105 def payment_request_ok(self):
1106 pr = self.payment_request
1108 if pr_id not in self.invoices:
1109 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1110 self.wallet.storage.put('invoices', self.invoices)
1111 self.update_invoices_tab()
1113 print_error('invoice already in list')
1115 status = self.invoices[pr_id][4]
1116 if status == PR_PAID:
1118 self.show_message("invoice already paid")
1119 self.payment_request = None
1122 self.payto_help.show()
1123 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1125 if not pr.has_expired():
1126 self.payto_e.setGreen()
1128 self.payto_e.setExpired()
1130 self.payto_e.setText(pr.domain)
1131 self.amount_e.setText(self.format_amount(pr.get_amount()))
1132 self.message_e.setText(pr.get_memo())
1134 def payment_request_error(self):
1136 self.show_message(self.payment_request.error)
1137 self.payment_request = None
1139 def pay_from_URI(self,URI):
1142 address, amount, label, message, request_url = util.parse_URI(URI)
1144 address, amount, label, message, request_url = util.parse_URI(URI)
1145 except Exception as e:
1146 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1149 self.tabs.setCurrentIndex(1)
1153 if self.wallet.labels.get(address) != label:
1154 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1155 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1156 self.wallet.addressbook.append(address)
1157 self.wallet.set_label(address, label)
1159 label = self.wallet.labels.get(address)
1161 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1163 self.message_e.setText(message)
1165 self.amount_e.setAmount(amount)
1168 from electrum import paymentrequest
1169 def payment_request():
1170 self.payment_request = paymentrequest.PaymentRequest(self.config)
1171 self.payment_request.read(request_url)
1172 if self.payment_request.verify():
1173 self.emit(SIGNAL('payment_request_ok'))
1175 self.emit(SIGNAL('payment_request_error'))
1177 self.pr_thread = threading.Thread(target=payment_request).start()
1178 self.prepare_for_payment_request()
1183 self.payto_e.is_pr = False
1184 self.payto_sig.setVisible(False)
1185 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1189 for h in [self.payto_help, self.amount_help, self.message_help]:
1192 self.payto_help.set_alt(None)
1193 self.set_pay_from([])
1194 self.update_status()
1198 def set_addrs_frozen(self,addrs,freeze):
1200 if not addr: continue
1201 if addr in self.wallet.frozen_addresses and not freeze:
1202 self.wallet.unfreeze(addr)
1203 elif addr not in self.wallet.frozen_addresses and freeze:
1204 self.wallet.freeze(addr)
1205 self.update_address_tab()
1209 def create_list_tab(self, headers):
1210 "generic tab creation method"
1211 l = MyTreeWidget(self)
1212 l.setColumnCount( len(headers) )
1213 l.setHeaderLabels( headers )
1216 vbox = QVBoxLayout()
1223 vbox.addWidget(buttons)
1228 def create_addresses_tab(self):
1229 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1230 for i,width in enumerate(self.column_widths['receive']):
1231 l.setColumnWidth(i, width)
1232 l.setContextMenuPolicy(Qt.CustomContextMenu)
1233 l.customContextMenuRequested.connect(self.create_receive_menu)
1234 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1235 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1236 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1237 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1238 self.address_list = l
1244 def save_column_widths(self):
1245 self.column_widths["receive"] = []
1246 for i in range(self.address_list.columnCount() -1):
1247 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1249 self.column_widths["history"] = []
1250 for i in range(self.history_list.columnCount() - 1):
1251 self.column_widths["history"].append(self.history_list.columnWidth(i))
1253 self.column_widths["contacts"] = []
1254 for i in range(self.contacts_list.columnCount() - 1):
1255 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1257 self.config.set_key("column_widths_2", self.column_widths, True)
1260 def create_contacts_tab(self):
1261 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1262 l.setContextMenuPolicy(Qt.CustomContextMenu)
1263 l.customContextMenuRequested.connect(self.create_contact_menu)
1264 for i,width in enumerate(self.column_widths['contacts']):
1265 l.setColumnWidth(i, width)
1266 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1267 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1268 self.contacts_list = l
1272 def create_invoices_tab(self):
1273 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1275 h.setStretchLastSection(False)
1276 h.setResizeMode(1, QHeaderView.Stretch)
1277 l.setContextMenuPolicy(Qt.CustomContextMenu)
1278 l.customContextMenuRequested.connect(self.create_invoice_menu)
1279 self.invoices_list = l
1282 def update_invoices_tab(self):
1283 invoices = self.wallet.storage.get('invoices', {})
1284 l = self.invoices_list
1286 for key, value in invoices.items():
1288 domain, memo, amount, expiration_date, status, tx_hash = value
1292 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1294 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1295 l.addTopLevelItem(item)
1297 l.setCurrentItem(l.topLevelItem(0))
1301 def delete_imported_key(self, addr):
1302 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1303 self.wallet.delete_imported_key(addr)
1304 self.update_address_tab()
1305 self.update_history_tab()
1307 def edit_account_label(self, k):
1308 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1310 label = unicode(text)
1311 self.wallet.set_label(k,label)
1312 self.update_address_tab()
1314 def account_set_expanded(self, item, k, b):
1316 self.accounts_expanded[k] = b
1318 def create_account_menu(self, position, k, item):
1320 if item.isExpanded():
1321 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1323 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1324 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1325 if self.wallet.seed_version > 4:
1326 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1327 if self.wallet.account_is_pending(k):
1328 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1329 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1331 def delete_pending_account(self, k):
1332 self.wallet.delete_pending_account(k)
1333 self.update_address_tab()
1335 def create_receive_menu(self, position):
1336 # fixme: this function apparently has a side effect.
1337 # if it is not called the menu pops up several times
1338 #self.address_list.selectedIndexes()
1340 selected = self.address_list.selectedItems()
1341 multi_select = len(selected) > 1
1342 addrs = [unicode(item.text(0)) for item in selected]
1343 if not multi_select:
1344 item = self.address_list.itemAt(position)
1348 if not is_valid(addr):
1349 k = str(item.data(0,32).toString())
1351 self.create_account_menu(position, k, item)
1353 item.setExpanded(not item.isExpanded())
1357 if not multi_select:
1358 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1359 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1360 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1361 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1362 if not self.wallet.is_watching_only():
1363 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1364 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1365 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1366 if self.wallet.is_imported(addr):
1367 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1369 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1370 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1371 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1372 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1375 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1376 if any(can_send(addr) for addr in addrs):
1377 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1379 run_hook('receive_menu', menu, addrs)
1380 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1383 def get_sendable_balance(self):
1384 return sum(map(lambda x:x['value'], self.get_coins()))
1387 def get_coins(self):
1389 return self.pay_from
1391 domain = self.wallet.get_account_addresses(self.current_account)
1392 for i in self.wallet.frozen_addresses:
1393 if i in domain: domain.remove(i)
1394 return self.wallet.get_unspent_coins(domain)
1397 def send_from_addresses(self, addrs):
1398 self.set_pay_from( addrs )
1399 self.tabs.setCurrentIndex(1)
1402 def payto(self, addr):
1404 label = self.wallet.labels.get(addr)
1405 m_addr = label + ' <' + addr + '>' if label else addr
1406 self.tabs.setCurrentIndex(1)
1407 self.payto_e.setText(m_addr)
1408 self.amount_e.setFocus()
1411 def delete_contact(self, x):
1412 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1413 self.wallet.delete_contact(x)
1414 self.wallet.set_label(x, None)
1415 self.update_history_tab()
1416 self.update_contacts_tab()
1417 self.update_completions()
1420 def create_contact_menu(self, position):
1421 item = self.contacts_list.itemAt(position)
1424 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1426 addr = unicode(item.text(0))
1427 label = unicode(item.text(1))
1428 is_editable = item.data(0,32).toBool()
1429 payto_addr = item.data(0,33).toString()
1430 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1431 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1432 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1434 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1435 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1437 run_hook('create_contact_menu', menu, item)
1438 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1440 def delete_invoice(self, key):
1441 self.invoices.pop(key)
1442 self.wallet.storage.put('invoices', self.invoices)
1443 self.update_invoices_tab()
1445 def show_invoice(self, key):
1446 from electrum.paymentrequest import PaymentRequest
1447 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1448 pr = PaymentRequest(self.config)
1452 self.show_pr_details(pr)
1454 def show_pr_details(self, pr):
1455 msg = 'Domain: ' + pr.domain
1456 msg += '\nStatus: ' + pr.get_status()
1457 msg += '\nMemo: ' + pr.get_memo()
1458 msg += '\nPayment URL: ' + pr.payment_url
1459 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1460 QMessageBox.information(self, 'Invoice', msg , 'OK')
1462 def do_pay_invoice(self, key):
1463 from electrum.paymentrequest import PaymentRequest
1464 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1465 pr = PaymentRequest(self.config)
1468 self.payment_request = pr
1469 self.prepare_for_payment_request()
1471 self.payment_request_ok()
1473 self.payment_request_error()
1476 def create_invoice_menu(self, position):
1477 item = self.invoices_list.itemAt(position)
1480 k = self.invoices_list.indexOfTopLevelItem(item)
1481 key = self.invoices.keys()[k]
1482 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1484 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1485 if status == PR_UNPAID:
1486 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1487 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1488 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1491 def update_address_item(self, item):
1492 item.setFont(0, QFont(MONOSPACE_FONT))
1493 address = str(item.data(0,0).toString())
1494 label = self.wallet.labels.get(address,'')
1495 item.setData(1,0,label)
1496 item.setData(0,32, True) # is editable
1498 run_hook('update_address_item', address, item)
1500 if not self.wallet.is_mine(address): return
1502 c, u = self.wallet.get_addr_balance(address)
1503 balance = self.format_amount(c + u)
1504 item.setData(2,0,balance)
1506 if address in self.wallet.frozen_addresses:
1507 item.setBackgroundColor(0, QColor('lightblue'))
1510 def update_address_tab(self):
1511 l = self.address_list
1512 # extend the syntax for consistency
1513 l.addChild = l.addTopLevelItem
1514 l.insertChild = l.insertTopLevelItem
1518 accounts = self.wallet.get_accounts()
1519 if self.current_account is None:
1520 account_items = sorted(accounts.items())
1522 account_items = [(self.current_account, accounts.get(self.current_account))]
1525 for k, account in account_items:
1527 if len(accounts) > 1:
1528 name = self.wallet.get_account_name(k)
1529 c,u = self.wallet.get_account_balance(k)
1530 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1531 l.addTopLevelItem(account_item)
1532 account_item.setExpanded(self.accounts_expanded.get(k, True))
1533 account_item.setData(0, 32, k)
1537 sequences = [0,1] if account.has_change() else [0]
1538 for is_change in sequences:
1539 if len(sequences) > 1:
1540 name = _("Receiving") if not is_change else _("Change")
1541 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1542 account_item.addChild(seq_item)
1544 seq_item.setExpanded(True)
1546 seq_item = account_item
1548 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1554 for address in account.get_addresses(is_change):
1556 num, is_used = self.wallet.is_used(address)
1559 if gap > self.wallet.gap_limit:
1564 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1565 self.update_address_item(item)
1567 item.setBackgroundColor(1, QColor('red'))
1571 seq_item.insertChild(0,used_item)
1573 used_item.addChild(item)
1575 seq_item.addChild(item)
1577 # we use column 1 because column 0 may be hidden
1578 l.setCurrentItem(l.topLevelItem(0),1)
1581 def update_contacts_tab(self):
1582 l = self.contacts_list
1585 for address in self.wallet.addressbook:
1586 label = self.wallet.labels.get(address,'')
1587 n = self.wallet.get_num_tx(address)
1588 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1589 item.setFont(0, QFont(MONOSPACE_FONT))
1590 # 32 = label can be edited (bool)
1591 item.setData(0,32, True)
1593 item.setData(0,33, address)
1594 l.addTopLevelItem(item)
1596 run_hook('update_contacts_tab', l)
1597 l.setCurrentItem(l.topLevelItem(0))
1601 def create_console_tab(self):
1602 from console import Console
1603 self.console = console = Console()
1607 def update_console(self):
1608 console = self.console
1609 console.history = self.config.get("console-history",[])
1610 console.history_index = len(console.history)
1612 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1613 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1615 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1617 def mkfunc(f, method):
1618 return lambda *args: apply( f, (method, args, self.password_dialog ))
1620 if m[0]=='_' or m in ['network','wallet']: continue
1621 methods[m] = mkfunc(c._run, m)
1623 console.updateNamespace(methods)
1626 def change_account(self,s):
1627 if s == _("All accounts"):
1628 self.current_account = None
1630 accounts = self.wallet.get_account_names()
1631 for k, v in accounts.items():
1633 self.current_account = k
1634 self.update_history_tab()
1635 self.update_status()
1636 self.update_address_tab()
1637 self.update_receive_tab()
1639 def create_status_bar(self):
1642 sb.setFixedHeight(35)
1643 qtVersion = qVersion()
1645 self.balance_label = QLabel("")
1646 sb.addWidget(self.balance_label)
1648 from version_getter import UpdateLabel
1649 self.updatelabel = UpdateLabel(self.config, sb)
1651 self.account_selector = QComboBox()
1652 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1653 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1654 sb.addPermanentWidget(self.account_selector)
1656 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1657 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1659 self.lock_icon = QIcon()
1660 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1661 sb.addPermanentWidget( self.password_button )
1663 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1664 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1665 sb.addPermanentWidget( self.seed_button )
1666 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1667 sb.addPermanentWidget( self.status_button )
1669 run_hook('create_status_bar', (sb,))
1671 self.setStatusBar(sb)
1674 def update_lock_icon(self):
1675 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1676 self.password_button.setIcon( icon )
1679 def update_buttons_on_seed(self):
1680 if self.wallet.has_seed():
1681 self.seed_button.show()
1683 self.seed_button.hide()
1685 if not self.wallet.is_watching_only():
1686 self.password_button.show()
1687 self.send_button.setText(_("Send"))
1689 self.password_button.hide()
1690 self.send_button.setText(_("Create unsigned transaction"))
1693 def change_password_dialog(self):
1694 from password_dialog import PasswordDialog
1695 d = PasswordDialog(self.wallet, self)
1697 self.update_lock_icon()
1700 def new_contact_dialog(self):
1703 d.setWindowTitle(_("New Contact"))
1704 vbox = QVBoxLayout(d)
1705 vbox.addWidget(QLabel(_('New Contact')+':'))
1707 grid = QGridLayout()
1710 grid.addWidget(QLabel(_("Address")), 1, 0)
1711 grid.addWidget(line1, 1, 1)
1712 grid.addWidget(QLabel(_("Name")), 2, 0)
1713 grid.addWidget(line2, 2, 1)
1715 vbox.addLayout(grid)
1716 vbox.addLayout(ok_cancel_buttons(d))
1721 address = str(line1.text())
1722 label = unicode(line2.text())
1724 if not is_valid(address):
1725 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1728 self.wallet.add_contact(address)
1730 self.wallet.set_label(address, label)
1732 self.update_contacts_tab()
1733 self.update_history_tab()
1734 self.update_completions()
1735 self.tabs.setCurrentIndex(3)
1739 def new_account_dialog(self, password):
1741 dialog = QDialog(self)
1743 dialog.setWindowTitle(_("New Account"))
1745 vbox = QVBoxLayout()
1746 vbox.addWidget(QLabel(_('Account name')+':'))
1749 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1750 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1755 vbox.addLayout(ok_cancel_buttons(dialog))
1756 dialog.setLayout(vbox)
1760 name = str(e.text())
1763 self.wallet.create_pending_account(name, password)
1764 self.update_address_tab()
1765 self.tabs.setCurrentIndex(2)
1770 def show_master_public_keys(self):
1772 dialog = QDialog(self)
1774 dialog.setWindowTitle(_("Master Public Keys"))
1776 main_layout = QGridLayout()
1777 mpk_dict = self.wallet.get_master_public_keys()
1779 for key, value in mpk_dict.items():
1780 main_layout.addWidget(QLabel(key), i, 0)
1781 mpk_text = QTextEdit()
1782 mpk_text.setReadOnly(True)
1783 mpk_text.setMaximumHeight(170)
1784 mpk_text.setText(value)
1785 main_layout.addWidget(mpk_text, i + 1, 0)
1788 vbox = QVBoxLayout()
1789 vbox.addLayout(main_layout)
1790 vbox.addLayout(close_button(dialog))
1792 dialog.setLayout(vbox)
1797 def show_seed_dialog(self, password):
1798 if not self.wallet.has_seed():
1799 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1803 mnemonic = self.wallet.get_mnemonic(password)
1805 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1807 from seed_dialog import SeedDialog
1808 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1813 def show_qrcode(self, data, title = _("QR code")):
1816 print_error("qrcode:", data)
1817 d = QRDialog(data, self, title)
1821 def do_protect(self, func, args):
1822 if self.wallet.use_encryption:
1823 password = self.password_dialog()
1829 if args != (False,):
1830 args = (self,) + args + (password,)
1832 args = (self,password)
1836 def show_public_keys(self, address):
1837 if not address: return
1839 pubkey_list = self.wallet.get_public_keys(address)
1840 except Exception as e:
1841 traceback.print_exc(file=sys.stdout)
1842 self.show_message(str(e))
1846 d.setMinimumSize(600, 200)
1848 vbox = QVBoxLayout()
1849 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1850 vbox.addWidget( QLabel(_("Public key") + ':'))
1852 keys.setReadOnly(True)
1853 keys.setText('\n'.join(pubkey_list))
1854 vbox.addWidget(keys)
1855 vbox.addLayout(close_button(d))
1860 def show_private_key(self, address, password):
1861 if not address: return
1863 pk_list = self.wallet.get_private_key(address, password)
1864 except Exception as e:
1865 traceback.print_exc(file=sys.stdout)
1866 self.show_message(str(e))
1870 d.setMinimumSize(600, 200)
1872 vbox = QVBoxLayout()
1873 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1874 vbox.addWidget( QLabel(_("Private key") + ':'))
1876 keys.setReadOnly(True)
1877 keys.setText('\n'.join(pk_list))
1878 vbox.addWidget(keys)
1879 vbox.addLayout(close_button(d))
1885 def do_sign(self, address, message, signature, password):
1886 message = unicode(message.toPlainText())
1887 message = message.encode('utf-8')
1889 sig = self.wallet.sign_message(str(address.text()), message, password)
1890 signature.setText(sig)
1891 except Exception as e:
1892 self.show_message(str(e))
1894 def do_verify(self, address, message, signature):
1895 message = unicode(message.toPlainText())
1896 message = message.encode('utf-8')
1897 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1898 self.show_message(_("Signature verified"))
1900 self.show_message(_("Error: wrong signature"))
1903 def sign_verify_message(self, address=''):
1906 d.setWindowTitle(_('Sign/verify Message'))
1907 d.setMinimumSize(410, 290)
1909 layout = QGridLayout(d)
1911 message_e = QTextEdit()
1912 layout.addWidget(QLabel(_('Message')), 1, 0)
1913 layout.addWidget(message_e, 1, 1)
1914 layout.setRowStretch(2,3)
1916 address_e = QLineEdit()
1917 address_e.setText(address)
1918 layout.addWidget(QLabel(_('Address')), 2, 0)
1919 layout.addWidget(address_e, 2, 1)
1921 signature_e = QTextEdit()
1922 layout.addWidget(QLabel(_('Signature')), 3, 0)
1923 layout.addWidget(signature_e, 3, 1)
1924 layout.setRowStretch(3,1)
1926 hbox = QHBoxLayout()
1928 b = QPushButton(_("Sign"))
1929 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1932 b = QPushButton(_("Verify"))
1933 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1936 b = QPushButton(_("Close"))
1937 b.clicked.connect(d.accept)
1939 layout.addLayout(hbox, 4, 1)
1944 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1946 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1947 message_e.setText(decrypted)
1948 except Exception as e:
1949 self.show_message(str(e))
1952 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1953 message = unicode(message_e.toPlainText())
1954 message = message.encode('utf-8')
1956 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1957 encrypted_e.setText(encrypted)
1958 except Exception as e:
1959 self.show_message(str(e))
1963 def encrypt_message(self, address = ''):
1966 d.setWindowTitle(_('Encrypt/decrypt Message'))
1967 d.setMinimumSize(610, 490)
1969 layout = QGridLayout(d)
1971 message_e = QTextEdit()
1972 layout.addWidget(QLabel(_('Message')), 1, 0)
1973 layout.addWidget(message_e, 1, 1)
1974 layout.setRowStretch(2,3)
1976 pubkey_e = QLineEdit()
1978 pubkey = self.wallet.getpubkeys(address)[0]
1979 pubkey_e.setText(pubkey)
1980 layout.addWidget(QLabel(_('Public key')), 2, 0)
1981 layout.addWidget(pubkey_e, 2, 1)
1983 encrypted_e = QTextEdit()
1984 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1985 layout.addWidget(encrypted_e, 3, 1)
1986 layout.setRowStretch(3,1)
1988 hbox = QHBoxLayout()
1989 b = QPushButton(_("Encrypt"))
1990 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1993 b = QPushButton(_("Decrypt"))
1994 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1997 b = QPushButton(_("Close"))
1998 b.clicked.connect(d.accept)
2001 layout.addLayout(hbox, 4, 1)
2005 def question(self, msg):
2006 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2008 def show_message(self, msg):
2009 QMessageBox.information(self, _('Message'), msg, _('OK'))
2011 def password_dialog(self, msg=None):
2014 d.setWindowTitle(_("Enter Password"))
2019 vbox = QVBoxLayout()
2021 msg = _('Please enter your password')
2022 vbox.addWidget(QLabel(msg))
2024 grid = QGridLayout()
2026 grid.addWidget(QLabel(_('Password')), 1, 0)
2027 grid.addWidget(pw, 1, 1)
2028 vbox.addLayout(grid)
2030 vbox.addLayout(ok_cancel_buttons(d))
2033 run_hook('password_dialog', pw, grid, 1)
2034 if not d.exec_(): return
2035 return unicode(pw.text())
2044 def tx_from_text(self, txt):
2045 "json or raw hexadecimal"
2048 tx = Transaction(txt)
2054 tx_dict = json.loads(str(txt))
2055 assert "hex" in tx_dict.keys()
2056 tx = Transaction(tx_dict["hex"])
2057 if tx_dict.has_key("input_info"):
2058 input_info = json.loads(tx_dict['input_info'])
2059 tx.add_input_info(input_info)
2062 traceback.print_exc(file=sys.stdout)
2065 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2069 def read_tx_from_file(self):
2070 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2074 with open(fileName, "r") as f:
2075 file_content = f.read()
2076 except (ValueError, IOError, os.error), reason:
2077 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2079 return self.tx_from_text(file_content)
2083 def sign_raw_transaction(self, tx, input_info, password):
2085 self.wallet.signrawtransaction(tx, input_info, [], password)
2086 except Exception as e:
2087 QMessageBox.warning(self, _("Error"), str(e))
2089 def do_process_from_text(self):
2090 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2093 tx = self.tx_from_text(text)
2095 self.show_transaction(tx)
2097 def do_process_from_file(self):
2098 tx = self.read_tx_from_file()
2100 self.show_transaction(tx)
2102 def do_process_from_txid(self):
2103 from electrum import transaction
2104 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2106 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2108 tx = transaction.Transaction(r)
2110 self.show_transaction(tx)
2112 self.show_message("unknown transaction")
2114 def do_process_from_csvReader(self, csvReader):
2119 for position, row in enumerate(csvReader):
2121 if not is_valid(address):
2122 errors.append((position, address))
2124 amount = Decimal(row[1])
2125 amount = int(100000000*amount)
2126 outputs.append((address, amount))
2127 except (ValueError, IOError, os.error), reason:
2128 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2132 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2133 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2137 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2138 except Exception as e:
2139 self.show_message(str(e))
2142 self.show_transaction(tx)
2144 def do_process_from_csv_file(self):
2145 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2149 with open(fileName, "r") as f:
2150 csvReader = csv.reader(f)
2151 self.do_process_from_csvReader(csvReader)
2152 except (ValueError, IOError, os.error), reason:
2153 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2156 def do_process_from_csv_text(self):
2157 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2158 + _("Format: address, amount. One output per line"), _("Load CSV"))
2161 f = StringIO.StringIO(text)
2162 csvReader = csv.reader(f)
2163 self.do_process_from_csvReader(csvReader)
2168 def export_privkeys_dialog(self, password):
2169 if self.wallet.is_watching_only():
2170 self.show_message(_("This is a watching-only wallet"))
2174 d.setWindowTitle(_('Private keys'))
2175 d.setMinimumSize(850, 300)
2176 vbox = QVBoxLayout(d)
2178 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2179 _("Exposing a single private key can compromise your entire wallet!"),
2180 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2181 vbox.addWidget(QLabel(msg))
2187 defaultname = 'electrum-private-keys.csv'
2188 select_msg = _('Select file to export your private keys to')
2189 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2190 vbox.addLayout(hbox)
2192 h, b = ok_cancel_buttons2(d, _('Export'))
2197 addresses = self.wallet.addresses(True)
2199 def privkeys_thread():
2200 for addr in addresses:
2204 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2205 d.emit(SIGNAL('computing_privkeys'))
2206 d.emit(SIGNAL('show_privkeys'))
2208 def show_privkeys():
2209 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2213 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2214 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2215 threading.Thread(target=privkeys_thread).start()
2221 filename = filename_e.text()
2226 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2227 except (IOError, os.error), reason:
2228 export_error_label = _("Electrum was unable to produce a private key-export.")
2229 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2231 except Exception as e:
2232 self.show_message(str(e))
2235 self.show_message(_("Private keys exported."))
2238 def do_export_privkeys(self, fileName, pklist, is_csv):
2239 with open(fileName, "w+") as f:
2241 transaction = csv.writer(f)
2242 transaction.writerow(["address", "private_key"])
2243 for addr, pk in pklist.items():
2244 transaction.writerow(["%34s"%addr,pk])
2247 f.write(json.dumps(pklist, indent = 4))
2250 def do_import_labels(self):
2251 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2252 if not labelsFile: return
2254 f = open(labelsFile, 'r')
2257 for key, value in json.loads(data).items():
2258 self.wallet.set_label(key, value)
2259 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2260 except (IOError, os.error), reason:
2261 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2264 def do_export_labels(self):
2265 labels = self.wallet.labels
2267 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2269 with open(fileName, 'w+') as f:
2270 json.dump(labels, f)
2271 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2272 except (IOError, os.error), reason:
2273 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2276 def export_history_dialog(self):
2279 d.setWindowTitle(_('Export History'))
2280 d.setMinimumSize(400, 200)
2281 vbox = QVBoxLayout(d)
2283 defaultname = os.path.expanduser('~/electrum-history.csv')
2284 select_msg = _('Select file to export your wallet transactions to')
2286 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2287 vbox.addLayout(hbox)
2291 h, b = ok_cancel_buttons2(d, _('Export'))
2296 filename = filename_e.text()
2301 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2302 except (IOError, os.error), reason:
2303 export_error_label = _("Electrum was unable to produce a transaction export.")
2304 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2307 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2310 def do_export_history(self, wallet, fileName, is_csv):
2311 history = wallet.get_tx_history()
2313 for item in history:
2314 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2316 if timestamp is not None:
2318 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2319 except [RuntimeError, TypeError, NameError] as reason:
2320 time_string = "unknown"
2323 time_string = "unknown"
2325 time_string = "pending"
2327 if value is not None:
2328 value_string = format_satoshis(value, True)
2333 fee_string = format_satoshis(fee, True)
2338 label, is_default_label = wallet.get_label(tx_hash)
2339 label = label.encode('utf-8')
2343 balance_string = format_satoshis(balance, False)
2345 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2347 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2349 with open(fileName, "w+") as f:
2351 transaction = csv.writer(f)
2352 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2354 transaction.writerow(line)
2357 f.write(json.dumps(lines, indent = 4))
2360 def sweep_key_dialog(self):
2362 d.setWindowTitle(_('Sweep private keys'))
2363 d.setMinimumSize(600, 300)
2365 vbox = QVBoxLayout(d)
2366 vbox.addWidget(QLabel(_("Enter private keys")))
2368 keys_e = QTextEdit()
2369 keys_e.setTabChangesFocus(True)
2370 vbox.addWidget(keys_e)
2372 h, address_e = address_field(self.wallet.addresses())
2376 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2377 vbox.addLayout(hbox)
2378 button.setEnabled(False)
2381 addr = str(address_e.text())
2382 if bitcoin.is_address(addr):
2386 pk = str(keys_e.toPlainText()).strip()
2387 if Wallet.is_private_key(pk):
2390 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2391 keys_e.textChanged.connect(f)
2392 address_e.textChanged.connect(f)
2396 fee = self.wallet.fee
2397 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2398 self.show_transaction(tx)
2402 def do_import_privkey(self, password):
2403 if not self.wallet.has_imported_keys():
2404 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2405 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2406 + _('Are you sure you understand what you are doing?'), 3, 4)
2409 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2412 text = str(text).split()
2417 addr = self.wallet.import_key(key, password)
2418 except Exception as e:
2424 addrlist.append(addr)
2426 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2428 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2429 self.update_address_tab()
2430 self.update_history_tab()
2433 def settings_dialog(self):
2435 d.setWindowTitle(_('Electrum Settings'))
2437 vbox = QVBoxLayout()
2438 grid = QGridLayout()
2439 grid.setColumnStretch(0,1)
2441 nz_label = QLabel(_('Display zeros') + ':')
2442 grid.addWidget(nz_label, 0, 0)
2443 nz_e = AmountEdit(None,True)
2444 nz_e.setText("%d"% self.num_zeros)
2445 grid.addWidget(nz_e, 0, 1)
2446 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2447 grid.addWidget(HelpButton(msg), 0, 2)
2448 if not self.config.is_modifiable('num_zeros'):
2449 for w in [nz_e, nz_label]: w.setEnabled(False)
2451 lang_label=QLabel(_('Language') + ':')
2452 grid.addWidget(lang_label, 1, 0)
2453 lang_combo = QComboBox()
2454 from electrum.i18n import languages
2455 lang_combo.addItems(languages.values())
2457 index = languages.keys().index(self.config.get("language",''))
2460 lang_combo.setCurrentIndex(index)
2461 grid.addWidget(lang_combo, 1, 1)
2462 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2463 if not self.config.is_modifiable('language'):
2464 for w in [lang_combo, lang_label]: w.setEnabled(False)
2467 fee_label = QLabel(_('Transaction fee') + ':')
2468 grid.addWidget(fee_label, 2, 0)
2469 fee_e = BTCAmountEdit(self.get_decimal_point)
2470 fee_e.setAmount(self.wallet.fee)
2471 grid.addWidget(fee_e, 2, 1)
2472 msg = _('Fee per kilobyte of transaction.') + '\n' \
2473 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2474 grid.addWidget(HelpButton(msg), 2, 2)
2475 if not self.config.is_modifiable('fee_per_kb'):
2476 for w in [fee_e, fee_label]: w.setEnabled(False)
2478 units = ['BTC', 'mBTC']
2479 unit_label = QLabel(_('Base unit') + ':')
2480 grid.addWidget(unit_label, 3, 0)
2481 unit_combo = QComboBox()
2482 unit_combo.addItems(units)
2483 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2484 grid.addWidget(unit_combo, 3, 1)
2485 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2486 + '\n1BTC=1000mBTC.\n' \
2487 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2489 usechange_cb = QCheckBox(_('Use change addresses'))
2490 usechange_cb.setChecked(self.wallet.use_change)
2491 grid.addWidget(usechange_cb, 4, 0)
2492 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2493 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2495 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2496 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2497 grid.addWidget(block_ex_label, 5, 0)
2498 block_ex_combo = QComboBox()
2499 block_ex_combo.addItems(block_explorers)
2500 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2501 grid.addWidget(block_ex_combo, 5, 1)
2502 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2504 show_tx = self.config.get('show_before_broadcast', False)
2505 showtx_cb = QCheckBox(_('Show before broadcast'))
2506 showtx_cb.setChecked(show_tx)
2507 grid.addWidget(showtx_cb, 6, 0)
2508 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2510 vbox.addLayout(grid)
2512 vbox.addLayout(ok_cancel_buttons(d))
2516 if not d.exec_(): return
2518 fee = fee_e.get_amount()
2520 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2523 self.wallet.set_fee(fee)
2525 nz = unicode(nz_e.text())
2530 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2533 if self.num_zeros != nz:
2535 self.config.set_key('num_zeros', nz, True)
2536 self.update_history_tab()
2537 self.update_address_tab()
2539 usechange_result = usechange_cb.isChecked()
2540 if self.wallet.use_change != usechange_result:
2541 self.wallet.use_change = usechange_result
2542 self.wallet.storage.put('use_change', self.wallet.use_change)
2544 if showtx_cb.isChecked() != show_tx:
2545 self.config.set_key('show_before_broadcast', not show_tx)
2547 unit_result = units[unit_combo.currentIndex()]
2548 if self.base_unit() != unit_result:
2549 self.decimal_point = 8 if unit_result == 'BTC' else 5
2550 self.config.set_key('decimal_point', self.decimal_point, True)
2551 self.update_history_tab()
2552 self.update_status()
2554 need_restart = False
2556 lang_request = languages.keys()[lang_combo.currentIndex()]
2557 if lang_request != self.config.get('language'):
2558 self.config.set_key("language", lang_request, True)
2561 be_result = block_explorers[block_ex_combo.currentIndex()]
2562 self.config.set_key('block_explorer', be_result, True)
2564 run_hook('close_settings_dialog')
2567 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2570 def run_network_dialog(self):
2571 if not self.network:
2573 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2575 def closeEvent(self, event):
2577 self.config.set_key("is_maximized", self.isMaximized())
2578 if not self.isMaximized():
2580 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2581 self.save_column_widths()
2582 self.config.set_key("console-history", self.console.history[-50:], True)
2583 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2587 def plugins_dialog(self):
2588 from electrum.plugins import plugins
2591 d.setWindowTitle(_('Electrum Plugins'))
2594 vbox = QVBoxLayout(d)
2597 scroll = QScrollArea()
2598 scroll.setEnabled(True)
2599 scroll.setWidgetResizable(True)
2600 scroll.setMinimumSize(400,250)
2601 vbox.addWidget(scroll)
2605 w.setMinimumHeight(len(plugins)*35)
2607 grid = QGridLayout()
2608 grid.setColumnStretch(0,1)
2611 def do_toggle(cb, p, w):
2614 if w: w.setEnabled(r)
2616 def mk_toggle(cb, p, w):
2617 return lambda: do_toggle(cb,p,w)
2619 for i, p in enumerate(plugins):
2621 cb = QCheckBox(p.fullname())
2622 cb.setDisabled(not p.is_available())
2623 cb.setChecked(p.is_enabled())
2624 grid.addWidget(cb, i, 0)
2625 if p.requires_settings():
2626 w = p.settings_widget(self)
2627 w.setEnabled( p.is_enabled() )
2628 grid.addWidget(w, i, 1)
2631 cb.clicked.connect(mk_toggle(cb,p,w))
2632 grid.addWidget(HelpButton(p.description()), i, 2)
2634 print_msg(_("Error: cannot display plugin"), p)
2635 traceback.print_exc(file=sys.stdout)
2636 grid.setRowStretch(i+1,1)
2638 vbox.addLayout(close_button(d))
2643 def show_account_details(self, k):
2644 account = self.wallet.accounts[k]
2647 d.setWindowTitle(_('Account Details'))
2650 vbox = QVBoxLayout(d)
2651 name = self.wallet.get_account_name(k)
2652 label = QLabel('Name: ' + name)
2653 vbox.addWidget(label)
2655 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2657 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2659 vbox.addWidget(QLabel(_('Master Public Key:')))
2662 text.setReadOnly(True)
2663 text.setMaximumHeight(170)
2664 vbox.addWidget(text)
2666 mpk_text = '\n'.join( account.get_master_pubkeys() )
2667 text.setText(mpk_text)
2669 vbox.addLayout(close_button(d))