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)
1058 self.wallet.add_keypairs(tx, keypairs, password)
1059 self.wallet.sign_transaction(tx, keypairs, password)
1060 except Exception as e:
1066 self.show_message(tx.error)
1067 self.send_button.setDisabled(False)
1069 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1070 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1071 self.send_button.setDisabled(False)
1074 self.wallet.set_label(tx.hash(), label)
1076 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1077 self.show_transaction(tx)
1079 self.send_button.setDisabled(False)
1082 self.broadcast_transaction(tx)
1084 # keep a reference to WaitingDialog or the gui might crash
1085 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1086 self.waiting_dialog.start()
1090 def broadcast_transaction(self, tx):
1092 def broadcast_thread():
1093 pr = self.payment_request
1095 return self.wallet.sendtx(tx)
1097 if pr.has_expired():
1098 self.payment_request = None
1099 return False, _("Payment request has expired")
1101 status, msg = self.wallet.sendtx(tx)
1105 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1106 self.wallet.storage.put('invoices', self.invoices)
1107 self.update_invoices_tab()
1108 self.payment_request = None
1109 refund_address = self.wallet.addresses()[0]
1110 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1116 def broadcast_done(status, msg):
1118 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1121 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1122 self.send_button.setDisabled(False)
1124 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1125 self.waiting_dialog.start()
1129 def prepare_for_payment_request(self):
1130 self.tabs.setCurrentIndex(1)
1131 self.payto_e.is_pr = True
1132 for e in [self.payto_e, self.amount_e, self.message_e]:
1134 for h in [self.payto_help, self.amount_help, self.message_help]:
1136 self.payto_e.setText(_("please wait..."))
1139 def payment_request_ok(self):
1140 pr = self.payment_request
1142 if pr_id not in self.invoices:
1143 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1144 self.wallet.storage.put('invoices', self.invoices)
1145 self.update_invoices_tab()
1147 print_error('invoice already in list')
1149 status = self.invoices[pr_id][4]
1150 if status == PR_PAID:
1152 self.show_message("invoice already paid")
1153 self.payment_request = None
1156 self.payto_help.show()
1157 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1159 if not pr.has_expired():
1160 self.payto_e.setGreen()
1162 self.payto_e.setExpired()
1164 self.payto_e.setText(pr.domain)
1165 self.amount_e.setText(self.format_amount(pr.get_amount()))
1166 self.message_e.setText(pr.get_memo())
1168 def payment_request_error(self):
1170 self.show_message(self.payment_request.error)
1171 self.payment_request = None
1173 def pay_from_URI(self,URI):
1176 address, amount, label, message, request_url = util.parse_URI(URI)
1178 address, amount, label, message, request_url = util.parse_URI(URI)
1179 except Exception as e:
1180 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1183 self.tabs.setCurrentIndex(1)
1187 if self.wallet.labels.get(address) != label:
1188 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1189 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1190 self.wallet.addressbook.append(address)
1191 self.wallet.set_label(address, label)
1193 label = self.wallet.labels.get(address)
1195 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1197 self.message_e.setText(message)
1199 self.amount_e.setAmount(amount)
1202 from electrum import paymentrequest
1203 def payment_request():
1204 self.payment_request = paymentrequest.PaymentRequest(self.config)
1205 self.payment_request.read(request_url)
1206 if self.payment_request.verify():
1207 self.emit(SIGNAL('payment_request_ok'))
1209 self.emit(SIGNAL('payment_request_error'))
1211 self.pr_thread = threading.Thread(target=payment_request).start()
1212 self.prepare_for_payment_request()
1217 self.payto_e.is_pr = False
1218 self.payto_sig.setVisible(False)
1219 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1223 for h in [self.payto_help, self.amount_help, self.message_help]:
1226 self.payto_help.set_alt(None)
1227 self.set_pay_from([])
1228 self.update_status()
1232 def set_addrs_frozen(self,addrs,freeze):
1234 if not addr: continue
1235 if addr in self.wallet.frozen_addresses and not freeze:
1236 self.wallet.unfreeze(addr)
1237 elif addr not in self.wallet.frozen_addresses and freeze:
1238 self.wallet.freeze(addr)
1239 self.update_address_tab()
1243 def create_list_tab(self, headers):
1244 "generic tab creation method"
1245 l = MyTreeWidget(self)
1246 l.setColumnCount( len(headers) )
1247 l.setHeaderLabels( headers )
1250 vbox = QVBoxLayout()
1257 vbox.addWidget(buttons)
1262 def create_addresses_tab(self):
1263 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1264 for i,width in enumerate(self.column_widths['receive']):
1265 l.setColumnWidth(i, width)
1266 l.setContextMenuPolicy(Qt.CustomContextMenu)
1267 l.customContextMenuRequested.connect(self.create_receive_menu)
1268 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1269 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1270 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1271 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1272 self.address_list = l
1278 def save_column_widths(self):
1279 self.column_widths["receive"] = []
1280 for i in range(self.address_list.columnCount() -1):
1281 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1283 self.column_widths["history"] = []
1284 for i in range(self.history_list.columnCount() - 1):
1285 self.column_widths["history"].append(self.history_list.columnWidth(i))
1287 self.column_widths["contacts"] = []
1288 for i in range(self.contacts_list.columnCount() - 1):
1289 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1291 self.config.set_key("column_widths_2", self.column_widths, True)
1294 def create_contacts_tab(self):
1295 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1296 l.setContextMenuPolicy(Qt.CustomContextMenu)
1297 l.customContextMenuRequested.connect(self.create_contact_menu)
1298 for i,width in enumerate(self.column_widths['contacts']):
1299 l.setColumnWidth(i, width)
1300 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1301 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1302 self.contacts_list = l
1306 def create_invoices_tab(self):
1307 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1308 l.setColumnWidth(0, 150)
1310 h.setStretchLastSection(False)
1311 h.setResizeMode(1, QHeaderView.Stretch)
1312 l.setContextMenuPolicy(Qt.CustomContextMenu)
1313 l.customContextMenuRequested.connect(self.create_invoice_menu)
1314 self.invoices_list = l
1317 def update_invoices_tab(self):
1318 invoices = self.wallet.storage.get('invoices', {})
1319 l = self.invoices_list
1321 for key, value in invoices.items():
1323 domain, memo, amount, expiration_date, status, tx_hash = value
1327 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1329 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1330 l.addTopLevelItem(item)
1332 l.setCurrentItem(l.topLevelItem(0))
1336 def delete_imported_key(self, addr):
1337 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1338 self.wallet.delete_imported_key(addr)
1339 self.update_address_tab()
1340 self.update_history_tab()
1342 def edit_account_label(self, k):
1343 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1345 label = unicode(text)
1346 self.wallet.set_label(k,label)
1347 self.update_address_tab()
1349 def account_set_expanded(self, item, k, b):
1351 self.accounts_expanded[k] = b
1353 def create_account_menu(self, position, k, item):
1355 if item.isExpanded():
1356 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1358 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1359 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1360 if self.wallet.seed_version > 4:
1361 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1362 if self.wallet.account_is_pending(k):
1363 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1364 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1366 def delete_pending_account(self, k):
1367 self.wallet.delete_pending_account(k)
1368 self.update_address_tab()
1370 def create_receive_menu(self, position):
1371 # fixme: this function apparently has a side effect.
1372 # if it is not called the menu pops up several times
1373 #self.address_list.selectedIndexes()
1375 selected = self.address_list.selectedItems()
1376 multi_select = len(selected) > 1
1377 addrs = [unicode(item.text(0)) for item in selected]
1378 if not multi_select:
1379 item = self.address_list.itemAt(position)
1383 if not is_valid(addr):
1384 k = str(item.data(0,32).toString())
1386 self.create_account_menu(position, k, item)
1388 item.setExpanded(not item.isExpanded())
1392 if not multi_select:
1393 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1394 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1395 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1396 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1397 if not self.wallet.is_watching_only():
1398 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1399 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1400 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1401 if self.wallet.is_imported(addr):
1402 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1404 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1405 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1406 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1407 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1410 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1411 if any(can_send(addr) for addr in addrs):
1412 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1414 run_hook('receive_menu', menu, addrs)
1415 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1418 def get_sendable_balance(self):
1419 return sum(map(lambda x:x['value'], self.get_coins()))
1422 def get_coins(self):
1424 return self.pay_from
1426 domain = self.wallet.get_account_addresses(self.current_account)
1427 for i in self.wallet.frozen_addresses:
1428 if i in domain: domain.remove(i)
1429 return self.wallet.get_unspent_coins(domain)
1432 def send_from_addresses(self, addrs):
1433 self.set_pay_from( addrs )
1434 self.tabs.setCurrentIndex(1)
1437 def payto(self, addr):
1439 label = self.wallet.labels.get(addr)
1440 m_addr = label + ' <' + addr + '>' if label else addr
1441 self.tabs.setCurrentIndex(1)
1442 self.payto_e.setText(m_addr)
1443 self.amount_e.setFocus()
1446 def delete_contact(self, x):
1447 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1448 self.wallet.delete_contact(x)
1449 self.wallet.set_label(x, None)
1450 self.update_history_tab()
1451 self.update_contacts_tab()
1452 self.update_completions()
1455 def create_contact_menu(self, position):
1456 item = self.contacts_list.itemAt(position)
1459 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1461 addr = unicode(item.text(0))
1462 label = unicode(item.text(1))
1463 is_editable = item.data(0,32).toBool()
1464 payto_addr = item.data(0,33).toString()
1465 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1466 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1467 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1469 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1470 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1472 run_hook('create_contact_menu', menu, item)
1473 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1475 def delete_invoice(self, key):
1476 self.invoices.pop(key)
1477 self.wallet.storage.put('invoices', self.invoices)
1478 self.update_invoices_tab()
1480 def show_invoice(self, key):
1481 from electrum.paymentrequest import PaymentRequest
1482 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1483 pr = PaymentRequest(self.config)
1487 self.show_pr_details(pr)
1489 def show_pr_details(self, pr):
1490 msg = 'Domain: ' + pr.domain
1491 msg += '\nStatus: ' + pr.get_status()
1492 msg += '\nMemo: ' + pr.get_memo()
1493 msg += '\nPayment URL: ' + pr.payment_url
1494 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1495 QMessageBox.information(self, 'Invoice', msg , 'OK')
1497 def do_pay_invoice(self, key):
1498 from electrum.paymentrequest import PaymentRequest
1499 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1500 pr = PaymentRequest(self.config)
1503 self.payment_request = pr
1504 self.prepare_for_payment_request()
1506 self.payment_request_ok()
1508 self.payment_request_error()
1511 def create_invoice_menu(self, position):
1512 item = self.invoices_list.itemAt(position)
1515 k = self.invoices_list.indexOfTopLevelItem(item)
1516 key = self.invoices.keys()[k]
1517 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1519 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1520 if status == PR_UNPAID:
1521 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1522 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1523 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1527 def update_address_tab(self):
1528 l = self.address_list
1529 # extend the syntax for consistency
1530 l.addChild = l.addTopLevelItem
1531 l.insertChild = l.insertTopLevelItem
1535 accounts = self.wallet.get_accounts()
1536 if self.current_account is None:
1537 account_items = sorted(accounts.items())
1539 account_items = [(self.current_account, accounts.get(self.current_account))]
1542 for k, account in account_items:
1544 if len(accounts) > 1:
1545 name = self.wallet.get_account_name(k)
1546 c,u = self.wallet.get_account_balance(k)
1547 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1548 l.addTopLevelItem(account_item)
1549 account_item.setExpanded(self.accounts_expanded.get(k, True))
1550 account_item.setData(0, 32, k)
1554 sequences = [0,1] if account.has_change() else [0]
1555 for is_change in sequences:
1556 if len(sequences) > 1:
1557 name = _("Receiving") if not is_change else _("Change")
1558 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1559 account_item.addChild(seq_item)
1561 seq_item.setExpanded(True)
1563 seq_item = account_item
1565 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1568 addr_list = account.get_addresses(is_change)
1569 for address in addr_list:
1570 num, is_used = self.wallet.is_used(address)
1571 label = self.wallet.labels.get(address,'')
1572 c, u = self.wallet.get_addr_balance(address)
1573 balance = self.format_amount(c + u)
1574 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1575 item.setFont(0, QFont(MONOSPACE_FONT))
1576 item.setData(0, 32, True) # label can be edited
1577 if address in self.wallet.frozen_addresses:
1578 item.setBackgroundColor(0, QColor('lightblue'))
1579 if self.wallet.is_beyond_limit(address, account, is_change):
1580 item.setBackgroundColor(0, QColor('red'))
1583 seq_item.insertChild(0, used_item)
1585 used_item.addChild(item)
1587 seq_item.addChild(item)
1589 # we use column 1 because column 0 may be hidden
1590 l.setCurrentItem(l.topLevelItem(0),1)
1593 def update_contacts_tab(self):
1594 l = self.contacts_list
1597 for address in self.wallet.addressbook:
1598 label = self.wallet.labels.get(address,'')
1599 n = self.wallet.get_num_tx(address)
1600 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1601 item.setFont(0, QFont(MONOSPACE_FONT))
1602 # 32 = label can be edited (bool)
1603 item.setData(0,32, True)
1605 item.setData(0,33, address)
1606 l.addTopLevelItem(item)
1608 run_hook('update_contacts_tab', l)
1609 l.setCurrentItem(l.topLevelItem(0))
1612 def create_console_tab(self):
1613 from console import Console
1614 self.console = console = Console()
1618 def update_console(self):
1619 console = self.console
1620 console.history = self.config.get("console-history",[])
1621 console.history_index = len(console.history)
1623 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1624 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1626 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1628 def mkfunc(f, method):
1629 return lambda *args: apply( f, (method, args, self.password_dialog ))
1631 if m[0]=='_' or m in ['network','wallet']: continue
1632 methods[m] = mkfunc(c._run, m)
1634 console.updateNamespace(methods)
1637 def change_account(self,s):
1638 if s == _("All accounts"):
1639 self.current_account = None
1641 accounts = self.wallet.get_account_names()
1642 for k, v in accounts.items():
1644 self.current_account = k
1645 self.update_history_tab()
1646 self.update_status()
1647 self.update_address_tab()
1648 self.update_receive_tab()
1650 def create_status_bar(self):
1653 sb.setFixedHeight(35)
1654 qtVersion = qVersion()
1656 self.balance_label = QLabel("")
1657 sb.addWidget(self.balance_label)
1659 from version_getter import UpdateLabel
1660 self.updatelabel = UpdateLabel(self.config, sb)
1662 self.account_selector = QComboBox()
1663 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1664 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1665 sb.addPermanentWidget(self.account_selector)
1667 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1668 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1670 self.lock_icon = QIcon()
1671 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1672 sb.addPermanentWidget( self.password_button )
1674 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1675 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1676 sb.addPermanentWidget( self.seed_button )
1677 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1678 sb.addPermanentWidget( self.status_button )
1680 run_hook('create_status_bar', (sb,))
1682 self.setStatusBar(sb)
1685 def update_lock_icon(self):
1686 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1687 self.password_button.setIcon( icon )
1690 def update_buttons_on_seed(self):
1691 if self.wallet.has_seed():
1692 self.seed_button.show()
1694 self.seed_button.hide()
1696 if not self.wallet.is_watching_only():
1697 self.password_button.show()
1698 self.send_button.setText(_("Send"))
1700 self.password_button.hide()
1701 self.send_button.setText(_("Create unsigned transaction"))
1704 def change_password_dialog(self):
1705 from password_dialog import PasswordDialog
1706 d = PasswordDialog(self.wallet, self)
1708 self.update_lock_icon()
1711 def new_contact_dialog(self):
1714 d.setWindowTitle(_("New Contact"))
1715 vbox = QVBoxLayout(d)
1716 vbox.addWidget(QLabel(_('New Contact')+':'))
1718 grid = QGridLayout()
1721 grid.addWidget(QLabel(_("Address")), 1, 0)
1722 grid.addWidget(line1, 1, 1)
1723 grid.addWidget(QLabel(_("Name")), 2, 0)
1724 grid.addWidget(line2, 2, 1)
1726 vbox.addLayout(grid)
1727 vbox.addLayout(ok_cancel_buttons(d))
1732 address = str(line1.text())
1733 label = unicode(line2.text())
1735 if not is_valid(address):
1736 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1739 self.wallet.add_contact(address)
1741 self.wallet.set_label(address, label)
1743 self.update_contacts_tab()
1744 self.update_history_tab()
1745 self.update_completions()
1746 self.tabs.setCurrentIndex(3)
1750 def new_account_dialog(self, password):
1752 dialog = QDialog(self)
1754 dialog.setWindowTitle(_("New Account"))
1756 vbox = QVBoxLayout()
1757 vbox.addWidget(QLabel(_('Account name')+':'))
1760 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1761 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1766 vbox.addLayout(ok_cancel_buttons(dialog))
1767 dialog.setLayout(vbox)
1771 name = str(e.text())
1774 self.wallet.create_pending_account(name, password)
1775 self.update_address_tab()
1776 self.tabs.setCurrentIndex(2)
1781 def show_master_public_keys(self):
1783 dialog = QDialog(self)
1785 dialog.setWindowTitle(_("Master Public Keys"))
1787 main_layout = QGridLayout()
1788 mpk_dict = self.wallet.get_master_public_keys()
1790 for key, value in mpk_dict.items():
1791 main_layout.addWidget(QLabel(key), i, 0)
1792 mpk_text = QTextEdit()
1793 mpk_text.setReadOnly(True)
1794 mpk_text.setMaximumHeight(170)
1795 mpk_text.setText(value)
1796 main_layout.addWidget(mpk_text, i + 1, 0)
1799 vbox = QVBoxLayout()
1800 vbox.addLayout(main_layout)
1801 vbox.addLayout(close_button(dialog))
1803 dialog.setLayout(vbox)
1808 def show_seed_dialog(self, password):
1809 if not self.wallet.has_seed():
1810 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1814 mnemonic = self.wallet.get_mnemonic(password)
1816 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1818 from seed_dialog import SeedDialog
1819 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1824 def show_qrcode(self, data, title = _("QR code")):
1827 d = QRDialog(data, self, title)
1831 def do_protect(self, func, args):
1832 if self.wallet.use_encryption:
1833 password = self.password_dialog()
1839 if args != (False,):
1840 args = (self,) + args + (password,)
1842 args = (self,password)
1846 def show_public_keys(self, address):
1847 if not address: return
1849 pubkey_list = self.wallet.get_public_keys(address)
1850 except Exception as e:
1851 traceback.print_exc(file=sys.stdout)
1852 self.show_message(str(e))
1856 d.setMinimumSize(600, 200)
1858 vbox = QVBoxLayout()
1859 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1860 vbox.addWidget( QLabel(_("Public key") + ':'))
1862 keys.setReadOnly(True)
1863 keys.setText('\n'.join(pubkey_list))
1864 vbox.addWidget(keys)
1865 vbox.addLayout(close_button(d))
1870 def show_private_key(self, address, password):
1871 if not address: return
1873 pk_list = self.wallet.get_private_key(address, password)
1874 except Exception as e:
1875 traceback.print_exc(file=sys.stdout)
1876 self.show_message(str(e))
1880 d.setMinimumSize(600, 200)
1882 vbox = QVBoxLayout()
1883 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1884 vbox.addWidget( QLabel(_("Private key") + ':'))
1886 keys.setReadOnly(True)
1887 keys.setText('\n'.join(pk_list))
1888 vbox.addWidget(keys)
1889 vbox.addLayout(close_button(d))
1895 def do_sign(self, address, message, signature, password):
1896 message = unicode(message.toPlainText())
1897 message = message.encode('utf-8')
1899 sig = self.wallet.sign_message(str(address.text()), message, password)
1900 signature.setText(sig)
1901 except Exception as e:
1902 self.show_message(str(e))
1904 def do_verify(self, address, message, signature):
1905 message = unicode(message.toPlainText())
1906 message = message.encode('utf-8')
1907 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1908 self.show_message(_("Signature verified"))
1910 self.show_message(_("Error: wrong signature"))
1913 def sign_verify_message(self, address=''):
1916 d.setWindowTitle(_('Sign/verify Message'))
1917 d.setMinimumSize(410, 290)
1919 layout = QGridLayout(d)
1921 message_e = QTextEdit()
1922 layout.addWidget(QLabel(_('Message')), 1, 0)
1923 layout.addWidget(message_e, 1, 1)
1924 layout.setRowStretch(2,3)
1926 address_e = QLineEdit()
1927 address_e.setText(address)
1928 layout.addWidget(QLabel(_('Address')), 2, 0)
1929 layout.addWidget(address_e, 2, 1)
1931 signature_e = QTextEdit()
1932 layout.addWidget(QLabel(_('Signature')), 3, 0)
1933 layout.addWidget(signature_e, 3, 1)
1934 layout.setRowStretch(3,1)
1936 hbox = QHBoxLayout()
1938 b = QPushButton(_("Sign"))
1939 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1942 b = QPushButton(_("Verify"))
1943 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1946 b = QPushButton(_("Close"))
1947 b.clicked.connect(d.accept)
1949 layout.addLayout(hbox, 4, 1)
1954 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1956 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1957 message_e.setText(decrypted)
1958 except Exception as e:
1959 self.show_message(str(e))
1962 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1963 message = unicode(message_e.toPlainText())
1964 message = message.encode('utf-8')
1966 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1967 encrypted_e.setText(encrypted)
1968 except Exception as e:
1969 self.show_message(str(e))
1973 def encrypt_message(self, address = ''):
1976 d.setWindowTitle(_('Encrypt/decrypt Message'))
1977 d.setMinimumSize(610, 490)
1979 layout = QGridLayout(d)
1981 message_e = QTextEdit()
1982 layout.addWidget(QLabel(_('Message')), 1, 0)
1983 layout.addWidget(message_e, 1, 1)
1984 layout.setRowStretch(2,3)
1986 pubkey_e = QLineEdit()
1988 pubkey = self.wallet.getpubkeys(address)[0]
1989 pubkey_e.setText(pubkey)
1990 layout.addWidget(QLabel(_('Public key')), 2, 0)
1991 layout.addWidget(pubkey_e, 2, 1)
1993 encrypted_e = QTextEdit()
1994 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1995 layout.addWidget(encrypted_e, 3, 1)
1996 layout.setRowStretch(3,1)
1998 hbox = QHBoxLayout()
1999 b = QPushButton(_("Encrypt"))
2000 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2003 b = QPushButton(_("Decrypt"))
2004 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2007 b = QPushButton(_("Close"))
2008 b.clicked.connect(d.accept)
2011 layout.addLayout(hbox, 4, 1)
2015 def question(self, msg):
2016 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2018 def show_message(self, msg):
2019 QMessageBox.information(self, _('Message'), msg, _('OK'))
2021 def password_dialog(self, msg=None):
2024 d.setWindowTitle(_("Enter Password"))
2029 vbox = QVBoxLayout()
2031 msg = _('Please enter your password')
2032 vbox.addWidget(QLabel(msg))
2034 grid = QGridLayout()
2036 grid.addWidget(QLabel(_('Password')), 1, 0)
2037 grid.addWidget(pw, 1, 1)
2038 vbox.addLayout(grid)
2040 vbox.addLayout(ok_cancel_buttons(d))
2043 run_hook('password_dialog', pw, grid, 1)
2044 if not d.exec_(): return
2045 return unicode(pw.text())
2054 def tx_from_text(self, txt):
2055 "json or raw hexadecimal"
2064 return Transaction(txt)
2066 traceback.print_exc(file=sys.stdout)
2067 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2071 tx_dict = json.loads(str(txt))
2072 assert "hex" in tx_dict.keys()
2073 tx = Transaction(tx_dict["hex"])
2074 #if tx_dict.has_key("input_info"):
2075 # input_info = json.loads(tx_dict['input_info'])
2076 # tx.add_input_info(input_info)
2079 traceback.print_exc(file=sys.stdout)
2080 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2084 def read_tx_from_file(self):
2085 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2089 with open(fileName, "r") as f:
2090 file_content = f.read()
2091 except (ValueError, IOError, os.error), reason:
2092 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2094 return self.tx_from_text(file_content)
2098 def sign_raw_transaction(self, tx, password):
2100 self.wallet.signrawtransaction(tx, [], password)
2101 except Exception as e:
2102 traceback.print_exc(file=sys.stdout)
2103 QMessageBox.warning(self, _("Error"), str(e))
2105 def do_process_from_text(self):
2106 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2109 tx = self.tx_from_text(text)
2111 self.show_transaction(tx)
2113 def do_process_from_file(self):
2114 tx = self.read_tx_from_file()
2116 self.show_transaction(tx)
2118 def do_process_from_txid(self):
2119 from electrum import transaction
2120 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2122 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2124 tx = transaction.Transaction(r)
2126 self.show_transaction(tx)
2128 self.show_message("unknown transaction")
2130 def do_process_from_csvReader(self, csvReader):
2135 for position, row in enumerate(csvReader):
2137 if not is_valid(address):
2138 errors.append((position, address))
2140 amount = Decimal(row[1])
2141 amount = int(100000000*amount)
2142 outputs.append((address, amount))
2143 except (ValueError, IOError, os.error), reason:
2144 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2148 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2149 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2153 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2154 except Exception as e:
2155 self.show_message(str(e))
2158 self.show_transaction(tx)
2160 def do_process_from_csv_file(self):
2161 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2165 with open(fileName, "r") as f:
2166 csvReader = csv.reader(f)
2167 self.do_process_from_csvReader(csvReader)
2168 except (ValueError, IOError, os.error), reason:
2169 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2172 def do_process_from_csv_text(self):
2173 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2174 + _("Format: address, amount. One output per line"), _("Load CSV"))
2177 f = StringIO.StringIO(text)
2178 csvReader = csv.reader(f)
2179 self.do_process_from_csvReader(csvReader)
2184 def export_privkeys_dialog(self, password):
2185 if self.wallet.is_watching_only():
2186 self.show_message(_("This is a watching-only wallet"))
2190 d.setWindowTitle(_('Private keys'))
2191 d.setMinimumSize(850, 300)
2192 vbox = QVBoxLayout(d)
2194 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2195 _("Exposing a single private key can compromise your entire wallet!"),
2196 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2197 vbox.addWidget(QLabel(msg))
2203 defaultname = 'electrum-private-keys.csv'
2204 select_msg = _('Select file to export your private keys to')
2205 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2206 vbox.addLayout(hbox)
2208 h, b = ok_cancel_buttons2(d, _('Export'))
2213 addresses = self.wallet.addresses(True)
2215 def privkeys_thread():
2216 for addr in addresses:
2220 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2221 d.emit(SIGNAL('computing_privkeys'))
2222 d.emit(SIGNAL('show_privkeys'))
2224 def show_privkeys():
2225 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2229 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2230 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2231 threading.Thread(target=privkeys_thread).start()
2237 filename = filename_e.text()
2242 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2243 except (IOError, os.error), reason:
2244 export_error_label = _("Electrum was unable to produce a private key-export.")
2245 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2247 except Exception as e:
2248 self.show_message(str(e))
2251 self.show_message(_("Private keys exported."))
2254 def do_export_privkeys(self, fileName, pklist, is_csv):
2255 with open(fileName, "w+") as f:
2257 transaction = csv.writer(f)
2258 transaction.writerow(["address", "private_key"])
2259 for addr, pk in pklist.items():
2260 transaction.writerow(["%34s"%addr,pk])
2263 f.write(json.dumps(pklist, indent = 4))
2266 def do_import_labels(self):
2267 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2268 if not labelsFile: return
2270 f = open(labelsFile, 'r')
2273 for key, value in json.loads(data).items():
2274 self.wallet.set_label(key, value)
2275 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2276 except (IOError, os.error), reason:
2277 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2280 def do_export_labels(self):
2281 labels = self.wallet.labels
2283 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2285 with open(fileName, 'w+') as f:
2286 json.dump(labels, f)
2287 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2288 except (IOError, os.error), reason:
2289 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2292 def export_history_dialog(self):
2295 d.setWindowTitle(_('Export History'))
2296 d.setMinimumSize(400, 200)
2297 vbox = QVBoxLayout(d)
2299 defaultname = os.path.expanduser('~/electrum-history.csv')
2300 select_msg = _('Select file to export your wallet transactions to')
2302 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2303 vbox.addLayout(hbox)
2307 h, b = ok_cancel_buttons2(d, _('Export'))
2312 filename = filename_e.text()
2317 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2318 except (IOError, os.error), reason:
2319 export_error_label = _("Electrum was unable to produce a transaction export.")
2320 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2323 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2326 def do_export_history(self, wallet, fileName, is_csv):
2327 history = wallet.get_tx_history()
2329 for item in history:
2330 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2332 if timestamp is not None:
2334 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2335 except [RuntimeError, TypeError, NameError] as reason:
2336 time_string = "unknown"
2339 time_string = "unknown"
2341 time_string = "pending"
2343 if value is not None:
2344 value_string = format_satoshis(value, True)
2349 fee_string = format_satoshis(fee, True)
2354 label, is_default_label = wallet.get_label(tx_hash)
2355 label = label.encode('utf-8')
2359 balance_string = format_satoshis(balance, False)
2361 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2363 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2365 with open(fileName, "w+") as f:
2367 transaction = csv.writer(f)
2368 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2370 transaction.writerow(line)
2373 f.write(json.dumps(lines, indent = 4))
2376 def sweep_key_dialog(self):
2378 d.setWindowTitle(_('Sweep private keys'))
2379 d.setMinimumSize(600, 300)
2381 vbox = QVBoxLayout(d)
2382 vbox.addWidget(QLabel(_("Enter private keys")))
2384 keys_e = QTextEdit()
2385 keys_e.setTabChangesFocus(True)
2386 vbox.addWidget(keys_e)
2388 h, address_e = address_field(self.wallet.addresses())
2392 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2393 vbox.addLayout(hbox)
2394 button.setEnabled(False)
2397 addr = str(address_e.text())
2398 if bitcoin.is_address(addr):
2402 pk = str(keys_e.toPlainText()).strip()
2403 if Wallet.is_private_key(pk):
2406 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2407 keys_e.textChanged.connect(f)
2408 address_e.textChanged.connect(f)
2412 fee = self.wallet.fee
2413 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2414 self.show_transaction(tx)
2418 def do_import_privkey(self, password):
2419 if not self.wallet.has_imported_keys():
2420 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2421 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2422 + _('Are you sure you understand what you are doing?'), 3, 4)
2425 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2428 text = str(text).split()
2433 addr = self.wallet.import_key(key, password)
2434 except Exception as e:
2440 addrlist.append(addr)
2442 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2444 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2445 self.update_address_tab()
2446 self.update_history_tab()
2449 def settings_dialog(self):
2451 d.setWindowTitle(_('Electrum Settings'))
2453 vbox = QVBoxLayout()
2454 grid = QGridLayout()
2455 grid.setColumnStretch(0,1)
2457 nz_label = QLabel(_('Display zeros') + ':')
2458 grid.addWidget(nz_label, 0, 0)
2459 nz_e = AmountEdit(None,True)
2460 nz_e.setText("%d"% self.num_zeros)
2461 grid.addWidget(nz_e, 0, 1)
2462 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2463 grid.addWidget(HelpButton(msg), 0, 2)
2464 if not self.config.is_modifiable('num_zeros'):
2465 for w in [nz_e, nz_label]: w.setEnabled(False)
2467 lang_label=QLabel(_('Language') + ':')
2468 grid.addWidget(lang_label, 1, 0)
2469 lang_combo = QComboBox()
2470 from electrum.i18n import languages
2471 lang_combo.addItems(languages.values())
2473 index = languages.keys().index(self.config.get("language",''))
2476 lang_combo.setCurrentIndex(index)
2477 grid.addWidget(lang_combo, 1, 1)
2478 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2479 if not self.config.is_modifiable('language'):
2480 for w in [lang_combo, lang_label]: w.setEnabled(False)
2483 fee_label = QLabel(_('Transaction fee') + ':')
2484 grid.addWidget(fee_label, 2, 0)
2485 fee_e = BTCAmountEdit(self.get_decimal_point)
2486 fee_e.setAmount(self.wallet.fee)
2487 grid.addWidget(fee_e, 2, 1)
2488 msg = _('Fee per kilobyte of transaction.') + '\n' \
2489 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2490 grid.addWidget(HelpButton(msg), 2, 2)
2491 if not self.config.is_modifiable('fee_per_kb'):
2492 for w in [fee_e, fee_label]: w.setEnabled(False)
2494 units = ['BTC', 'mBTC', 'bits']
2495 unit_label = QLabel(_('Base unit') + ':')
2496 grid.addWidget(unit_label, 3, 0)
2497 unit_combo = QComboBox()
2498 unit_combo.addItems(units)
2499 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2500 grid.addWidget(unit_combo, 3, 1)
2501 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2502 + '\n1BTC=1000mBTC.\n' \
2503 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2505 usechange_cb = QCheckBox(_('Use change addresses'))
2506 usechange_cb.setChecked(self.wallet.use_change)
2507 grid.addWidget(usechange_cb, 4, 0)
2508 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2509 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2511 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2512 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2513 grid.addWidget(block_ex_label, 5, 0)
2514 block_ex_combo = QComboBox()
2515 block_ex_combo.addItems(block_explorers)
2516 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2517 grid.addWidget(block_ex_combo, 5, 1)
2518 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2520 show_tx = self.config.get('show_before_broadcast', False)
2521 showtx_cb = QCheckBox(_('Show before broadcast'))
2522 showtx_cb.setChecked(show_tx)
2523 grid.addWidget(showtx_cb, 6, 0)
2524 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2526 vbox.addLayout(grid)
2528 vbox.addLayout(ok_cancel_buttons(d))
2532 if not d.exec_(): return
2534 fee = fee_e.get_amount()
2536 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2539 self.wallet.set_fee(fee)
2541 nz = unicode(nz_e.text())
2546 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2549 if self.num_zeros != nz:
2551 self.config.set_key('num_zeros', nz, True)
2552 self.update_history_tab()
2553 self.update_address_tab()
2555 usechange_result = usechange_cb.isChecked()
2556 if self.wallet.use_change != usechange_result:
2557 self.wallet.use_change = usechange_result
2558 self.wallet.storage.put('use_change', self.wallet.use_change)
2560 if showtx_cb.isChecked() != show_tx:
2561 self.config.set_key('show_before_broadcast', not show_tx)
2563 unit_result = units[unit_combo.currentIndex()]
2564 if self.base_unit() != unit_result:
2565 if unit_result == 'BTC':
2566 self.decimal_point = 8
2567 elif unit_result == 'mBTC':
2568 self.decimal_point = 5
2569 elif unit_result == 'bits':
2570 self.decimal_point = 2
2572 raise Exception('Unknown base unit')
2573 self.config.set_key('decimal_point', self.decimal_point, True)
2574 self.update_history_tab()
2575 self.update_status()
2577 need_restart = False
2579 lang_request = languages.keys()[lang_combo.currentIndex()]
2580 if lang_request != self.config.get('language'):
2581 self.config.set_key("language", lang_request, True)
2584 be_result = block_explorers[block_ex_combo.currentIndex()]
2585 self.config.set_key('block_explorer', be_result, True)
2587 run_hook('close_settings_dialog')
2590 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2593 def run_network_dialog(self):
2594 if not self.network:
2596 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2598 def closeEvent(self, event):
2600 self.config.set_key("is_maximized", self.isMaximized())
2601 if not self.isMaximized():
2603 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2604 self.save_column_widths()
2605 self.config.set_key("console-history", self.console.history[-50:], True)
2606 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2610 def plugins_dialog(self):
2611 from electrum.plugins import plugins
2614 d.setWindowTitle(_('Electrum Plugins'))
2617 vbox = QVBoxLayout(d)
2620 scroll = QScrollArea()
2621 scroll.setEnabled(True)
2622 scroll.setWidgetResizable(True)
2623 scroll.setMinimumSize(400,250)
2624 vbox.addWidget(scroll)
2628 w.setMinimumHeight(len(plugins)*35)
2630 grid = QGridLayout()
2631 grid.setColumnStretch(0,1)
2634 def do_toggle(cb, p, w):
2637 if w: w.setEnabled(r)
2639 def mk_toggle(cb, p, w):
2640 return lambda: do_toggle(cb,p,w)
2642 for i, p in enumerate(plugins):
2644 cb = QCheckBox(p.fullname())
2645 cb.setDisabled(not p.is_available())
2646 cb.setChecked(p.is_enabled())
2647 grid.addWidget(cb, i, 0)
2648 if p.requires_settings():
2649 w = p.settings_widget(self)
2650 w.setEnabled( p.is_enabled() )
2651 grid.addWidget(w, i, 1)
2654 cb.clicked.connect(mk_toggle(cb,p,w))
2655 grid.addWidget(HelpButton(p.description()), i, 2)
2657 print_msg(_("Error: cannot display plugin"), p)
2658 traceback.print_exc(file=sys.stdout)
2659 grid.setRowStretch(i+1,1)
2661 vbox.addLayout(close_button(d))
2666 def show_account_details(self, k):
2667 account = self.wallet.accounts[k]
2670 d.setWindowTitle(_('Account Details'))
2673 vbox = QVBoxLayout(d)
2674 name = self.wallet.get_account_name(k)
2675 label = QLabel('Name: ' + name)
2676 vbox.addWidget(label)
2678 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2680 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2682 vbox.addWidget(QLabel(_('Master Public Key:')))
2685 text.setReadOnly(True)
2686 text.setMaximumHeight(170)
2687 vbox.addWidget(text)
2689 mpk_text = '\n'.join( account.get_master_pubkeys() )
2690 text.setText(mpk_text)
2692 vbox.addLayout(close_button(d))