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 [2, 5, 8]
448 if self.decimal_point == 2:
450 if self.decimal_point == 5:
452 if self.decimal_point == 8:
454 raise Exception('Unknown base unit')
456 def update_status(self):
457 if self.network is None or not self.network.is_running():
459 icon = QIcon(":icons/status_disconnected.png")
461 elif self.network.is_connected():
462 if not self.wallet.up_to_date:
463 text = _("Synchronizing...")
464 icon = QIcon(":icons/status_waiting.png")
465 elif self.network.server_lag > 1:
466 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
467 icon = QIcon(":icons/status_lagging.png")
469 c, u = self.wallet.get_account_balance(self.current_account)
470 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
471 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
473 # append fiat balance and price from exchange rate plugin
475 run_hook('get_fiat_status_text', c+u, r)
480 self.tray.setToolTip(text)
481 icon = QIcon(":icons/status_connected.png")
483 text = _("Not connected")
484 icon = QIcon(":icons/status_disconnected.png")
486 self.balance_label.setText(text)
487 self.status_button.setIcon( icon )
490 def update_wallet(self):
492 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
493 self.update_history_tab()
494 self.update_receive_tab()
495 self.update_address_tab()
496 self.update_contacts_tab()
497 self.update_completions()
498 self.update_invoices_tab()
501 def create_history_tab(self):
502 self.history_list = l = MyTreeWidget(self)
504 for i,width in enumerate(self.column_widths['history']):
505 l.setColumnWidth(i, width)
506 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
507 l.itemDoubleClicked.connect(self.tx_label_clicked)
508 l.itemChanged.connect(self.tx_label_changed)
509 l.customContextMenuRequested.connect(self.create_history_menu)
513 def create_history_menu(self, position):
514 self.history_list.selectedIndexes()
515 item = self.history_list.currentItem()
516 be = self.config.get('block_explorer', 'Blockchain.info')
517 if be == 'Blockchain.info':
518 block_explorer = 'https://blockchain.info/tx/'
519 elif be == 'Blockr.io':
520 block_explorer = 'https://blockr.io/tx/info/'
521 elif be == 'Insight.is':
522 block_explorer = 'http://live.insight.is/tx/'
524 tx_hash = str(item.data(0, Qt.UserRole).toString())
525 if not tx_hash: return
527 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
528 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
529 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
530 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
531 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
534 def show_transaction(self, tx):
535 import transaction_dialog
536 d = transaction_dialog.TxDialog(tx, self)
539 def tx_label_clicked(self, item, column):
540 if column==2 and item.isSelected():
542 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
543 self.history_list.editItem( item, column )
544 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
547 def tx_label_changed(self, item, column):
551 tx_hash = str(item.data(0, Qt.UserRole).toString())
552 tx = self.wallet.transactions.get(tx_hash)
553 text = unicode( item.text(2) )
554 self.wallet.set_label(tx_hash, text)
556 item.setForeground(2, QBrush(QColor('black')))
558 text = self.wallet.get_default_label(tx_hash)
559 item.setText(2, text)
560 item.setForeground(2, QBrush(QColor('gray')))
564 def edit_label(self, is_recv):
565 l = self.address_list if is_recv else self.contacts_list
566 item = l.currentItem()
567 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568 l.editItem( item, 1 )
569 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
573 def address_label_clicked(self, item, column, l, column_addr, column_label):
574 if column == column_label and item.isSelected():
575 is_editable = item.data(0, 32).toBool()
578 addr = unicode( item.text(column_addr) )
579 label = unicode( item.text(column_label) )
580 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
581 l.editItem( item, column )
582 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
585 def address_label_changed(self, item, column, l, column_addr, column_label):
586 if column == column_label:
587 addr = unicode( item.text(column_addr) )
588 text = unicode( item.text(column_label) )
589 is_editable = item.data(0, 32).toBool()
593 changed = self.wallet.set_label(addr, text)
595 self.update_history_tab()
596 self.update_completions()
598 self.current_item_changed(item)
600 run_hook('item_changed', item, column)
603 def current_item_changed(self, a):
604 run_hook('current_item_changed', a)
608 def update_history_tab(self):
610 self.history_list.clear()
611 for item in self.wallet.get_tx_history(self.current_account):
612 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
613 time_str = _("unknown")
616 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
618 time_str = _("error")
621 time_str = 'unverified'
622 icon = QIcon(":icons/unconfirmed.png")
625 icon = QIcon(":icons/unconfirmed.png")
627 icon = QIcon(":icons/clock%d.png"%conf)
629 icon = QIcon(":icons/confirmed.png")
631 if value is not None:
632 v_str = self.format_amount(value, True, whitespaces=True)
636 balance_str = self.format_amount(balance, whitespaces=True)
639 label, is_default_label = self.wallet.get_label(tx_hash)
641 label = _('Pruned transaction outputs')
642 is_default_label = False
644 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
645 item.setFont(2, QFont(MONOSPACE_FONT))
646 item.setFont(3, QFont(MONOSPACE_FONT))
647 item.setFont(4, QFont(MONOSPACE_FONT))
649 item.setForeground(3, QBrush(QColor("#BC1E1E")))
651 item.setData(0, Qt.UserRole, tx_hash)
652 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
654 item.setForeground(2, QBrush(QColor('grey')))
656 item.setIcon(0, icon)
657 self.history_list.insertTopLevelItem(0,item)
660 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
661 run_hook('history_tab_update')
664 def create_receive_tab(self):
666 grid = QGridLayout(w)
667 grid.setColumnMinimumWidth(3, 300)
668 grid.setColumnStretch(5, 1)
670 self.receive_address_e = QLineEdit()
671 self.receive_address_e.setReadOnly(True)
672 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
673 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
674 self.receive_address_e.textChanged.connect(self.update_receive_qr)
676 self.receive_message_e = QLineEdit()
677 grid.addWidget(QLabel(_('Message')), 1, 0)
678 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
679 self.receive_message_e.textChanged.connect(self.update_receive_qr)
681 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
682 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
683 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
684 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
686 self.save_request_button = QPushButton(_('Save'))
687 self.save_request_button.clicked.connect(self.save_payment_request)
688 grid.addWidget(self.save_request_button, 3, 1)
689 clear_button = QPushButton(_('New'))
690 clear_button.clicked.connect(self.new_receive_address)
691 grid.addWidget(clear_button, 3, 2)
692 grid.setRowStretch(4, 1)
694 self.receive_qr = QRCodeWidget(fixedSize=200)
695 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
697 grid.setRowStretch(5, 1)
699 self.receive_requests_label = QLabel(_('Saved Requests'))
700 self.receive_list = MyTreeWidget(self)
701 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
702 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
703 self.receive_list.itemClicked.connect(self.receive_item_changed)
704 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
705 self.receive_list.setColumnWidth(0, 340)
706 h = self.receive_list.header()
707 h.setStretchLastSection(False)
708 h.setResizeMode(1, QHeaderView.Stretch)
710 grid.addWidget(self.receive_requests_label, 6, 0)
711 grid.addWidget(self.receive_list, 7, 0, 1, 6)
714 def receive_item_changed(self, item):
717 addr = str(item.text(0))
718 amount, message = self.receive_requests[addr]
719 self.receive_address_e.setText(addr)
720 self.receive_message_e.setText(message)
721 self.receive_amount_e.setAmount(amount)
724 def receive_list_delete(self, item):
725 addr = str(item.text(0))
726 self.receive_requests.pop(addr)
727 self.update_receive_tab()
728 self.clear_receive_tab()
730 def receive_list_menu(self, position):
731 item = self.receive_list.itemAt(position)
733 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
734 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
736 def save_payment_request(self):
737 addr = str(self.receive_address_e.text())
738 amount = self.receive_amount_e.get_amount()
739 message = str(self.receive_message_e.text())
740 if not message and not amount:
741 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
743 self.receive_requests = self.wallet.storage.get('receive_requests',{})
744 self.receive_requests[addr] = (amount, message)
745 self.wallet.storage.put('receive_requests', self.receive_requests)
746 self.update_receive_tab()
748 def new_receive_address(self):
749 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
751 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
754 if isinstance(self.wallet, Imported_Wallet):
755 self.show_message(_('No more addresses in your wallet.'))
757 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?")):
759 addr = self.wallet.create_new_address(self.current_account, False)
760 self.receive_address_e.setText(addr)
761 self.receive_message_e.setText('')
762 self.receive_amount_e.setAmount(None)
764 def clear_receive_tab(self):
765 self.receive_requests = self.wallet.storage.get('receive_requests',{})
766 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
768 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
772 self.receive_address_e.setText(addr)
773 self.receive_message_e.setText('')
774 self.receive_amount_e.setAmount(None)
776 def receive_at(self, addr):
777 if not bitcoin.is_address(addr):
779 self.tabs.setCurrentIndex(2)
780 self.receive_address_e.setText(addr)
782 def update_receive_tab(self):
783 self.receive_requests = self.wallet.storage.get('receive_requests',{})
784 b = len(self.receive_requests) > 0
785 self.receive_list.setVisible(b)
786 self.receive_requests_label.setVisible(b)
788 self.receive_list.clear()
789 for address, v in self.receive_requests.items():
791 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
792 item.setFont(0, QFont(MONOSPACE_FONT))
793 self.receive_list.addTopLevelItem(item)
796 def update_receive_qr(self):
797 import urlparse, urllib
798 addr = str(self.receive_address_e.text())
799 amount = self.receive_amount_e.get_amount()
800 message = unicode(self.receive_message_e.text()).encode('utf8')
801 self.save_request_button.setEnabled((amount is not None) or (message != ""))
805 query.append('amount=%s'%format_satoshis(amount))
807 query.append('message=%s'%urllib.quote(message))
808 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
809 url = urlparse.urlunparse(p)
812 self.receive_qr.setData(url)
813 run_hook('update_receive_qr', addr, amount, message, url)
816 def create_send_tab(self):
819 self.send_grid = grid = QGridLayout(w)
821 grid.setColumnMinimumWidth(3,300)
822 grid.setColumnStretch(5,1)
823 grid.setRowStretch(8, 1)
825 from paytoedit import PayToEdit
826 self.amount_e = BTCAmountEdit(self.get_decimal_point)
827 self.payto_e = PayToEdit(self)
828 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)'))
829 grid.addWidget(QLabel(_('Pay to')), 1, 0)
830 grid.addWidget(self.payto_e, 1, 1, 1, 3)
831 grid.addWidget(self.payto_help, 1, 4)
833 completer = QCompleter()
834 completer.setCaseSensitivity(False)
835 self.payto_e.setCompleter(completer)
836 completer.setModel(self.completions)
838 self.message_e = MyLineEdit()
839 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.'))
840 grid.addWidget(QLabel(_('Description')), 2, 0)
841 grid.addWidget(self.message_e, 2, 1, 1, 3)
842 grid.addWidget(self.message_help, 2, 4)
844 self.from_label = QLabel(_('From'))
845 grid.addWidget(self.from_label, 3, 0)
846 self.from_list = MyTreeWidget(self)
847 self.from_list.setColumnCount(2)
848 self.from_list.setColumnWidth(0, 350)
849 self.from_list.setColumnWidth(1, 50)
850 self.from_list.setHeaderHidden(True)
851 self.from_list.setMaximumHeight(80)
852 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
853 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
854 grid.addWidget(self.from_list, 3, 1, 1, 3)
855 self.set_pay_from([])
857 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
858 + _('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.') \
859 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
860 grid.addWidget(QLabel(_('Amount')), 4, 0)
861 grid.addWidget(self.amount_e, 4, 1, 1, 2)
862 grid.addWidget(self.amount_help, 4, 3)
864 self.fee_e = BTCAmountEdit(self.get_decimal_point)
865 grid.addWidget(QLabel(_('Fee')), 5, 0)
866 grid.addWidget(self.fee_e, 5, 1, 1, 2)
867 grid.addWidget(HelpButton(
868 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
869 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
870 + _('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)
872 self.send_button = EnterButton(_("Send"), self.do_send)
873 grid.addWidget(self.send_button, 6, 1)
875 b = EnterButton(_("Clear"), self.do_clear)
876 grid.addWidget(b, 6, 2)
878 self.payto_sig = QLabel('')
879 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
881 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
882 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
885 def entry_changed( is_fee ):
887 if self.amount_e.is_shortcut:
888 self.amount_e.is_shortcut = False
889 sendable = self.get_sendable_balance()
890 # there is only one output because we are completely spending inputs
891 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
892 fee = self.wallet.estimated_fee(inputs, 1)
894 self.amount_e.setAmount(amount)
895 self.amount_e.textEdited.emit("")
896 self.fee_e.setAmount(fee)
899 amount = self.amount_e.get_amount()
900 fee = self.fee_e.get_amount()
901 outputs = self.payto_e.get_outputs()
907 self.fee_e.setAmount(None)
908 not_enough_funds = False
910 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, len(outputs), coins = self.get_coins())
911 not_enough_funds = len(inputs) == 0
913 self.fee_e.setAmount(fee)
915 if not not_enough_funds:
917 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
921 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
922 text = _( "Not enough funds" )
923 c, u = self.wallet.get_frozen_balance()
924 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
926 self.statusBar().showMessage(text)
927 self.amount_e.setPalette(palette)
928 self.fee_e.setPalette(palette)
930 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
931 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
933 run_hook('create_send_tab', grid)
936 def from_list_delete(self, item):
937 i = self.from_list.indexOfTopLevelItem(item)
939 self.redraw_from_list()
941 def from_list_menu(self, position):
942 item = self.from_list.itemAt(position)
944 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
945 menu.exec_(self.from_list.viewport().mapToGlobal(position))
947 def set_pay_from(self, domain = None):
948 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
949 self.redraw_from_list()
951 def redraw_from_list(self):
952 self.from_list.clear()
953 self.from_label.setHidden(len(self.pay_from) == 0)
954 self.from_list.setHidden(len(self.pay_from) == 0)
957 h = x.get('prevout_hash')
958 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
960 for item in self.pay_from:
961 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
963 def update_completions(self):
965 for addr,label in self.wallet.labels.items():
966 if addr in self.wallet.addressbook:
967 l.append( label + ' <' + addr + '>')
969 run_hook('update_completions', l)
970 self.completions.setStringList(l)
974 return lambda s, *args: s.do_protect(func, args)
977 def read_send_tab(self):
979 if self.payment_request and self.payment_request.has_expired():
980 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
983 label = unicode( self.message_e.text() )
985 if self.payment_request:
986 outputs = self.payment_request.get_outputs()
988 outputs = self.payto_e.get_outputs()
991 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
994 for addr, x in outputs:
996 QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK'))
998 if addr.startswith('OP_RETURN:'):
1000 if not bitcoin.is_address(addr):
1001 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
1004 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
1007 amount = sum(map(lambda x:x[1], outputs))
1009 fee = self.fee_e.get_amount()
1011 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
1014 confirm_amount = self.config.get('confirm_amount', 100000000)
1015 if amount >= confirm_amount:
1016 o = '\n'.join(map(lambda x:x[0], outputs))
1017 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
1020 confirm_fee = self.config.get('confirm_fee', 100000)
1021 if fee >= confirm_fee:
1022 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()}):
1025 coins = self.get_coins()
1026 return outputs, fee, label, coins
1030 r = self.read_send_tab()
1033 outputs, fee, label, coins = r
1034 self.send_tx(outputs, fee, label, coins)
1038 def send_tx(self, outputs, fee, label, coins, password):
1039 self.send_button.setDisabled(True)
1041 # first, create an unsigned tx
1043 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1045 except Exception as e:
1046 traceback.print_exc(file=sys.stdout)
1047 self.show_message(str(e))
1048 self.send_button.setDisabled(False)
1051 # call hook to see if plugin needs gui interaction
1052 run_hook('send_tx', tx)
1056 if self.wallet.is_watching_only():
1060 self.wallet.add_keypairs(tx, keypairs, password)
1061 self.wallet.sign_transaction(tx, keypairs, password)
1062 except Exception as e:
1068 self.show_message(tx.error)
1069 self.send_button.setDisabled(False)
1071 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1072 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1073 self.send_button.setDisabled(False)
1076 self.wallet.set_label(tx.hash(), label)
1078 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1079 self.show_transaction(tx)
1081 self.send_button.setDisabled(False)
1084 self.broadcast_transaction(tx)
1086 # keep a reference to WaitingDialog or the gui might crash
1087 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1088 self.waiting_dialog.start()
1092 def broadcast_transaction(self, tx):
1094 def broadcast_thread():
1095 pr = self.payment_request
1097 return self.wallet.sendtx(tx)
1099 if pr.has_expired():
1100 self.payment_request = None
1101 return False, _("Payment request has expired")
1103 status, msg = self.wallet.sendtx(tx)
1107 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1108 self.wallet.storage.put('invoices', self.invoices)
1109 self.update_invoices_tab()
1110 self.payment_request = None
1111 refund_address = self.wallet.addresses()[0]
1112 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1118 def broadcast_done(status, msg):
1120 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1123 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1124 self.send_button.setDisabled(False)
1126 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1127 self.waiting_dialog.start()
1131 def prepare_for_payment_request(self):
1132 self.tabs.setCurrentIndex(1)
1133 self.payto_e.is_pr = True
1134 for e in [self.payto_e, self.amount_e, self.message_e]:
1136 for h in [self.payto_help, self.amount_help, self.message_help]:
1138 self.payto_e.setText(_("please wait..."))
1141 def payment_request_ok(self):
1142 pr = self.payment_request
1144 if pr_id not in self.invoices:
1145 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1146 self.wallet.storage.put('invoices', self.invoices)
1147 self.update_invoices_tab()
1149 print_error('invoice already in list')
1151 status = self.invoices[pr_id][4]
1152 if status == PR_PAID:
1154 self.show_message("invoice already paid")
1155 self.payment_request = None
1158 self.payto_help.show()
1159 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1161 if not pr.has_expired():
1162 self.payto_e.setGreen()
1164 self.payto_e.setExpired()
1166 self.payto_e.setText(pr.domain)
1167 self.amount_e.setText(self.format_amount(pr.get_amount()))
1168 self.message_e.setText(pr.get_memo())
1170 def payment_request_error(self):
1172 self.show_message(self.payment_request.error)
1173 self.payment_request = None
1175 def pay_from_URI(self,URI):
1178 address, amount, label, message, request_url = util.parse_URI(URI)
1180 address, amount, label, message, request_url = util.parse_URI(URI)
1181 except Exception as e:
1182 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1185 self.tabs.setCurrentIndex(1)
1189 if self.wallet.labels.get(address) != label:
1190 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1191 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1192 self.wallet.addressbook.append(address)
1193 self.wallet.set_label(address, label)
1195 label = self.wallet.labels.get(address)
1197 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1199 self.message_e.setText(message)
1201 self.amount_e.setAmount(amount)
1204 from electrum import paymentrequest
1205 def payment_request():
1206 self.payment_request = paymentrequest.PaymentRequest(self.config)
1207 self.payment_request.read(request_url)
1208 if self.payment_request.verify():
1209 self.emit(SIGNAL('payment_request_ok'))
1211 self.emit(SIGNAL('payment_request_error'))
1213 self.pr_thread = threading.Thread(target=payment_request).start()
1214 self.prepare_for_payment_request()
1219 self.payto_e.is_pr = False
1220 self.payto_sig.setVisible(False)
1221 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1225 for h in [self.payto_help, self.amount_help, self.message_help]:
1228 self.payto_help.set_alt(None)
1229 self.set_pay_from([])
1230 self.update_status()
1234 def set_addrs_frozen(self,addrs,freeze):
1236 if not addr: continue
1237 if addr in self.wallet.frozen_addresses and not freeze:
1238 self.wallet.unfreeze(addr)
1239 elif addr not in self.wallet.frozen_addresses and freeze:
1240 self.wallet.freeze(addr)
1241 self.update_address_tab()
1245 def create_list_tab(self, headers):
1246 "generic tab creation method"
1247 l = MyTreeWidget(self)
1248 l.setColumnCount( len(headers) )
1249 l.setHeaderLabels( headers )
1252 vbox = QVBoxLayout()
1259 vbox.addWidget(buttons)
1264 def create_addresses_tab(self):
1265 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1266 for i,width in enumerate(self.column_widths['receive']):
1267 l.setColumnWidth(i, width)
1268 l.setContextMenuPolicy(Qt.CustomContextMenu)
1269 l.customContextMenuRequested.connect(self.create_receive_menu)
1270 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1271 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1272 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1273 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1274 self.address_list = l
1280 def save_column_widths(self):
1281 self.column_widths["receive"] = []
1282 for i in range(self.address_list.columnCount() -1):
1283 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1285 self.column_widths["history"] = []
1286 for i in range(self.history_list.columnCount() - 1):
1287 self.column_widths["history"].append(self.history_list.columnWidth(i))
1289 self.column_widths["contacts"] = []
1290 for i in range(self.contacts_list.columnCount() - 1):
1291 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1293 self.config.set_key("column_widths_2", self.column_widths, True)
1296 def create_contacts_tab(self):
1297 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1298 l.setContextMenuPolicy(Qt.CustomContextMenu)
1299 l.customContextMenuRequested.connect(self.create_contact_menu)
1300 for i,width in enumerate(self.column_widths['contacts']):
1301 l.setColumnWidth(i, width)
1302 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1303 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1304 self.contacts_list = l
1308 def create_invoices_tab(self):
1309 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1310 l.setColumnWidth(0, 150)
1312 h.setStretchLastSection(False)
1313 h.setResizeMode(1, QHeaderView.Stretch)
1314 l.setContextMenuPolicy(Qt.CustomContextMenu)
1315 l.customContextMenuRequested.connect(self.create_invoice_menu)
1316 self.invoices_list = l
1319 def update_invoices_tab(self):
1320 invoices = self.wallet.storage.get('invoices', {})
1321 l = self.invoices_list
1323 for key, value in invoices.items():
1325 domain, memo, amount, expiration_date, status, tx_hash = value
1329 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1331 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1332 l.addTopLevelItem(item)
1334 l.setCurrentItem(l.topLevelItem(0))
1338 def delete_imported_key(self, addr):
1339 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1340 self.wallet.delete_imported_key(addr)
1341 self.update_address_tab()
1342 self.update_history_tab()
1344 def edit_account_label(self, k):
1345 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1347 label = unicode(text)
1348 self.wallet.set_label(k,label)
1349 self.update_address_tab()
1351 def account_set_expanded(self, item, k, b):
1353 self.accounts_expanded[k] = b
1355 def create_account_menu(self, position, k, item):
1357 if item.isExpanded():
1358 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1360 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1361 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1362 if self.wallet.seed_version > 4:
1363 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1364 if self.wallet.account_is_pending(k):
1365 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1366 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1368 def delete_pending_account(self, k):
1369 self.wallet.delete_pending_account(k)
1370 self.update_address_tab()
1372 def create_receive_menu(self, position):
1373 # fixme: this function apparently has a side effect.
1374 # if it is not called the menu pops up several times
1375 #self.address_list.selectedIndexes()
1377 selected = self.address_list.selectedItems()
1378 multi_select = len(selected) > 1
1379 addrs = [unicode(item.text(0)) for item in selected]
1380 if not multi_select:
1381 item = self.address_list.itemAt(position)
1385 if not is_valid(addr):
1386 k = str(item.data(0,32).toString())
1388 self.create_account_menu(position, k, item)
1390 item.setExpanded(not item.isExpanded())
1394 if not multi_select:
1395 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1396 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1397 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1398 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1399 if not self.wallet.is_watching_only():
1400 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1401 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1402 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1403 if self.wallet.is_imported(addr):
1404 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1406 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1407 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1408 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1409 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1412 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1413 if any(can_send(addr) for addr in addrs):
1414 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1416 run_hook('receive_menu', menu, addrs)
1417 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1420 def get_sendable_balance(self):
1421 return sum(map(lambda x:x['value'], self.get_coins()))
1424 def get_coins(self):
1426 return self.pay_from
1428 domain = self.wallet.get_account_addresses(self.current_account)
1429 for i in self.wallet.frozen_addresses:
1430 if i in domain: domain.remove(i)
1431 return self.wallet.get_unspent_coins(domain)
1434 def send_from_addresses(self, addrs):
1435 self.set_pay_from( addrs )
1436 self.tabs.setCurrentIndex(1)
1439 def payto(self, addr):
1441 label = self.wallet.labels.get(addr)
1442 m_addr = label + ' <' + addr + '>' if label else addr
1443 self.tabs.setCurrentIndex(1)
1444 self.payto_e.setText(m_addr)
1445 self.amount_e.setFocus()
1448 def delete_contact(self, x):
1449 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1450 self.wallet.delete_contact(x)
1451 self.wallet.set_label(x, None)
1452 self.update_history_tab()
1453 self.update_contacts_tab()
1454 self.update_completions()
1457 def create_contact_menu(self, position):
1458 item = self.contacts_list.itemAt(position)
1461 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1463 addr = unicode(item.text(0))
1464 label = unicode(item.text(1))
1465 is_editable = item.data(0,32).toBool()
1466 payto_addr = item.data(0,33).toString()
1467 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1468 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1469 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1471 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1472 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1474 run_hook('create_contact_menu', menu, item)
1475 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1477 def delete_invoice(self, key):
1478 self.invoices.pop(key)
1479 self.wallet.storage.put('invoices', self.invoices)
1480 self.update_invoices_tab()
1482 def show_invoice(self, key):
1483 from electrum.paymentrequest import PaymentRequest
1484 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1485 pr = PaymentRequest(self.config)
1489 self.show_pr_details(pr)
1491 def show_pr_details(self, pr):
1492 msg = 'Domain: ' + pr.domain
1493 msg += '\nStatus: ' + pr.get_status()
1494 msg += '\nMemo: ' + pr.get_memo()
1495 msg += '\nPayment URL: ' + pr.payment_url
1496 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1497 QMessageBox.information(self, 'Invoice', msg , 'OK')
1499 def do_pay_invoice(self, key):
1500 from electrum.paymentrequest import PaymentRequest
1501 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1502 pr = PaymentRequest(self.config)
1505 self.payment_request = pr
1506 self.prepare_for_payment_request()
1508 self.payment_request_ok()
1510 self.payment_request_error()
1513 def create_invoice_menu(self, position):
1514 item = self.invoices_list.itemAt(position)
1517 k = self.invoices_list.indexOfTopLevelItem(item)
1518 key = self.invoices.keys()[k]
1519 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1521 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1522 if status == PR_UNPAID:
1523 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1524 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1525 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1529 def update_address_tab(self):
1530 l = self.address_list
1531 # extend the syntax for consistency
1532 l.addChild = l.addTopLevelItem
1533 l.insertChild = l.insertTopLevelItem
1537 accounts = self.wallet.get_accounts()
1538 if self.current_account is None:
1539 account_items = sorted(accounts.items())
1541 account_items = [(self.current_account, accounts.get(self.current_account))]
1544 for k, account in account_items:
1546 if len(accounts) > 1:
1547 name = self.wallet.get_account_name(k)
1548 c,u = self.wallet.get_account_balance(k)
1549 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1550 l.addTopLevelItem(account_item)
1551 account_item.setExpanded(self.accounts_expanded.get(k, True))
1552 account_item.setData(0, 32, k)
1556 sequences = [0,1] if account.has_change() else [0]
1557 for is_change in sequences:
1558 if len(sequences) > 1:
1559 name = _("Receiving") if not is_change else _("Change")
1560 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1561 account_item.addChild(seq_item)
1563 seq_item.setExpanded(True)
1565 seq_item = account_item
1567 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1570 addr_list = account.get_addresses(is_change)
1571 for address in addr_list:
1572 num, is_used = self.wallet.is_used(address)
1573 label = self.wallet.labels.get(address,'')
1574 c, u = self.wallet.get_addr_balance(address)
1575 balance = self.format_amount(c + u)
1576 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1577 item.setFont(0, QFont(MONOSPACE_FONT))
1578 item.setData(0, 32, True) # label can be edited
1579 if address in self.wallet.frozen_addresses:
1580 item.setBackgroundColor(0, QColor('lightblue'))
1581 if self.wallet.is_beyond_limit(address, account, is_change):
1582 item.setBackgroundColor(0, QColor('red'))
1585 seq_item.insertChild(0, used_item)
1587 used_item.addChild(item)
1589 seq_item.addChild(item)
1591 # we use column 1 because column 0 may be hidden
1592 l.setCurrentItem(l.topLevelItem(0),1)
1595 def update_contacts_tab(self):
1596 l = self.contacts_list
1599 for address in self.wallet.addressbook:
1600 label = self.wallet.labels.get(address,'')
1601 n = self.wallet.get_num_tx(address)
1602 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1603 item.setFont(0, QFont(MONOSPACE_FONT))
1604 # 32 = label can be edited (bool)
1605 item.setData(0,32, True)
1607 item.setData(0,33, address)
1608 l.addTopLevelItem(item)
1610 run_hook('update_contacts_tab', l)
1611 l.setCurrentItem(l.topLevelItem(0))
1614 def create_console_tab(self):
1615 from console import Console
1616 self.console = console = Console()
1620 def update_console(self):
1621 console = self.console
1622 console.history = self.config.get("console-history",[])
1623 console.history_index = len(console.history)
1625 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1626 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1628 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1630 def mkfunc(f, method):
1631 return lambda *args: apply( f, (method, args, self.password_dialog ))
1633 if m[0]=='_' or m in ['network','wallet']: continue
1634 methods[m] = mkfunc(c._run, m)
1636 console.updateNamespace(methods)
1639 def change_account(self,s):
1640 if s == _("All accounts"):
1641 self.current_account = None
1643 accounts = self.wallet.get_account_names()
1644 for k, v in accounts.items():
1646 self.current_account = k
1647 self.update_history_tab()
1648 self.update_status()
1649 self.update_address_tab()
1650 self.update_receive_tab()
1652 def create_status_bar(self):
1655 sb.setFixedHeight(35)
1656 qtVersion = qVersion()
1658 self.balance_label = QLabel("")
1659 sb.addWidget(self.balance_label)
1661 from version_getter import UpdateLabel
1662 self.updatelabel = UpdateLabel(self.config, sb)
1664 self.account_selector = QComboBox()
1665 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1666 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1667 sb.addPermanentWidget(self.account_selector)
1669 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1670 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1672 self.lock_icon = QIcon()
1673 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1674 sb.addPermanentWidget( self.password_button )
1676 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1677 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1678 sb.addPermanentWidget( self.seed_button )
1679 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1680 sb.addPermanentWidget( self.status_button )
1682 run_hook('create_status_bar', (sb,))
1684 self.setStatusBar(sb)
1687 def update_lock_icon(self):
1688 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1689 self.password_button.setIcon( icon )
1692 def update_buttons_on_seed(self):
1693 if self.wallet.has_seed():
1694 self.seed_button.show()
1696 self.seed_button.hide()
1698 if not self.wallet.is_watching_only():
1699 self.password_button.show()
1700 self.send_button.setText(_("Send"))
1702 self.password_button.hide()
1703 self.send_button.setText(_("Create unsigned transaction"))
1706 def change_password_dialog(self):
1707 from password_dialog import PasswordDialog
1708 d = PasswordDialog(self.wallet, self)
1710 self.update_lock_icon()
1713 def new_contact_dialog(self):
1716 d.setWindowTitle(_("New Contact"))
1717 vbox = QVBoxLayout(d)
1718 vbox.addWidget(QLabel(_('New Contact')+':'))
1720 grid = QGridLayout()
1723 grid.addWidget(QLabel(_("Address")), 1, 0)
1724 grid.addWidget(line1, 1, 1)
1725 grid.addWidget(QLabel(_("Name")), 2, 0)
1726 grid.addWidget(line2, 2, 1)
1728 vbox.addLayout(grid)
1729 vbox.addLayout(ok_cancel_buttons(d))
1734 address = str(line1.text())
1735 label = unicode(line2.text())
1737 if not is_valid(address):
1738 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1741 self.wallet.add_contact(address)
1743 self.wallet.set_label(address, label)
1745 self.update_contacts_tab()
1746 self.update_history_tab()
1747 self.update_completions()
1748 self.tabs.setCurrentIndex(3)
1752 def new_account_dialog(self, password):
1754 dialog = QDialog(self)
1756 dialog.setWindowTitle(_("New Account"))
1758 vbox = QVBoxLayout()
1759 vbox.addWidget(QLabel(_('Account name')+':'))
1762 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1763 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1768 vbox.addLayout(ok_cancel_buttons(dialog))
1769 dialog.setLayout(vbox)
1773 name = str(e.text())
1776 self.wallet.create_pending_account(name, password)
1777 self.update_address_tab()
1778 self.tabs.setCurrentIndex(2)
1783 def show_master_public_keys(self):
1785 dialog = QDialog(self)
1787 dialog.setWindowTitle(_("Master Public Keys"))
1789 main_layout = QGridLayout()
1790 mpk_dict = self.wallet.get_master_public_keys()
1792 for key, value in mpk_dict.items():
1793 main_layout.addWidget(QLabel(key), i, 0)
1794 mpk_text = QTextEdit()
1795 mpk_text.setReadOnly(True)
1796 mpk_text.setMaximumHeight(170)
1797 mpk_text.setText(value)
1798 main_layout.addWidget(mpk_text, i + 1, 0)
1801 vbox = QVBoxLayout()
1802 vbox.addLayout(main_layout)
1803 vbox.addLayout(close_button(dialog))
1805 dialog.setLayout(vbox)
1810 def show_seed_dialog(self, password):
1811 if not self.wallet.has_seed():
1812 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1816 mnemonic = self.wallet.get_mnemonic(password)
1818 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1820 from seed_dialog import SeedDialog
1821 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1826 def show_qrcode(self, data, title = _("QR code")):
1829 d = QRDialog(data, self, title)
1833 def do_protect(self, func, args):
1834 if self.wallet.use_encryption:
1835 password = self.password_dialog()
1841 if args != (False,):
1842 args = (self,) + args + (password,)
1844 args = (self,password)
1848 def show_public_keys(self, address):
1849 if not address: return
1851 pubkey_list = self.wallet.get_public_keys(address)
1852 except Exception as e:
1853 traceback.print_exc(file=sys.stdout)
1854 self.show_message(str(e))
1858 d.setMinimumSize(600, 200)
1860 vbox = QVBoxLayout()
1861 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1862 vbox.addWidget( QLabel(_("Public key") + ':'))
1864 keys.setReadOnly(True)
1865 keys.setText('\n'.join(pubkey_list))
1866 vbox.addWidget(keys)
1867 vbox.addLayout(close_button(d))
1872 def show_private_key(self, address, password):
1873 if not address: return
1875 pk_list = self.wallet.get_private_key(address, password)
1876 except Exception as e:
1877 traceback.print_exc(file=sys.stdout)
1878 self.show_message(str(e))
1882 d.setMinimumSize(600, 200)
1884 vbox = QVBoxLayout()
1885 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1886 vbox.addWidget( QLabel(_("Private key") + ':'))
1888 keys.setReadOnly(True)
1889 keys.setText('\n'.join(pk_list))
1890 vbox.addWidget(keys)
1891 vbox.addLayout(close_button(d))
1897 def do_sign(self, address, message, signature, password):
1898 message = unicode(message.toPlainText())
1899 message = message.encode('utf-8')
1901 sig = self.wallet.sign_message(str(address.text()), message, password)
1902 signature.setText(sig)
1903 except Exception as e:
1904 self.show_message(str(e))
1906 def do_verify(self, address, message, signature):
1907 message = unicode(message.toPlainText())
1908 message = message.encode('utf-8')
1909 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1910 self.show_message(_("Signature verified"))
1912 self.show_message(_("Error: wrong signature"))
1915 def sign_verify_message(self, address=''):
1918 d.setWindowTitle(_('Sign/verify Message'))
1919 d.setMinimumSize(410, 290)
1921 layout = QGridLayout(d)
1923 message_e = QTextEdit()
1924 layout.addWidget(QLabel(_('Message')), 1, 0)
1925 layout.addWidget(message_e, 1, 1)
1926 layout.setRowStretch(2,3)
1928 address_e = QLineEdit()
1929 address_e.setText(address)
1930 layout.addWidget(QLabel(_('Address')), 2, 0)
1931 layout.addWidget(address_e, 2, 1)
1933 signature_e = QTextEdit()
1934 layout.addWidget(QLabel(_('Signature')), 3, 0)
1935 layout.addWidget(signature_e, 3, 1)
1936 layout.setRowStretch(3,1)
1938 hbox = QHBoxLayout()
1940 b = QPushButton(_("Sign"))
1941 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1944 b = QPushButton(_("Verify"))
1945 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1948 b = QPushButton(_("Close"))
1949 b.clicked.connect(d.accept)
1951 layout.addLayout(hbox, 4, 1)
1956 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1958 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1959 message_e.setText(decrypted)
1960 except Exception as e:
1961 self.show_message(str(e))
1964 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1965 message = unicode(message_e.toPlainText())
1966 message = message.encode('utf-8')
1968 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1969 encrypted_e.setText(encrypted)
1970 except Exception as e:
1971 self.show_message(str(e))
1975 def encrypt_message(self, address = ''):
1978 d.setWindowTitle(_('Encrypt/decrypt Message'))
1979 d.setMinimumSize(610, 490)
1981 layout = QGridLayout(d)
1983 message_e = QTextEdit()
1984 layout.addWidget(QLabel(_('Message')), 1, 0)
1985 layout.addWidget(message_e, 1, 1)
1986 layout.setRowStretch(2,3)
1988 pubkey_e = QLineEdit()
1990 pubkey = self.wallet.getpubkeys(address)[0]
1991 pubkey_e.setText(pubkey)
1992 layout.addWidget(QLabel(_('Public key')), 2, 0)
1993 layout.addWidget(pubkey_e, 2, 1)
1995 encrypted_e = QTextEdit()
1996 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1997 layout.addWidget(encrypted_e, 3, 1)
1998 layout.setRowStretch(3,1)
2000 hbox = QHBoxLayout()
2001 b = QPushButton(_("Encrypt"))
2002 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2005 b = QPushButton(_("Decrypt"))
2006 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2009 b = QPushButton(_("Close"))
2010 b.clicked.connect(d.accept)
2013 layout.addLayout(hbox, 4, 1)
2017 def question(self, msg):
2018 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2020 def show_message(self, msg):
2021 QMessageBox.information(self, _('Message'), msg, _('OK'))
2023 def password_dialog(self, msg=None):
2026 d.setWindowTitle(_("Enter Password"))
2031 vbox = QVBoxLayout()
2033 msg = _('Please enter your password')
2034 vbox.addWidget(QLabel(msg))
2036 grid = QGridLayout()
2038 grid.addWidget(QLabel(_('Password')), 1, 0)
2039 grid.addWidget(pw, 1, 1)
2040 vbox.addLayout(grid)
2042 vbox.addLayout(ok_cancel_buttons(d))
2045 run_hook('password_dialog', pw, grid, 1)
2046 if not d.exec_(): return
2047 return unicode(pw.text())
2056 def tx_from_text(self, txt):
2057 "json or raw hexadecimal"
2066 return Transaction(txt)
2068 traceback.print_exc(file=sys.stdout)
2069 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2073 tx_dict = json.loads(str(txt))
2074 assert "hex" in tx_dict.keys()
2075 tx = Transaction(tx_dict["hex"])
2076 #if tx_dict.has_key("input_info"):
2077 # input_info = json.loads(tx_dict['input_info'])
2078 # tx.add_input_info(input_info)
2081 traceback.print_exc(file=sys.stdout)
2082 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2086 def read_tx_from_file(self):
2087 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2091 with open(fileName, "r") as f:
2092 file_content = f.read()
2093 except (ValueError, IOError, os.error), reason:
2094 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2096 return self.tx_from_text(file_content)
2100 def sign_raw_transaction(self, tx, password):
2102 self.wallet.signrawtransaction(tx, [], password)
2103 except Exception as e:
2104 traceback.print_exc(file=sys.stdout)
2105 QMessageBox.warning(self, _("Error"), str(e))
2107 def do_process_from_text(self):
2108 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2111 tx = self.tx_from_text(text)
2113 self.show_transaction(tx)
2115 def do_process_from_file(self):
2116 tx = self.read_tx_from_file()
2118 self.show_transaction(tx)
2120 def do_process_from_txid(self):
2121 from electrum import transaction
2122 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2124 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2126 tx = transaction.Transaction(r)
2128 self.show_transaction(tx)
2130 self.show_message("unknown transaction")
2132 def do_process_from_csvReader(self, csvReader):
2137 for position, row in enumerate(csvReader):
2139 if not is_valid(address):
2140 errors.append((position, address))
2142 amount = Decimal(row[1])
2143 amount = int(100000000*amount)
2144 outputs.append((address, amount))
2145 except (ValueError, IOError, os.error), reason:
2146 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2150 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2151 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2155 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2156 except Exception as e:
2157 self.show_message(str(e))
2160 self.show_transaction(tx)
2162 def do_process_from_csv_file(self):
2163 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2167 with open(fileName, "r") as f:
2168 csvReader = csv.reader(f)
2169 self.do_process_from_csvReader(csvReader)
2170 except (ValueError, IOError, os.error), reason:
2171 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2174 def do_process_from_csv_text(self):
2175 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2176 + _("Format: address, amount. One output per line"), _("Load CSV"))
2179 f = StringIO.StringIO(text)
2180 csvReader = csv.reader(f)
2181 self.do_process_from_csvReader(csvReader)
2186 def export_privkeys_dialog(self, password):
2187 if self.wallet.is_watching_only():
2188 self.show_message(_("This is a watching-only wallet"))
2192 d.setWindowTitle(_('Private keys'))
2193 d.setMinimumSize(850, 300)
2194 vbox = QVBoxLayout(d)
2196 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2197 _("Exposing a single private key can compromise your entire wallet!"),
2198 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2199 vbox.addWidget(QLabel(msg))
2205 defaultname = 'electrum-private-keys.csv'
2206 select_msg = _('Select file to export your private keys to')
2207 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2208 vbox.addLayout(hbox)
2210 h, b = ok_cancel_buttons2(d, _('Export'))
2215 addresses = self.wallet.addresses(True)
2217 def privkeys_thread():
2218 for addr in addresses:
2222 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2223 d.emit(SIGNAL('computing_privkeys'))
2224 d.emit(SIGNAL('show_privkeys'))
2226 def show_privkeys():
2227 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2231 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2232 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2233 threading.Thread(target=privkeys_thread).start()
2239 filename = filename_e.text()
2244 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2245 except (IOError, os.error), reason:
2246 export_error_label = _("Electrum was unable to produce a private key-export.")
2247 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2249 except Exception as e:
2250 self.show_message(str(e))
2253 self.show_message(_("Private keys exported."))
2256 def do_export_privkeys(self, fileName, pklist, is_csv):
2257 with open(fileName, "w+") as f:
2259 transaction = csv.writer(f)
2260 transaction.writerow(["address", "private_key"])
2261 for addr, pk in pklist.items():
2262 transaction.writerow(["%34s"%addr,pk])
2265 f.write(json.dumps(pklist, indent = 4))
2268 def do_import_labels(self):
2269 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2270 if not labelsFile: return
2272 f = open(labelsFile, 'r')
2275 for key, value in json.loads(data).items():
2276 self.wallet.set_label(key, value)
2277 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2278 except (IOError, os.error), reason:
2279 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2282 def do_export_labels(self):
2283 labels = self.wallet.labels
2285 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2287 with open(fileName, 'w+') as f:
2288 json.dump(labels, f)
2289 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2290 except (IOError, os.error), reason:
2291 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2294 def export_history_dialog(self):
2297 d.setWindowTitle(_('Export History'))
2298 d.setMinimumSize(400, 200)
2299 vbox = QVBoxLayout(d)
2301 defaultname = os.path.expanduser('~/electrum-history.csv')
2302 select_msg = _('Select file to export your wallet transactions to')
2304 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2305 vbox.addLayout(hbox)
2309 h, b = ok_cancel_buttons2(d, _('Export'))
2314 filename = filename_e.text()
2319 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2320 except (IOError, os.error), reason:
2321 export_error_label = _("Electrum was unable to produce a transaction export.")
2322 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2325 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2328 def do_export_history(self, wallet, fileName, is_csv):
2329 history = wallet.get_tx_history()
2331 for item in history:
2332 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2334 if timestamp is not None:
2336 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2337 except [RuntimeError, TypeError, NameError] as reason:
2338 time_string = "unknown"
2341 time_string = "unknown"
2343 time_string = "pending"
2345 if value is not None:
2346 value_string = format_satoshis(value, True)
2351 fee_string = format_satoshis(fee, True)
2356 label, is_default_label = wallet.get_label(tx_hash)
2357 label = label.encode('utf-8')
2361 balance_string = format_satoshis(balance, False)
2363 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2365 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2367 with open(fileName, "w+") as f:
2369 transaction = csv.writer(f)
2370 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2372 transaction.writerow(line)
2375 f.write(json.dumps(lines, indent = 4))
2378 def sweep_key_dialog(self):
2380 d.setWindowTitle(_('Sweep private keys'))
2381 d.setMinimumSize(600, 300)
2383 vbox = QVBoxLayout(d)
2384 vbox.addWidget(QLabel(_("Enter private keys")))
2386 keys_e = QTextEdit()
2387 keys_e.setTabChangesFocus(True)
2388 vbox.addWidget(keys_e)
2390 h, address_e = address_field(self.wallet.addresses())
2394 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2395 vbox.addLayout(hbox)
2396 button.setEnabled(False)
2399 addr = str(address_e.text())
2400 if bitcoin.is_address(addr):
2404 pk = str(keys_e.toPlainText()).strip()
2405 if Wallet.is_private_key(pk):
2408 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2409 keys_e.textChanged.connect(f)
2410 address_e.textChanged.connect(f)
2414 fee = self.wallet.fee
2415 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2416 self.show_transaction(tx)
2420 def do_import_privkey(self, password):
2421 if not self.wallet.has_imported_keys():
2422 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2423 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2424 + _('Are you sure you understand what you are doing?'), 3, 4)
2427 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2430 text = str(text).split()
2435 addr = self.wallet.import_key(key, password)
2436 except Exception as e:
2442 addrlist.append(addr)
2444 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2446 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2447 self.update_address_tab()
2448 self.update_history_tab()
2451 def settings_dialog(self):
2453 d.setWindowTitle(_('Electrum Settings'))
2455 vbox = QVBoxLayout()
2456 grid = QGridLayout()
2457 grid.setColumnStretch(0,1)
2459 nz_label = QLabel(_('Display zeros') + ':')
2460 grid.addWidget(nz_label, 0, 0)
2461 nz_e = AmountEdit(None,True)
2462 nz_e.setText("%d"% self.num_zeros)
2463 grid.addWidget(nz_e, 0, 1)
2464 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2465 grid.addWidget(HelpButton(msg), 0, 2)
2466 if not self.config.is_modifiable('num_zeros'):
2467 for w in [nz_e, nz_label]: w.setEnabled(False)
2469 lang_label=QLabel(_('Language') + ':')
2470 grid.addWidget(lang_label, 1, 0)
2471 lang_combo = QComboBox()
2472 from electrum.i18n import languages
2473 lang_combo.addItems(languages.values())
2475 index = languages.keys().index(self.config.get("language",''))
2478 lang_combo.setCurrentIndex(index)
2479 grid.addWidget(lang_combo, 1, 1)
2480 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2481 if not self.config.is_modifiable('language'):
2482 for w in [lang_combo, lang_label]: w.setEnabled(False)
2485 fee_label = QLabel(_('Transaction fee') + ':')
2486 grid.addWidget(fee_label, 2, 0)
2487 fee_e = BTCAmountEdit(self.get_decimal_point)
2488 fee_e.setAmount(self.wallet.fee)
2489 grid.addWidget(fee_e, 2, 1)
2490 msg = _('Fee per kilobyte of transaction.') + '\n' \
2491 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2492 grid.addWidget(HelpButton(msg), 2, 2)
2493 if not self.config.is_modifiable('fee_per_kb'):
2494 for w in [fee_e, fee_label]: w.setEnabled(False)
2496 units = ['BTC', 'mBTC', 'bits']
2497 unit_label = QLabel(_('Base unit') + ':')
2498 grid.addWidget(unit_label, 3, 0)
2499 unit_combo = QComboBox()
2500 unit_combo.addItems(units)
2501 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2502 grid.addWidget(unit_combo, 3, 1)
2503 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2504 + '\n1BTC=1000mBTC.\n' \
2505 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2507 usechange_cb = QCheckBox(_('Use change addresses'))
2508 usechange_cb.setChecked(self.wallet.use_change)
2509 grid.addWidget(usechange_cb, 4, 0)
2510 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2511 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2513 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2514 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2515 grid.addWidget(block_ex_label, 5, 0)
2516 block_ex_combo = QComboBox()
2517 block_ex_combo.addItems(block_explorers)
2518 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2519 grid.addWidget(block_ex_combo, 5, 1)
2520 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2522 show_tx = self.config.get('show_before_broadcast', False)
2523 showtx_cb = QCheckBox(_('Show before broadcast'))
2524 showtx_cb.setChecked(show_tx)
2525 grid.addWidget(showtx_cb, 6, 0)
2526 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2528 vbox.addLayout(grid)
2530 vbox.addLayout(ok_cancel_buttons(d))
2534 if not d.exec_(): return
2536 fee = fee_e.get_amount()
2538 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2541 self.wallet.set_fee(fee)
2543 nz = unicode(nz_e.text())
2548 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2551 if self.num_zeros != nz:
2553 self.config.set_key('num_zeros', nz, True)
2554 self.update_history_tab()
2555 self.update_address_tab()
2557 usechange_result = usechange_cb.isChecked()
2558 if self.wallet.use_change != usechange_result:
2559 self.wallet.use_change = usechange_result
2560 self.wallet.storage.put('use_change', self.wallet.use_change)
2562 if showtx_cb.isChecked() != show_tx:
2563 self.config.set_key('show_before_broadcast', not show_tx)
2565 unit_result = units[unit_combo.currentIndex()]
2566 if self.base_unit() != unit_result:
2567 if unit_result == 'BTC':
2568 self.decimal_point = 8
2569 elif unit_result == 'mBTC':
2570 self.decimal_point = 5
2571 elif unit_result == 'bits':
2572 self.decimal_point = 2
2574 raise Exception('Unknown base unit')
2575 self.config.set_key('decimal_point', self.decimal_point, True)
2576 self.update_history_tab()
2577 self.update_status()
2579 need_restart = False
2581 lang_request = languages.keys()[lang_combo.currentIndex()]
2582 if lang_request != self.config.get('language'):
2583 self.config.set_key("language", lang_request, True)
2586 be_result = block_explorers[block_ex_combo.currentIndex()]
2587 self.config.set_key('block_explorer', be_result, True)
2589 run_hook('close_settings_dialog')
2592 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2595 def run_network_dialog(self):
2596 if not self.network:
2598 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2600 def closeEvent(self, event):
2602 self.config.set_key("is_maximized", self.isMaximized())
2603 if not self.isMaximized():
2605 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2606 self.save_column_widths()
2607 self.config.set_key("console-history", self.console.history[-50:], True)
2608 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2612 def plugins_dialog(self):
2613 from electrum.plugins import plugins
2616 d.setWindowTitle(_('Electrum Plugins'))
2619 vbox = QVBoxLayout(d)
2622 scroll = QScrollArea()
2623 scroll.setEnabled(True)
2624 scroll.setWidgetResizable(True)
2625 scroll.setMinimumSize(400,250)
2626 vbox.addWidget(scroll)
2630 w.setMinimumHeight(len(plugins)*35)
2632 grid = QGridLayout()
2633 grid.setColumnStretch(0,1)
2636 def do_toggle(cb, p, w):
2639 if w: w.setEnabled(r)
2641 def mk_toggle(cb, p, w):
2642 return lambda: do_toggle(cb,p,w)
2644 for i, p in enumerate(plugins):
2646 cb = QCheckBox(p.fullname())
2647 cb.setDisabled(not p.is_available())
2648 cb.setChecked(p.is_enabled())
2649 grid.addWidget(cb, i, 0)
2650 if p.requires_settings():
2651 w = p.settings_widget(self)
2652 w.setEnabled( p.is_enabled() )
2653 grid.addWidget(w, i, 1)
2656 cb.clicked.connect(mk_toggle(cb,p,w))
2657 grid.addWidget(HelpButton(p.description()), i, 2)
2659 print_msg(_("Error: cannot display plugin"), p)
2660 traceback.print_exc(file=sys.stdout)
2661 grid.setRowStretch(i+1,1)
2663 vbox.addLayout(close_button(d))
2668 def show_account_details(self, k):
2669 account = self.wallet.accounts[k]
2672 d.setWindowTitle(_('Account Details'))
2675 vbox = QVBoxLayout(d)
2676 name = self.wallet.get_account_name(k)
2677 label = QLabel('Name: ' + name)
2678 vbox.addWidget(label)
2680 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2682 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2684 vbox.addWidget(QLabel(_('Master Public Key:')))
2687 text.setReadOnly(True)
2688 text.setMaximumHeight(170)
2689 vbox.addWidget(text)
2691 mpk_text = '\n'.join( account.get_master_pubkeys() )
2692 text.setText(mpk_text)
2694 vbox.addLayout(close_button(d))