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.util 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
43 from electrum import Imported_Wallet
45 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
46 from network_dialog import NetworkDialog
47 from qrcodewidget import QRCodeWidget, QRDialog
48 from qrtextedit import QRTextEdit
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
67 # status of payment requests
70 PR_SENT = 2 # sent but not propagated
71 PR_PAID = 3 # send and propagated
72 PR_ERROR = 4 # could not parse
75 from electrum import ELECTRUM_VERSION
90 class StatusBarButton(QPushButton):
91 def __init__(self, icon, tooltip, func):
92 QPushButton.__init__(self, icon, '')
93 self.setToolTip(tooltip)
95 self.setMaximumWidth(25)
96 self.clicked.connect(func)
98 self.setIconSize(QSize(25,25))
100 def keyPressEvent(self, e):
101 if e.key() == QtCore.Qt.Key_Return:
113 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
115 class ElectrumWindow(QMainWindow):
119 def __init__(self, config, network, gui_object):
120 QMainWindow.__init__(self)
123 self.network = network
124 self.gui_object = gui_object
125 self.tray = gui_object.tray
126 self.go_lite = gui_object.go_lite
129 self.create_status_bar()
130 self.need_update = threading.Event()
132 self.decimal_point = config.get('decimal_point', 5)
133 self.num_zeros = int(config.get('num_zeros',0))
136 set_language(config.get('language'))
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 l.itemDoubleClicked.connect(self.tx_label_clicked)
503 l.itemChanged.connect(self.tx_label_changed)
504 l.customContextMenuRequested.connect(self.create_history_menu)
508 def create_history_menu(self, position):
509 self.history_list.selectedIndexes()
510 item = self.history_list.currentItem()
511 be = self.config.get('block_explorer', 'Blockchain.info')
512 if be == 'Blockchain.info':
513 block_explorer = 'https://blockchain.info/tx/'
514 elif be == 'Blockr.io':
515 block_explorer = 'https://blockr.io/tx/info/'
516 elif be == 'Insight.is':
517 block_explorer = 'http://live.insight.is/tx/'
519 tx_hash = str(item.data(0, Qt.UserRole).toString())
520 if not tx_hash: return
522 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
523 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
524 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
525 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
526 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
529 def show_transaction(self, tx):
530 import transaction_dialog
531 d = transaction_dialog.TxDialog(tx, self)
534 def tx_label_clicked(self, item, column):
535 if column==2 and item.isSelected():
537 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
538 self.history_list.editItem( item, column )
539 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
542 def tx_label_changed(self, item, column):
546 tx_hash = str(item.data(0, Qt.UserRole).toString())
547 tx = self.wallet.transactions.get(tx_hash)
548 text = unicode( item.text(2) )
549 self.wallet.set_label(tx_hash, text)
551 item.setForeground(2, QBrush(QColor('black')))
553 text = self.wallet.get_default_label(tx_hash)
554 item.setText(2, text)
555 item.setForeground(2, QBrush(QColor('gray')))
559 def edit_label(self, is_recv):
560 l = self.address_list if is_recv else self.contacts_list
561 item = l.currentItem()
562 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
563 l.editItem( item, 1 )
564 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568 def address_label_clicked(self, item, column, l, column_addr, column_label):
569 if column == column_label and item.isSelected():
570 is_editable = item.data(0, 32).toBool()
573 addr = unicode( item.text(column_addr) )
574 label = unicode( item.text(column_label) )
575 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
576 l.editItem( item, column )
577 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
580 def address_label_changed(self, item, column, l, column_addr, column_label):
581 if column == column_label:
582 addr = unicode( item.text(column_addr) )
583 text = unicode( item.text(column_label) )
584 is_editable = item.data(0, 32).toBool()
588 changed = self.wallet.set_label(addr, text)
590 self.update_history_tab()
591 self.update_completions()
593 self.current_item_changed(item)
595 run_hook('item_changed', item, column)
598 def current_item_changed(self, a):
599 run_hook('current_item_changed', a)
603 def update_history_tab(self):
605 self.history_list.clear()
606 for item in self.wallet.get_tx_history(self.current_account):
607 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
608 time_str = _("unknown")
611 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
613 time_str = _("error")
616 time_str = 'unverified'
617 icon = QIcon(":icons/unconfirmed.png")
620 icon = QIcon(":icons/unconfirmed.png")
622 icon = QIcon(":icons/clock%d.png"%conf)
624 icon = QIcon(":icons/confirmed.png")
626 if value is not None:
627 v_str = self.format_amount(value, True, whitespaces=True)
631 balance_str = self.format_amount(balance, whitespaces=True)
634 label, is_default_label = self.wallet.get_label(tx_hash)
636 label = _('Pruned transaction outputs')
637 is_default_label = False
639 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
640 item.setFont(2, QFont(MONOSPACE_FONT))
641 item.setFont(3, QFont(MONOSPACE_FONT))
642 item.setFont(4, QFont(MONOSPACE_FONT))
644 item.setForeground(3, QBrush(QColor("#BC1E1E")))
646 item.setData(0, Qt.UserRole, tx_hash)
647 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
649 item.setForeground(2, QBrush(QColor('grey')))
651 item.setIcon(0, icon)
652 self.history_list.insertTopLevelItem(0,item)
655 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
656 run_hook('history_tab_update')
659 def create_receive_tab(self):
661 grid = QGridLayout(w)
662 grid.setColumnMinimumWidth(3, 300)
663 grid.setColumnStretch(5, 1)
665 self.receive_address_e = QLineEdit()
666 self.receive_address_e.setReadOnly(True)
667 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
668 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
669 self.receive_address_e.textChanged.connect(self.update_receive_qr)
671 self.receive_message_e = QLineEdit()
672 grid.addWidget(QLabel(_('Message')), 1, 0)
673 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
674 self.receive_message_e.textChanged.connect(self.update_receive_qr)
676 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
677 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
678 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
679 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
681 self.save_request_button = QPushButton(_('Save'))
682 self.save_request_button.clicked.connect(self.save_payment_request)
683 grid.addWidget(self.save_request_button, 3, 1)
684 clear_button = QPushButton(_('New'))
685 clear_button.clicked.connect(self.new_receive_address)
686 grid.addWidget(clear_button, 3, 2)
687 grid.setRowStretch(4, 1)
689 self.receive_qr = QRCodeWidget(fixedSize=200)
690 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
692 grid.setRowStretch(5, 1)
694 self.receive_requests_label = QLabel(_('Saved Requests'))
695 self.receive_list = MyTreeWidget(self)
696 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
697 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
698 self.receive_list.itemClicked.connect(self.receive_item_changed)
699 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
700 self.receive_list.setColumnWidth(0, 340)
701 h = self.receive_list.header()
702 h.setStretchLastSection(False)
703 h.setResizeMode(1, QHeaderView.Stretch)
705 grid.addWidget(self.receive_requests_label, 6, 0)
706 grid.addWidget(self.receive_list, 7, 0, 1, 6)
709 def receive_item_changed(self, item):
712 addr = str(item.text(0))
713 amount, message = self.receive_requests[addr]
714 self.receive_address_e.setText(addr)
715 self.receive_message_e.setText(message)
716 self.receive_amount_e.setAmount(amount)
719 def receive_list_delete(self, item):
720 addr = str(item.text(0))
721 self.receive_requests.pop(addr)
722 self.update_receive_tab()
723 self.clear_receive_tab()
725 def receive_list_menu(self, position):
726 item = self.receive_list.itemAt(position)
728 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
729 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
731 def save_payment_request(self):
732 addr = str(self.receive_address_e.text())
733 amount = self.receive_amount_e.get_amount()
734 message = str(self.receive_message_e.text())
735 if not message and not amount:
736 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
738 self.receive_requests = self.wallet.storage.get('receive_requests',{})
739 self.receive_requests[addr] = (amount, message)
740 self.wallet.storage.put('receive_requests', self.receive_requests)
741 self.update_receive_tab()
743 def new_receive_address(self):
744 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
746 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
749 if isinstance(self.wallet, Imported_Wallet):
750 self.show_message(_('No more addresses in your wallet.'))
752 if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
754 addr = self.wallet.create_new_address(self.current_account, False)
755 self.receive_address_e.setText(addr)
756 self.receive_message_e.setText('')
757 self.receive_amount_e.setAmount(None)
759 def clear_receive_tab(self):
760 self.receive_requests = self.wallet.storage.get('receive_requests',{})
761 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
763 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
767 self.receive_address_e.setText(addr)
768 self.receive_message_e.setText('')
769 self.receive_amount_e.setAmount(None)
771 def receive_at(self, addr):
772 if not bitcoin.is_address(addr):
774 self.tabs.setCurrentIndex(2)
775 self.receive_address_e.setText(addr)
777 def update_receive_tab(self):
778 self.receive_requests = self.wallet.storage.get('receive_requests',{})
779 b = len(self.receive_requests) > 0
780 self.receive_list.setVisible(b)
781 self.receive_requests_label.setVisible(b)
783 self.receive_list.clear()
784 for address, v in self.receive_requests.items():
786 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
787 item.setFont(0, QFont(MONOSPACE_FONT))
788 self.receive_list.addTopLevelItem(item)
791 def update_receive_qr(self):
792 import urlparse, urllib
793 addr = str(self.receive_address_e.text())
794 amount = self.receive_amount_e.get_amount()
795 message = unicode(self.receive_message_e.text()).encode('utf8')
796 self.save_request_button.setEnabled((amount is not None) or (message != ""))
800 query.append('amount=%s'%format_satoshis(amount))
802 query.append('message=%s'%urllib.quote(message))
803 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
804 url = urlparse.urlunparse(p)
807 self.receive_qr.setData(url)
808 run_hook('update_receive_qr', addr, amount, message, url)
811 def create_send_tab(self):
814 self.send_grid = grid = QGridLayout(w)
816 grid.setColumnMinimumWidth(3,300)
817 grid.setColumnStretch(5,1)
818 grid.setRowStretch(8, 1)
820 from paytoedit import PayToEdit
821 self.amount_e = BTCAmountEdit(self.get_decimal_point)
822 self.payto_e = PayToEdit(self)
823 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)'))
824 grid.addWidget(QLabel(_('Pay to')), 1, 0)
825 grid.addWidget(self.payto_e, 1, 1, 1, 3)
826 grid.addWidget(self.payto_help, 1, 4)
828 completer = QCompleter()
829 completer.setCaseSensitivity(False)
830 self.payto_e.setCompleter(completer)
831 completer.setModel(self.completions)
833 self.message_e = MyLineEdit()
834 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.'))
835 grid.addWidget(QLabel(_('Description')), 2, 0)
836 grid.addWidget(self.message_e, 2, 1, 1, 3)
837 grid.addWidget(self.message_help, 2, 4)
839 self.from_label = QLabel(_('From'))
840 grid.addWidget(self.from_label, 3, 0)
841 self.from_list = MyTreeWidget(self)
842 self.from_list.setColumnCount(2)
843 self.from_list.setColumnWidth(0, 350)
844 self.from_list.setColumnWidth(1, 50)
845 self.from_list.setHeaderHidden(True)
846 self.from_list.setMaximumHeight(80)
847 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
848 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
849 grid.addWidget(self.from_list, 3, 1, 1, 3)
850 self.set_pay_from([])
852 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
853 + _('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.') \
854 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
855 grid.addWidget(QLabel(_('Amount')), 4, 0)
856 grid.addWidget(self.amount_e, 4, 1, 1, 2)
857 grid.addWidget(self.amount_help, 4, 3)
859 self.fee_e = BTCAmountEdit(self.get_decimal_point)
860 grid.addWidget(QLabel(_('Fee')), 5, 0)
861 grid.addWidget(self.fee_e, 5, 1, 1, 2)
862 grid.addWidget(HelpButton(
863 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
864 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
865 + _('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)
867 self.send_button = EnterButton(_("Send"), self.do_send)
868 grid.addWidget(self.send_button, 6, 1)
870 b = EnterButton(_("Clear"), self.do_clear)
871 grid.addWidget(b, 6, 2)
873 self.payto_sig = QLabel('')
874 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
876 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
877 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
880 def entry_changed( is_fee ):
882 if self.amount_e.is_shortcut:
883 self.amount_e.is_shortcut = False
884 sendable = self.get_sendable_balance()
885 # there is only one output because we are completely spending inputs
886 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
887 fee = self.wallet.estimated_fee(inputs, 1)
889 self.amount_e.setAmount(amount)
890 self.amount_e.textEdited.emit("")
891 self.fee_e.setAmount(fee)
894 amount = self.amount_e.get_amount()
895 fee = self.fee_e.get_amount()
896 outputs = self.payto_e.get_outputs()
902 self.fee_e.setAmount(None)
903 not_enough_funds = False
905 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, len(outputs), coins = self.get_coins())
906 not_enough_funds = len(inputs) == 0
908 self.fee_e.setAmount(fee)
910 if not not_enough_funds:
912 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
916 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
917 text = _( "Not enough funds" )
918 c, u = self.wallet.get_frozen_balance()
919 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
921 self.statusBar().showMessage(text)
922 self.amount_e.setPalette(palette)
923 self.fee_e.setPalette(palette)
925 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
926 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
928 run_hook('create_send_tab', grid)
931 def from_list_delete(self, item):
932 i = self.from_list.indexOfTopLevelItem(item)
934 self.redraw_from_list()
936 def from_list_menu(self, position):
937 item = self.from_list.itemAt(position)
939 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
940 menu.exec_(self.from_list.viewport().mapToGlobal(position))
942 def set_pay_from(self, domain = None):
943 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
944 self.redraw_from_list()
946 def redraw_from_list(self):
947 self.from_list.clear()
948 self.from_label.setHidden(len(self.pay_from) == 0)
949 self.from_list.setHidden(len(self.pay_from) == 0)
952 h = x.get('prevout_hash')
953 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
955 for item in self.pay_from:
956 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
958 def update_completions(self):
960 for addr,label in self.wallet.labels.items():
961 if addr in self.wallet.addressbook:
962 l.append( label + ' <' + addr + '>')
964 run_hook('update_completions', l)
965 self.completions.setStringList(l)
969 return lambda s, *args: s.do_protect(func, args)
972 def read_send_tab(self):
974 if self.payment_request and self.payment_request.has_expired():
975 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
978 label = unicode( self.message_e.text() )
980 if self.payment_request:
981 outputs = self.payment_request.get_outputs()
983 outputs = self.payto_e.get_outputs()
986 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
989 for addr, x in outputs:
990 if addr is None or not bitcoin.is_address(addr):
991 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
994 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
997 amount = sum(map(lambda x:x[1], outputs))
999 fee = self.fee_e.get_amount()
1001 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
1004 confirm_amount = self.config.get('confirm_amount', 100000000)
1005 if amount >= confirm_amount:
1006 o = '\n'.join(map(lambda x:x[0], outputs))
1007 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
1010 confirm_fee = self.config.get('confirm_fee', 100000)
1011 if fee >= confirm_fee:
1012 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()}):
1015 coins = self.get_coins()
1016 return outputs, fee, label, coins
1020 r = self.read_send_tab()
1023 outputs, fee, label, coins = r
1024 self.send_tx(outputs, fee, label, coins)
1028 def send_tx(self, outputs, fee, label, coins, password):
1029 self.send_button.setDisabled(True)
1031 # first, create an unsigned tx
1033 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1035 except Exception as e:
1036 traceback.print_exc(file=sys.stdout)
1037 self.show_message(str(e))
1038 self.send_button.setDisabled(False)
1041 # call hook to see if plugin needs gui interaction
1042 run_hook('send_tx', tx)
1048 self.wallet.add_keypairs(tx, keypairs, password)
1049 self.wallet.sign_transaction(tx, keypairs, password)
1050 except Exception as e:
1056 self.show_message(tx.error)
1057 self.send_button.setDisabled(False)
1059 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1060 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1061 self.send_button.setDisabled(False)
1064 self.wallet.set_label(tx.hash(), label)
1066 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1067 self.show_transaction(tx)
1069 self.send_button.setDisabled(False)
1072 self.broadcast_transaction(tx)
1074 # keep a reference to WaitingDialog or the gui might crash
1075 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1076 self.waiting_dialog.start()
1080 def broadcast_transaction(self, tx):
1082 def broadcast_thread():
1083 pr = self.payment_request
1085 return self.wallet.sendtx(tx)
1087 if pr.has_expired():
1088 self.payment_request = None
1089 return False, _("Payment request has expired")
1091 status, msg = self.wallet.sendtx(tx)
1095 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1096 self.wallet.storage.put('invoices', self.invoices)
1097 self.update_invoices_tab()
1098 self.payment_request = None
1099 refund_address = self.wallet.addresses()[0]
1100 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1106 def broadcast_done(status, msg):
1108 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1111 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1112 self.send_button.setDisabled(False)
1114 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1115 self.waiting_dialog.start()
1119 def prepare_for_payment_request(self):
1120 self.tabs.setCurrentIndex(1)
1121 self.payto_e.is_pr = True
1122 for e in [self.payto_e, self.amount_e, self.message_e]:
1124 for h in [self.payto_help, self.amount_help, self.message_help]:
1126 self.payto_e.setText(_("please wait..."))
1129 def payment_request_ok(self):
1130 pr = self.payment_request
1132 if pr_id not in self.invoices:
1133 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1134 self.wallet.storage.put('invoices', self.invoices)
1135 self.update_invoices_tab()
1137 print_error('invoice already in list')
1139 status = self.invoices[pr_id][4]
1140 if status == PR_PAID:
1142 self.show_message("invoice already paid")
1143 self.payment_request = None
1146 self.payto_help.show()
1147 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1149 if not pr.has_expired():
1150 self.payto_e.setGreen()
1152 self.payto_e.setExpired()
1154 self.payto_e.setText(pr.domain)
1155 self.amount_e.setText(self.format_amount(pr.get_amount()))
1156 self.message_e.setText(pr.get_memo())
1158 def payment_request_error(self):
1160 self.show_message(self.payment_request.error)
1161 self.payment_request = None
1163 def pay_from_URI(self,URI):
1166 address, amount, label, message, request_url = util.parse_URI(URI)
1168 address, amount, label, message, request_url = util.parse_URI(URI)
1169 except Exception as e:
1170 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1173 self.tabs.setCurrentIndex(1)
1177 if self.wallet.labels.get(address) != label:
1178 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1179 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1180 self.wallet.addressbook.append(address)
1181 self.wallet.set_label(address, label)
1183 label = self.wallet.labels.get(address)
1185 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1187 self.message_e.setText(message)
1189 self.amount_e.setAmount(amount)
1192 from electrum import paymentrequest
1193 def payment_request():
1194 self.payment_request = paymentrequest.PaymentRequest(self.config)
1195 self.payment_request.read(request_url)
1196 if self.payment_request.verify():
1197 self.emit(SIGNAL('payment_request_ok'))
1199 self.emit(SIGNAL('payment_request_error'))
1201 self.pr_thread = threading.Thread(target=payment_request).start()
1202 self.prepare_for_payment_request()
1207 self.payto_e.is_pr = False
1208 self.payto_sig.setVisible(False)
1209 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1213 for h in [self.payto_help, self.amount_help, self.message_help]:
1216 self.payto_help.set_alt(None)
1217 self.set_pay_from([])
1218 self.update_status()
1222 def set_addrs_frozen(self,addrs,freeze):
1224 if not addr: continue
1225 if addr in self.wallet.frozen_addresses and not freeze:
1226 self.wallet.unfreeze(addr)
1227 elif addr not in self.wallet.frozen_addresses and freeze:
1228 self.wallet.freeze(addr)
1229 self.update_address_tab()
1233 def create_list_tab(self, headers):
1234 "generic tab creation method"
1235 l = MyTreeWidget(self)
1236 l.setColumnCount( len(headers) )
1237 l.setHeaderLabels( headers )
1240 vbox = QVBoxLayout()
1247 vbox.addWidget(buttons)
1252 def create_addresses_tab(self):
1253 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1254 for i,width in enumerate(self.column_widths['receive']):
1255 l.setColumnWidth(i, width)
1256 l.setContextMenuPolicy(Qt.CustomContextMenu)
1257 l.customContextMenuRequested.connect(self.create_receive_menu)
1258 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1259 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1260 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1261 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1262 self.address_list = l
1268 def save_column_widths(self):
1269 self.column_widths["receive"] = []
1270 for i in range(self.address_list.columnCount() -1):
1271 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1273 self.column_widths["history"] = []
1274 for i in range(self.history_list.columnCount() - 1):
1275 self.column_widths["history"].append(self.history_list.columnWidth(i))
1277 self.column_widths["contacts"] = []
1278 for i in range(self.contacts_list.columnCount() - 1):
1279 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1281 self.config.set_key("column_widths_2", self.column_widths, True)
1284 def create_contacts_tab(self):
1285 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1286 l.setContextMenuPolicy(Qt.CustomContextMenu)
1287 l.customContextMenuRequested.connect(self.create_contact_menu)
1288 for i,width in enumerate(self.column_widths['contacts']):
1289 l.setColumnWidth(i, width)
1290 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1291 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1292 self.contacts_list = l
1296 def create_invoices_tab(self):
1297 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1298 l.setColumnWidth(0, 150)
1300 h.setStretchLastSection(False)
1301 h.setResizeMode(1, QHeaderView.Stretch)
1302 l.setContextMenuPolicy(Qt.CustomContextMenu)
1303 l.customContextMenuRequested.connect(self.create_invoice_menu)
1304 self.invoices_list = l
1307 def update_invoices_tab(self):
1308 invoices = self.wallet.storage.get('invoices', {})
1309 l = self.invoices_list
1311 for key, value in invoices.items():
1313 domain, memo, amount, expiration_date, status, tx_hash = value
1317 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1319 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1320 l.addTopLevelItem(item)
1322 l.setCurrentItem(l.topLevelItem(0))
1326 def delete_imported_key(self, addr):
1327 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1328 self.wallet.delete_imported_key(addr)
1329 self.update_address_tab()
1330 self.update_history_tab()
1332 def edit_account_label(self, k):
1333 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1335 label = unicode(text)
1336 self.wallet.set_label(k,label)
1337 self.update_address_tab()
1339 def account_set_expanded(self, item, k, b):
1341 self.accounts_expanded[k] = b
1343 def create_account_menu(self, position, k, item):
1345 if item.isExpanded():
1346 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1348 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1349 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1350 if self.wallet.seed_version > 4:
1351 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1352 if self.wallet.account_is_pending(k):
1353 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1354 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1356 def delete_pending_account(self, k):
1357 self.wallet.delete_pending_account(k)
1358 self.update_address_tab()
1360 def create_receive_menu(self, position):
1361 # fixme: this function apparently has a side effect.
1362 # if it is not called the menu pops up several times
1363 #self.address_list.selectedIndexes()
1365 selected = self.address_list.selectedItems()
1366 multi_select = len(selected) > 1
1367 addrs = [unicode(item.text(0)) for item in selected]
1368 if not multi_select:
1369 item = self.address_list.itemAt(position)
1373 if not is_valid(addr):
1374 k = str(item.data(0,32).toString())
1376 self.create_account_menu(position, k, item)
1378 item.setExpanded(not item.isExpanded())
1382 if not multi_select:
1383 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1384 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1385 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1386 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1387 if not self.wallet.is_watching_only():
1388 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1389 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1390 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1391 if self.wallet.is_imported(addr):
1392 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1394 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1395 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1396 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1397 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1400 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1401 if any(can_send(addr) for addr in addrs):
1402 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1404 run_hook('receive_menu', menu, addrs)
1405 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1408 def get_sendable_balance(self):
1409 return sum(map(lambda x:x['value'], self.get_coins()))
1412 def get_coins(self):
1414 return self.pay_from
1416 domain = self.wallet.get_account_addresses(self.current_account)
1417 for i in self.wallet.frozen_addresses:
1418 if i in domain: domain.remove(i)
1419 return self.wallet.get_unspent_coins(domain)
1422 def send_from_addresses(self, addrs):
1423 self.set_pay_from( addrs )
1424 self.tabs.setCurrentIndex(1)
1427 def payto(self, addr):
1429 label = self.wallet.labels.get(addr)
1430 m_addr = label + ' <' + addr + '>' if label else addr
1431 self.tabs.setCurrentIndex(1)
1432 self.payto_e.setText(m_addr)
1433 self.amount_e.setFocus()
1436 def delete_contact(self, x):
1437 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1438 self.wallet.delete_contact(x)
1439 self.wallet.set_label(x, None)
1440 self.update_history_tab()
1441 self.update_contacts_tab()
1442 self.update_completions()
1445 def create_contact_menu(self, position):
1446 item = self.contacts_list.itemAt(position)
1449 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1451 addr = unicode(item.text(0))
1452 label = unicode(item.text(1))
1453 is_editable = item.data(0,32).toBool()
1454 payto_addr = item.data(0,33).toString()
1455 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1456 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1457 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1459 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1460 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1462 run_hook('create_contact_menu', menu, item)
1463 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1465 def delete_invoice(self, key):
1466 self.invoices.pop(key)
1467 self.wallet.storage.put('invoices', self.invoices)
1468 self.update_invoices_tab()
1470 def show_invoice(self, key):
1471 from electrum.paymentrequest import PaymentRequest
1472 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1473 pr = PaymentRequest(self.config)
1477 self.show_pr_details(pr)
1479 def show_pr_details(self, pr):
1480 msg = 'Domain: ' + pr.domain
1481 msg += '\nStatus: ' + pr.get_status()
1482 msg += '\nMemo: ' + pr.get_memo()
1483 msg += '\nPayment URL: ' + pr.payment_url
1484 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1485 QMessageBox.information(self, 'Invoice', msg , 'OK')
1487 def do_pay_invoice(self, key):
1488 from electrum.paymentrequest import PaymentRequest
1489 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1490 pr = PaymentRequest(self.config)
1493 self.payment_request = pr
1494 self.prepare_for_payment_request()
1496 self.payment_request_ok()
1498 self.payment_request_error()
1501 def create_invoice_menu(self, position):
1502 item = self.invoices_list.itemAt(position)
1505 k = self.invoices_list.indexOfTopLevelItem(item)
1506 key = self.invoices.keys()[k]
1507 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1509 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1510 if status == PR_UNPAID:
1511 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1512 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1513 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1517 def update_address_tab(self):
1518 l = self.address_list
1519 # extend the syntax for consistency
1520 l.addChild = l.addTopLevelItem
1521 l.insertChild = l.insertTopLevelItem
1525 accounts = self.wallet.get_accounts()
1526 if self.current_account is None:
1527 account_items = sorted(accounts.items())
1529 account_items = [(self.current_account, accounts.get(self.current_account))]
1532 for k, account in account_items:
1534 if len(accounts) > 1:
1535 name = self.wallet.get_account_name(k)
1536 c,u = self.wallet.get_account_balance(k)
1537 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1538 l.addTopLevelItem(account_item)
1539 account_item.setExpanded(self.accounts_expanded.get(k, True))
1540 account_item.setData(0, 32, k)
1544 sequences = [0,1] if account.has_change() else [0]
1545 for is_change in sequences:
1546 if len(sequences) > 1:
1547 name = _("Receiving") if not is_change else _("Change")
1548 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1549 account_item.addChild(seq_item)
1551 seq_item.setExpanded(True)
1553 seq_item = account_item
1555 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1558 addr_list = account.get_addresses(is_change)
1559 for address in addr_list:
1560 num, is_used = self.wallet.is_used(address)
1561 label = self.wallet.labels.get(address,'')
1562 c, u = self.wallet.get_addr_balance(address)
1563 balance = self.format_amount(c + u)
1564 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1565 item.setFont(0, QFont(MONOSPACE_FONT))
1566 item.setData(0, 32, True) # label can be edited
1567 if address in self.wallet.frozen_addresses:
1568 item.setBackgroundColor(0, QColor('lightblue'))
1569 if self.wallet.is_beyond_limit(address, account, is_change):
1570 item.setBackgroundColor(0, QColor('red'))
1573 seq_item.insertChild(0, used_item)
1575 used_item.addChild(item)
1577 seq_item.addChild(item)
1579 # we use column 1 because column 0 may be hidden
1580 l.setCurrentItem(l.topLevelItem(0),1)
1583 def update_contacts_tab(self):
1584 l = self.contacts_list
1587 for address in self.wallet.addressbook:
1588 label = self.wallet.labels.get(address,'')
1589 n = self.wallet.get_num_tx(address)
1590 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1591 item.setFont(0, QFont(MONOSPACE_FONT))
1592 # 32 = label can be edited (bool)
1593 item.setData(0,32, True)
1595 item.setData(0,33, address)
1596 l.addTopLevelItem(item)
1598 run_hook('update_contacts_tab', l)
1599 l.setCurrentItem(l.topLevelItem(0))
1602 def create_console_tab(self):
1603 from console import Console
1604 self.console = console = Console()
1608 def update_console(self):
1609 console = self.console
1610 console.history = self.config.get("console-history",[])
1611 console.history_index = len(console.history)
1613 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1614 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1616 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1618 def mkfunc(f, method):
1619 return lambda *args: apply( f, (method, args, self.password_dialog ))
1621 if m[0]=='_' or m in ['network','wallet']: continue
1622 methods[m] = mkfunc(c._run, m)
1624 console.updateNamespace(methods)
1627 def change_account(self,s):
1628 if s == _("All accounts"):
1629 self.current_account = None
1631 accounts = self.wallet.get_account_names()
1632 for k, v in accounts.items():
1634 self.current_account = k
1635 self.update_history_tab()
1636 self.update_status()
1637 self.update_address_tab()
1638 self.update_receive_tab()
1640 def create_status_bar(self):
1643 sb.setFixedHeight(35)
1644 qtVersion = qVersion()
1646 self.balance_label = QLabel("")
1647 sb.addWidget(self.balance_label)
1649 from version_getter import UpdateLabel
1650 self.updatelabel = UpdateLabel(self.config, sb)
1652 self.account_selector = QComboBox()
1653 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1654 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1655 sb.addPermanentWidget(self.account_selector)
1657 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1658 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1660 self.lock_icon = QIcon()
1661 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1662 sb.addPermanentWidget( self.password_button )
1664 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1665 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1666 sb.addPermanentWidget( self.seed_button )
1667 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1668 sb.addPermanentWidget( self.status_button )
1670 run_hook('create_status_bar', (sb,))
1672 self.setStatusBar(sb)
1675 def update_lock_icon(self):
1676 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1677 self.password_button.setIcon( icon )
1680 def update_buttons_on_seed(self):
1681 if self.wallet.has_seed():
1682 self.seed_button.show()
1684 self.seed_button.hide()
1686 if not self.wallet.is_watching_only():
1687 self.password_button.show()
1688 self.send_button.setText(_("Send"))
1690 self.password_button.hide()
1691 self.send_button.setText(_("Create unsigned transaction"))
1694 def change_password_dialog(self):
1695 from password_dialog import PasswordDialog
1696 d = PasswordDialog(self.wallet, self)
1698 self.update_lock_icon()
1701 def new_contact_dialog(self):
1704 d.setWindowTitle(_("New Contact"))
1705 vbox = QVBoxLayout(d)
1706 vbox.addWidget(QLabel(_('New Contact')+':'))
1708 grid = QGridLayout()
1711 grid.addWidget(QLabel(_("Address")), 1, 0)
1712 grid.addWidget(line1, 1, 1)
1713 grid.addWidget(QLabel(_("Name")), 2, 0)
1714 grid.addWidget(line2, 2, 1)
1716 vbox.addLayout(grid)
1717 vbox.addLayout(ok_cancel_buttons(d))
1722 address = str(line1.text())
1723 label = unicode(line2.text())
1725 if not is_valid(address):
1726 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1729 self.wallet.add_contact(address)
1731 self.wallet.set_label(address, label)
1733 self.update_contacts_tab()
1734 self.update_history_tab()
1735 self.update_completions()
1736 self.tabs.setCurrentIndex(3)
1740 def new_account_dialog(self, password):
1742 dialog = QDialog(self)
1744 dialog.setWindowTitle(_("New Account"))
1746 vbox = QVBoxLayout()
1747 vbox.addWidget(QLabel(_('Account name')+':'))
1750 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1751 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1756 vbox.addLayout(ok_cancel_buttons(dialog))
1757 dialog.setLayout(vbox)
1761 name = str(e.text())
1764 self.wallet.create_pending_account(name, password)
1765 self.update_address_tab()
1766 self.tabs.setCurrentIndex(2)
1771 def show_master_public_keys(self):
1773 dialog = QDialog(self)
1775 dialog.setWindowTitle(_("Master Public Keys"))
1777 main_layout = QGridLayout()
1778 mpk_dict = self.wallet.get_master_public_keys()
1780 for key, value in mpk_dict.items():
1781 main_layout.addWidget(QLabel(key), i, 0)
1782 mpk_text = QTextEdit()
1783 mpk_text.setReadOnly(True)
1784 mpk_text.setMaximumHeight(170)
1785 mpk_text.setText(value)
1786 main_layout.addWidget(mpk_text, i + 1, 0)
1789 vbox = QVBoxLayout()
1790 vbox.addLayout(main_layout)
1791 vbox.addLayout(close_button(dialog))
1793 dialog.setLayout(vbox)
1798 def show_seed_dialog(self, password):
1799 if not self.wallet.has_seed():
1800 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1804 mnemonic = self.wallet.get_mnemonic(password)
1806 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1808 from seed_dialog import SeedDialog
1809 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1814 def show_qrcode(self, data, title = _("QR code")):
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"
2054 return Transaction(txt)
2056 traceback.print_exc(file=sys.stdout)
2057 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2061 tx_dict = json.loads(str(txt))
2062 assert "hex" in tx_dict.keys()
2063 tx = Transaction(tx_dict["hex"])
2064 #if tx_dict.has_key("input_info"):
2065 # input_info = json.loads(tx_dict['input_info'])
2066 # tx.add_input_info(input_info)
2069 traceback.print_exc(file=sys.stdout)
2070 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2074 def read_tx_from_file(self):
2075 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2079 with open(fileName, "r") as f:
2080 file_content = f.read()
2081 except (ValueError, IOError, os.error), reason:
2082 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2084 return self.tx_from_text(file_content)
2088 def sign_raw_transaction(self, tx, password):
2090 self.wallet.signrawtransaction(tx, [], password)
2091 except Exception as e:
2092 traceback.print_exc(file=sys.stdout)
2093 QMessageBox.warning(self, _("Error"), str(e))
2095 def do_process_from_text(self):
2096 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2099 tx = self.tx_from_text(text)
2101 self.show_transaction(tx)
2103 def do_process_from_file(self):
2104 tx = self.read_tx_from_file()
2106 self.show_transaction(tx)
2108 def do_process_from_txid(self):
2109 from electrum import transaction
2110 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2112 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2114 tx = transaction.Transaction(r)
2116 self.show_transaction(tx)
2118 self.show_message("unknown transaction")
2120 def do_process_from_csvReader(self, csvReader):
2125 for position, row in enumerate(csvReader):
2127 if not is_valid(address):
2128 errors.append((position, address))
2130 amount = Decimal(row[1])
2131 amount = int(100000000*amount)
2132 outputs.append((address, amount))
2133 except (ValueError, IOError, os.error), reason:
2134 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2138 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2139 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2143 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2144 except Exception as e:
2145 self.show_message(str(e))
2148 self.show_transaction(tx)
2150 def do_process_from_csv_file(self):
2151 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2155 with open(fileName, "r") as f:
2156 csvReader = csv.reader(f)
2157 self.do_process_from_csvReader(csvReader)
2158 except (ValueError, IOError, os.error), reason:
2159 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2162 def do_process_from_csv_text(self):
2163 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2164 + _("Format: address, amount. One output per line"), _("Load CSV"))
2167 f = StringIO.StringIO(text)
2168 csvReader = csv.reader(f)
2169 self.do_process_from_csvReader(csvReader)
2174 def export_privkeys_dialog(self, password):
2175 if self.wallet.is_watching_only():
2176 self.show_message(_("This is a watching-only wallet"))
2180 d.setWindowTitle(_('Private keys'))
2181 d.setMinimumSize(850, 300)
2182 vbox = QVBoxLayout(d)
2184 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2185 _("Exposing a single private key can compromise your entire wallet!"),
2186 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2187 vbox.addWidget(QLabel(msg))
2193 defaultname = 'electrum-private-keys.csv'
2194 select_msg = _('Select file to export your private keys to')
2195 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2196 vbox.addLayout(hbox)
2198 h, b = ok_cancel_buttons2(d, _('Export'))
2203 addresses = self.wallet.addresses(True)
2205 def privkeys_thread():
2206 for addr in addresses:
2210 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2211 d.emit(SIGNAL('computing_privkeys'))
2212 d.emit(SIGNAL('show_privkeys'))
2214 def show_privkeys():
2215 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2219 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2220 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2221 threading.Thread(target=privkeys_thread).start()
2227 filename = filename_e.text()
2232 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2233 except (IOError, os.error), reason:
2234 export_error_label = _("Electrum was unable to produce a private key-export.")
2235 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2237 except Exception as e:
2238 self.show_message(str(e))
2241 self.show_message(_("Private keys exported."))
2244 def do_export_privkeys(self, fileName, pklist, is_csv):
2245 with open(fileName, "w+") as f:
2247 transaction = csv.writer(f)
2248 transaction.writerow(["address", "private_key"])
2249 for addr, pk in pklist.items():
2250 transaction.writerow(["%34s"%addr,pk])
2253 f.write(json.dumps(pklist, indent = 4))
2256 def do_import_labels(self):
2257 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2258 if not labelsFile: return
2260 f = open(labelsFile, 'r')
2263 for key, value in json.loads(data).items():
2264 self.wallet.set_label(key, value)
2265 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2266 except (IOError, os.error), reason:
2267 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2270 def do_export_labels(self):
2271 labels = self.wallet.labels
2273 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2275 with open(fileName, 'w+') as f:
2276 json.dump(labels, f)
2277 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2278 except (IOError, os.error), reason:
2279 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2282 def export_history_dialog(self):
2285 d.setWindowTitle(_('Export History'))
2286 d.setMinimumSize(400, 200)
2287 vbox = QVBoxLayout(d)
2289 defaultname = os.path.expanduser('~/electrum-history.csv')
2290 select_msg = _('Select file to export your wallet transactions to')
2292 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2293 vbox.addLayout(hbox)
2297 h, b = ok_cancel_buttons2(d, _('Export'))
2302 filename = filename_e.text()
2307 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2308 except (IOError, os.error), reason:
2309 export_error_label = _("Electrum was unable to produce a transaction export.")
2310 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2313 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2316 def do_export_history(self, wallet, fileName, is_csv):
2317 history = wallet.get_tx_history()
2319 for item in history:
2320 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2322 if timestamp is not None:
2324 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2325 except [RuntimeError, TypeError, NameError] as reason:
2326 time_string = "unknown"
2329 time_string = "unknown"
2331 time_string = "pending"
2333 if value is not None:
2334 value_string = format_satoshis(value, True)
2339 fee_string = format_satoshis(fee, True)
2344 label, is_default_label = wallet.get_label(tx_hash)
2345 label = label.encode('utf-8')
2349 balance_string = format_satoshis(balance, False)
2351 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2353 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2355 with open(fileName, "w+") as f:
2357 transaction = csv.writer(f)
2358 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2360 transaction.writerow(line)
2363 f.write(json.dumps(lines, indent = 4))
2366 def sweep_key_dialog(self):
2368 d.setWindowTitle(_('Sweep private keys'))
2369 d.setMinimumSize(600, 300)
2371 vbox = QVBoxLayout(d)
2372 vbox.addWidget(QLabel(_("Enter private keys")))
2374 keys_e = QTextEdit()
2375 keys_e.setTabChangesFocus(True)
2376 vbox.addWidget(keys_e)
2378 h, address_e = address_field(self.wallet.addresses())
2382 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2383 vbox.addLayout(hbox)
2384 button.setEnabled(False)
2387 addr = str(address_e.text())
2388 if bitcoin.is_address(addr):
2392 pk = str(keys_e.toPlainText()).strip()
2393 if Wallet.is_private_key(pk):
2396 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2397 keys_e.textChanged.connect(f)
2398 address_e.textChanged.connect(f)
2402 fee = self.wallet.fee
2403 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2404 self.show_transaction(tx)
2408 def do_import_privkey(self, password):
2409 if not self.wallet.has_imported_keys():
2410 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2411 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2412 + _('Are you sure you understand what you are doing?'), 3, 4)
2415 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2418 text = str(text).split()
2423 addr = self.wallet.import_key(key, password)
2424 except Exception as e:
2430 addrlist.append(addr)
2432 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2434 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2435 self.update_address_tab()
2436 self.update_history_tab()
2439 def settings_dialog(self):
2441 d.setWindowTitle(_('Electrum Settings'))
2443 vbox = QVBoxLayout()
2444 grid = QGridLayout()
2445 grid.setColumnStretch(0,1)
2447 nz_label = QLabel(_('Display zeros') + ':')
2448 grid.addWidget(nz_label, 0, 0)
2449 nz_e = AmountEdit(None,True)
2450 nz_e.setText("%d"% self.num_zeros)
2451 grid.addWidget(nz_e, 0, 1)
2452 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2453 grid.addWidget(HelpButton(msg), 0, 2)
2454 if not self.config.is_modifiable('num_zeros'):
2455 for w in [nz_e, nz_label]: w.setEnabled(False)
2457 lang_label=QLabel(_('Language') + ':')
2458 grid.addWidget(lang_label, 1, 0)
2459 lang_combo = QComboBox()
2460 from electrum.i18n import languages
2461 lang_combo.addItems(languages.values())
2463 index = languages.keys().index(self.config.get("language",''))
2466 lang_combo.setCurrentIndex(index)
2467 grid.addWidget(lang_combo, 1, 1)
2468 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2469 if not self.config.is_modifiable('language'):
2470 for w in [lang_combo, lang_label]: w.setEnabled(False)
2473 fee_label = QLabel(_('Transaction fee') + ':')
2474 grid.addWidget(fee_label, 2, 0)
2475 fee_e = BTCAmountEdit(self.get_decimal_point)
2476 fee_e.setAmount(self.wallet.fee)
2477 grid.addWidget(fee_e, 2, 1)
2478 msg = _('Fee per kilobyte of transaction.') + '\n' \
2479 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2480 grid.addWidget(HelpButton(msg), 2, 2)
2481 if not self.config.is_modifiable('fee_per_kb'):
2482 for w in [fee_e, fee_label]: w.setEnabled(False)
2484 units = ['BTC', 'mBTC']
2485 unit_label = QLabel(_('Base unit') + ':')
2486 grid.addWidget(unit_label, 3, 0)
2487 unit_combo = QComboBox()
2488 unit_combo.addItems(units)
2489 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2490 grid.addWidget(unit_combo, 3, 1)
2491 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2492 + '\n1BTC=1000mBTC.\n' \
2493 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2495 usechange_cb = QCheckBox(_('Use change addresses'))
2496 usechange_cb.setChecked(self.wallet.use_change)
2497 grid.addWidget(usechange_cb, 4, 0)
2498 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2499 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2501 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2502 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2503 grid.addWidget(block_ex_label, 5, 0)
2504 block_ex_combo = QComboBox()
2505 block_ex_combo.addItems(block_explorers)
2506 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2507 grid.addWidget(block_ex_combo, 5, 1)
2508 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2510 show_tx = self.config.get('show_before_broadcast', False)
2511 showtx_cb = QCheckBox(_('Show before broadcast'))
2512 showtx_cb.setChecked(show_tx)
2513 grid.addWidget(showtx_cb, 6, 0)
2514 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2516 vbox.addLayout(grid)
2518 vbox.addLayout(ok_cancel_buttons(d))
2522 if not d.exec_(): return
2524 fee = fee_e.get_amount()
2526 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2529 self.wallet.set_fee(fee)
2531 nz = unicode(nz_e.text())
2536 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2539 if self.num_zeros != nz:
2541 self.config.set_key('num_zeros', nz, True)
2542 self.update_history_tab()
2543 self.update_address_tab()
2545 usechange_result = usechange_cb.isChecked()
2546 if self.wallet.use_change != usechange_result:
2547 self.wallet.use_change = usechange_result
2548 self.wallet.storage.put('use_change', self.wallet.use_change)
2550 if showtx_cb.isChecked() != show_tx:
2551 self.config.set_key('show_before_broadcast', not show_tx)
2553 unit_result = units[unit_combo.currentIndex()]
2554 if self.base_unit() != unit_result:
2555 self.decimal_point = 8 if unit_result == 'BTC' else 5
2556 self.config.set_key('decimal_point', self.decimal_point, True)
2557 self.update_history_tab()
2558 self.update_status()
2560 need_restart = False
2562 lang_request = languages.keys()[lang_combo.currentIndex()]
2563 if lang_request != self.config.get('language'):
2564 self.config.set_key("language", lang_request, True)
2567 be_result = block_explorers[block_ex_combo.currentIndex()]
2568 self.config.set_key('block_explorer', be_result, True)
2570 run_hook('close_settings_dialog')
2573 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2576 def run_network_dialog(self):
2577 if not self.network:
2579 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2581 def closeEvent(self, event):
2583 self.config.set_key("is_maximized", self.isMaximized())
2584 if not self.isMaximized():
2586 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2587 self.save_column_widths()
2588 self.config.set_key("console-history", self.console.history[-50:], True)
2589 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2593 def plugins_dialog(self):
2594 from electrum.plugins import plugins
2597 d.setWindowTitle(_('Electrum Plugins'))
2600 vbox = QVBoxLayout(d)
2603 scroll = QScrollArea()
2604 scroll.setEnabled(True)
2605 scroll.setWidgetResizable(True)
2606 scroll.setMinimumSize(400,250)
2607 vbox.addWidget(scroll)
2611 w.setMinimumHeight(len(plugins)*35)
2613 grid = QGridLayout()
2614 grid.setColumnStretch(0,1)
2617 def do_toggle(cb, p, w):
2620 if w: w.setEnabled(r)
2622 def mk_toggle(cb, p, w):
2623 return lambda: do_toggle(cb,p,w)
2625 for i, p in enumerate(plugins):
2627 cb = QCheckBox(p.fullname())
2628 cb.setDisabled(not p.is_available())
2629 cb.setChecked(p.is_enabled())
2630 grid.addWidget(cb, i, 0)
2631 if p.requires_settings():
2632 w = p.settings_widget(self)
2633 w.setEnabled( p.is_enabled() )
2634 grid.addWidget(w, i, 1)
2637 cb.clicked.connect(mk_toggle(cb,p,w))
2638 grid.addWidget(HelpButton(p.description()), i, 2)
2640 print_msg(_("Error: cannot display plugin"), p)
2641 traceback.print_exc(file=sys.stdout)
2642 grid.setRowStretch(i+1,1)
2644 vbox.addLayout(close_button(d))
2649 def show_account_details(self, k):
2650 account = self.wallet.accounts[k]
2653 d.setWindowTitle(_('Account Details'))
2656 vbox = QVBoxLayout(d)
2657 name = self.wallet.get_account_name(k)
2658 label = QLabel('Name: ' + name)
2659 vbox.addWidget(label)
2661 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2663 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2665 vbox.addWidget(QLabel(_('Master Public Key:')))
2668 text.setReadOnly(True)
2669 text.setMaximumHeight(170)
2670 vbox.addWidget(text)
2672 mpk_text = '\n'.join( account.get_master_pubkeys() )
2673 text.setText(mpk_text)
2675 vbox.addLayout(close_button(d))