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.history.get(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 type, addr, amount in outputs:
996 QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK'))
998 if type == 'op_return':
1000 if type == 'address' and 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[2], 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[1], 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:
1063 traceback.print_exc(file=sys.stdout)
1069 self.show_message(tx.error)
1070 self.send_button.setDisabled(False)
1072 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1073 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1074 self.send_button.setDisabled(False)
1077 self.wallet.set_label(tx.hash(), label)
1079 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1080 self.show_transaction(tx)
1082 self.send_button.setDisabled(False)
1085 self.broadcast_transaction(tx)
1087 # keep a reference to WaitingDialog or the gui might crash
1088 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1089 self.waiting_dialog.start()
1093 def broadcast_transaction(self, tx):
1095 def broadcast_thread():
1096 pr = self.payment_request
1098 return self.wallet.sendtx(tx)
1100 if pr.has_expired():
1101 self.payment_request = None
1102 return False, _("Payment request has expired")
1104 status, msg = self.wallet.sendtx(tx)
1108 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1109 self.wallet.storage.put('invoices', self.invoices)
1110 self.update_invoices_tab()
1111 self.payment_request = None
1112 refund_address = self.wallet.addresses()[0]
1113 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1119 def broadcast_done(status, msg):
1121 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1124 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1125 self.send_button.setDisabled(False)
1127 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1128 self.waiting_dialog.start()
1132 def prepare_for_payment_request(self):
1133 self.tabs.setCurrentIndex(1)
1134 self.payto_e.is_pr = True
1135 for e in [self.payto_e, self.amount_e, self.message_e]:
1137 for h in [self.payto_help, self.amount_help, self.message_help]:
1139 self.payto_e.setText(_("please wait..."))
1142 def payment_request_ok(self):
1143 pr = self.payment_request
1145 if pr_id not in self.invoices:
1146 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1147 self.wallet.storage.put('invoices', self.invoices)
1148 self.update_invoices_tab()
1150 print_error('invoice already in list')
1152 status = self.invoices[pr_id][4]
1153 if status == PR_PAID:
1155 self.show_message("invoice already paid")
1156 self.payment_request = None
1159 self.payto_help.show()
1160 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1162 if not pr.has_expired():
1163 self.payto_e.setGreen()
1165 self.payto_e.setExpired()
1167 self.payto_e.setText(pr.domain)
1168 self.amount_e.setText(self.format_amount(pr.get_amount()))
1169 self.message_e.setText(pr.get_memo())
1171 def payment_request_error(self):
1173 self.show_message(self.payment_request.error)
1174 self.payment_request = None
1176 def pay_from_URI(self,URI):
1179 address, amount, label, message, request_url = util.parse_URI(URI)
1181 address, amount, label, message, request_url = util.parse_URI(URI)
1182 except Exception as e:
1183 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1186 self.tabs.setCurrentIndex(1)
1190 if self.wallet.labels.get(address) != label:
1191 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1192 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1193 self.wallet.addressbook.append(address)
1194 self.wallet.set_label(address, label)
1196 label = self.wallet.labels.get(address)
1198 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1200 self.message_e.setText(message)
1202 self.amount_e.setAmount(amount)
1205 from electrum import paymentrequest
1206 def payment_request():
1207 self.payment_request = paymentrequest.PaymentRequest(self.config)
1208 self.payment_request.read(request_url)
1209 if self.payment_request.verify():
1210 self.emit(SIGNAL('payment_request_ok'))
1212 self.emit(SIGNAL('payment_request_error'))
1214 self.pr_thread = threading.Thread(target=payment_request).start()
1215 self.prepare_for_payment_request()
1220 self.payto_e.is_pr = False
1221 self.payto_sig.setVisible(False)
1222 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1226 for h in [self.payto_help, self.amount_help, self.message_help]:
1229 self.payto_help.set_alt(None)
1230 self.set_pay_from([])
1231 self.update_status()
1235 def set_addrs_frozen(self,addrs,freeze):
1237 if not addr: continue
1238 if addr in self.wallet.frozen_addresses and not freeze:
1239 self.wallet.unfreeze(addr)
1240 elif addr not in self.wallet.frozen_addresses and freeze:
1241 self.wallet.freeze(addr)
1242 self.update_address_tab()
1246 def create_list_tab(self, headers):
1247 "generic tab creation method"
1248 l = MyTreeWidget(self)
1249 l.setColumnCount( len(headers) )
1250 l.setHeaderLabels( headers )
1253 vbox = QVBoxLayout()
1260 vbox.addWidget(buttons)
1265 def create_addresses_tab(self):
1266 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1267 for i,width in enumerate(self.column_widths['receive']):
1268 l.setColumnWidth(i, width)
1269 l.setContextMenuPolicy(Qt.CustomContextMenu)
1270 l.customContextMenuRequested.connect(self.create_receive_menu)
1271 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1272 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1273 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1274 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1275 self.address_list = l
1281 def save_column_widths(self):
1282 self.column_widths["receive"] = []
1283 for i in range(self.address_list.columnCount() -1):
1284 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1286 self.column_widths["history"] = []
1287 for i in range(self.history_list.columnCount() - 1):
1288 self.column_widths["history"].append(self.history_list.columnWidth(i))
1290 self.column_widths["contacts"] = []
1291 for i in range(self.contacts_list.columnCount() - 1):
1292 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1294 self.config.set_key("column_widths_2", self.column_widths, True)
1297 def create_contacts_tab(self):
1298 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1299 l.setContextMenuPolicy(Qt.CustomContextMenu)
1300 l.customContextMenuRequested.connect(self.create_contact_menu)
1301 for i,width in enumerate(self.column_widths['contacts']):
1302 l.setColumnWidth(i, width)
1303 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1304 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1305 self.contacts_list = l
1309 def create_invoices_tab(self):
1310 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1311 l.setColumnWidth(0, 150)
1313 h.setStretchLastSection(False)
1314 h.setResizeMode(1, QHeaderView.Stretch)
1315 l.setContextMenuPolicy(Qt.CustomContextMenu)
1316 l.customContextMenuRequested.connect(self.create_invoice_menu)
1317 self.invoices_list = l
1320 def update_invoices_tab(self):
1321 invoices = self.wallet.storage.get('invoices', {})
1322 l = self.invoices_list
1324 for key, value in invoices.items():
1326 domain, memo, amount, expiration_date, status, tx_hash = value
1330 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1332 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1333 l.addTopLevelItem(item)
1335 l.setCurrentItem(l.topLevelItem(0))
1339 def delete_imported_key(self, addr):
1340 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1341 self.wallet.delete_imported_key(addr)
1342 self.update_address_tab()
1343 self.update_history_tab()
1345 def edit_account_label(self, k):
1346 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1348 label = unicode(text)
1349 self.wallet.set_label(k,label)
1350 self.update_address_tab()
1352 def account_set_expanded(self, item, k, b):
1354 self.accounts_expanded[k] = b
1356 def create_account_menu(self, position, k, item):
1358 if item.isExpanded():
1359 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1361 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1362 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1363 if self.wallet.seed_version > 4:
1364 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1365 if self.wallet.account_is_pending(k):
1366 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1367 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1369 def delete_pending_account(self, k):
1370 self.wallet.delete_pending_account(k)
1371 self.update_address_tab()
1373 def create_receive_menu(self, position):
1374 # fixme: this function apparently has a side effect.
1375 # if it is not called the menu pops up several times
1376 #self.address_list.selectedIndexes()
1378 selected = self.address_list.selectedItems()
1379 multi_select = len(selected) > 1
1380 addrs = [unicode(item.text(0)) for item in selected]
1381 if not multi_select:
1382 item = self.address_list.itemAt(position)
1386 if not is_valid(addr):
1387 k = str(item.data(0,32).toString())
1389 self.create_account_menu(position, k, item)
1391 item.setExpanded(not item.isExpanded())
1395 if not multi_select:
1396 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1397 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1398 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1399 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1400 if not self.wallet.is_watching_only():
1401 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1402 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1403 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1404 if self.wallet.is_imported(addr):
1405 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1407 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1408 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1409 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1410 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1413 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1414 if any(can_send(addr) for addr in addrs):
1415 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1417 run_hook('receive_menu', menu, addrs)
1418 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1421 def get_sendable_balance(self):
1422 return sum(map(lambda x:x['value'], self.get_coins()))
1425 def get_coins(self):
1427 return self.pay_from
1429 domain = self.wallet.get_account_addresses(self.current_account)
1430 for i in self.wallet.frozen_addresses:
1431 if i in domain: domain.remove(i)
1432 return self.wallet.get_unspent_coins(domain)
1435 def send_from_addresses(self, addrs):
1436 self.set_pay_from( addrs )
1437 self.tabs.setCurrentIndex(1)
1440 def payto(self, addr):
1442 label = self.wallet.labels.get(addr)
1443 m_addr = label + ' <' + addr + '>' if label else addr
1444 self.tabs.setCurrentIndex(1)
1445 self.payto_e.setText(m_addr)
1446 self.amount_e.setFocus()
1449 def delete_contact(self, x):
1450 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1451 self.wallet.delete_contact(x)
1452 self.wallet.set_label(x, None)
1453 self.update_history_tab()
1454 self.update_contacts_tab()
1455 self.update_completions()
1458 def create_contact_menu(self, position):
1459 item = self.contacts_list.itemAt(position)
1462 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1464 addr = unicode(item.text(0))
1465 label = unicode(item.text(1))
1466 is_editable = item.data(0,32).toBool()
1467 payto_addr = item.data(0,33).toString()
1468 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1469 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1470 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1472 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1473 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1475 run_hook('create_contact_menu', menu, item)
1476 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1478 def delete_invoice(self, key):
1479 self.invoices.pop(key)
1480 self.wallet.storage.put('invoices', self.invoices)
1481 self.update_invoices_tab()
1483 def show_invoice(self, key):
1484 from electrum.paymentrequest import PaymentRequest
1485 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1486 pr = PaymentRequest(self.config)
1490 self.show_pr_details(pr)
1492 def show_pr_details(self, pr):
1493 msg = 'Domain: ' + pr.domain
1494 msg += '\nStatus: ' + pr.get_status()
1495 msg += '\nMemo: ' + pr.get_memo()
1496 msg += '\nPayment URL: ' + pr.payment_url
1497 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1498 QMessageBox.information(self, 'Invoice', msg , 'OK')
1500 def do_pay_invoice(self, key):
1501 from electrum.paymentrequest import PaymentRequest
1502 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1503 pr = PaymentRequest(self.config)
1506 self.payment_request = pr
1507 self.prepare_for_payment_request()
1509 self.payment_request_ok()
1511 self.payment_request_error()
1514 def create_invoice_menu(self, position):
1515 item = self.invoices_list.itemAt(position)
1518 k = self.invoices_list.indexOfTopLevelItem(item)
1519 key = self.invoices.keys()[k]
1520 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1522 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1523 if status == PR_UNPAID:
1524 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1525 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1526 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1530 def update_address_tab(self):
1531 l = self.address_list
1532 # extend the syntax for consistency
1533 l.addChild = l.addTopLevelItem
1534 l.insertChild = l.insertTopLevelItem
1538 accounts = self.wallet.get_accounts()
1539 if self.current_account is None:
1540 account_items = sorted(accounts.items())
1542 account_items = [(self.current_account, accounts.get(self.current_account))]
1545 for k, account in account_items:
1547 if len(accounts) > 1:
1548 name = self.wallet.get_account_name(k)
1549 c,u = self.wallet.get_account_balance(k)
1550 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1551 l.addTopLevelItem(account_item)
1552 account_item.setExpanded(self.accounts_expanded.get(k, True))
1553 account_item.setData(0, 32, k)
1557 sequences = [0,1] if account.has_change() else [0]
1558 for is_change in sequences:
1559 if len(sequences) > 1:
1560 name = _("Receiving") if not is_change else _("Change")
1561 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1562 account_item.addChild(seq_item)
1564 seq_item.setExpanded(True)
1566 seq_item = account_item
1568 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1571 addr_list = account.get_addresses(is_change)
1572 for address in addr_list:
1573 num, is_used = self.wallet.is_used(address)
1574 label = self.wallet.labels.get(address,'')
1575 c, u = self.wallet.get_addr_balance(address)
1576 balance = self.format_amount(c + u)
1577 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1578 item.setFont(0, QFont(MONOSPACE_FONT))
1579 item.setData(0, 32, True) # label can be edited
1580 if address in self.wallet.frozen_addresses:
1581 item.setBackgroundColor(0, QColor('lightblue'))
1582 if self.wallet.is_beyond_limit(address, account, is_change):
1583 item.setBackgroundColor(0, QColor('red'))
1586 seq_item.insertChild(0, used_item)
1588 used_item.addChild(item)
1590 seq_item.addChild(item)
1592 # we use column 1 because column 0 may be hidden
1593 l.setCurrentItem(l.topLevelItem(0),1)
1596 def update_contacts_tab(self):
1597 l = self.contacts_list
1600 for address in self.wallet.addressbook:
1601 label = self.wallet.labels.get(address,'')
1602 n = self.wallet.get_num_tx(address)
1603 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1604 item.setFont(0, QFont(MONOSPACE_FONT))
1605 # 32 = label can be edited (bool)
1606 item.setData(0,32, True)
1608 item.setData(0,33, address)
1609 l.addTopLevelItem(item)
1611 run_hook('update_contacts_tab', l)
1612 l.setCurrentItem(l.topLevelItem(0))
1615 def create_console_tab(self):
1616 from console import Console
1617 self.console = console = Console()
1621 def update_console(self):
1622 console = self.console
1623 console.history = self.config.get("console-history",[])
1624 console.history_index = len(console.history)
1626 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1627 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1629 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1631 def mkfunc(f, method):
1632 return lambda *args: apply( f, (method, args, self.password_dialog ))
1634 if m[0]=='_' or m in ['network','wallet']: continue
1635 methods[m] = mkfunc(c._run, m)
1637 console.updateNamespace(methods)
1640 def change_account(self,s):
1641 if s == _("All accounts"):
1642 self.current_account = None
1644 accounts = self.wallet.get_account_names()
1645 for k, v in accounts.items():
1647 self.current_account = k
1648 self.update_history_tab()
1649 self.update_status()
1650 self.update_address_tab()
1651 self.update_receive_tab()
1653 def create_status_bar(self):
1656 sb.setFixedHeight(35)
1657 qtVersion = qVersion()
1659 self.balance_label = QLabel("")
1660 sb.addWidget(self.balance_label)
1662 from version_getter import UpdateLabel
1663 self.updatelabel = UpdateLabel(self.config, sb)
1665 self.account_selector = QComboBox()
1666 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1667 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1668 sb.addPermanentWidget(self.account_selector)
1670 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1671 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1673 self.lock_icon = QIcon()
1674 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1675 sb.addPermanentWidget( self.password_button )
1677 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1678 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1679 sb.addPermanentWidget( self.seed_button )
1680 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1681 sb.addPermanentWidget( self.status_button )
1683 run_hook('create_status_bar', (sb,))
1685 self.setStatusBar(sb)
1688 def update_lock_icon(self):
1689 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1690 self.password_button.setIcon( icon )
1693 def update_buttons_on_seed(self):
1694 if self.wallet.has_seed():
1695 self.seed_button.show()
1697 self.seed_button.hide()
1699 if not self.wallet.is_watching_only():
1700 self.password_button.show()
1701 self.send_button.setText(_("Send"))
1703 self.password_button.hide()
1704 self.send_button.setText(_("Create unsigned transaction"))
1707 def change_password_dialog(self):
1708 from password_dialog import PasswordDialog
1709 d = PasswordDialog(self.wallet, self)
1711 self.update_lock_icon()
1714 def new_contact_dialog(self):
1717 d.setWindowTitle(_("New Contact"))
1718 vbox = QVBoxLayout(d)
1719 vbox.addWidget(QLabel(_('New Contact')+':'))
1721 grid = QGridLayout()
1724 grid.addWidget(QLabel(_("Address")), 1, 0)
1725 grid.addWidget(line1, 1, 1)
1726 grid.addWidget(QLabel(_("Name")), 2, 0)
1727 grid.addWidget(line2, 2, 1)
1729 vbox.addLayout(grid)
1730 vbox.addLayout(ok_cancel_buttons(d))
1735 address = str(line1.text())
1736 label = unicode(line2.text())
1738 if not is_valid(address):
1739 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1742 self.wallet.add_contact(address)
1744 self.wallet.set_label(address, label)
1746 self.update_contacts_tab()
1747 self.update_history_tab()
1748 self.update_completions()
1749 self.tabs.setCurrentIndex(3)
1753 def new_account_dialog(self, password):
1755 dialog = QDialog(self)
1757 dialog.setWindowTitle(_("New Account"))
1759 vbox = QVBoxLayout()
1760 vbox.addWidget(QLabel(_('Account name')+':'))
1763 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1764 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1769 vbox.addLayout(ok_cancel_buttons(dialog))
1770 dialog.setLayout(vbox)
1774 name = str(e.text())
1777 self.wallet.create_pending_account(name, password)
1778 self.update_address_tab()
1779 self.tabs.setCurrentIndex(3)
1784 def show_master_public_keys(self):
1786 dialog = QDialog(self)
1788 dialog.setWindowTitle(_("Master Public Keys"))
1790 main_layout = QGridLayout()
1791 mpk_dict = self.wallet.get_master_public_keys()
1793 for key, value in mpk_dict.items():
1794 main_layout.addWidget(QLabel(key), i, 0)
1795 mpk_text = QTextEdit()
1796 mpk_text.setReadOnly(True)
1797 mpk_text.setMaximumHeight(170)
1798 mpk_text.setText(value)
1799 main_layout.addWidget(mpk_text, i + 1, 0)
1802 vbox = QVBoxLayout()
1803 vbox.addLayout(main_layout)
1804 vbox.addLayout(close_button(dialog))
1806 dialog.setLayout(vbox)
1811 def show_seed_dialog(self, password):
1812 if not self.wallet.has_seed():
1813 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1817 mnemonic = self.wallet.get_mnemonic(password)
1819 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1821 from seed_dialog import SeedDialog
1822 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1827 def show_qrcode(self, data, title = _("QR code")):
1830 d = QRDialog(data, self, title)
1834 def do_protect(self, func, args):
1835 if self.wallet.use_encryption:
1836 password = self.password_dialog()
1842 if args != (False,):
1843 args = (self,) + args + (password,)
1845 args = (self,password)
1849 def show_public_keys(self, address):
1850 if not address: return
1852 pubkey_list = self.wallet.get_public_keys(address)
1853 except Exception as e:
1854 traceback.print_exc(file=sys.stdout)
1855 self.show_message(str(e))
1859 d.setMinimumSize(600, 200)
1861 vbox = QVBoxLayout()
1862 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1863 vbox.addWidget( QLabel(_("Public key") + ':'))
1865 keys.setReadOnly(True)
1866 keys.setText('\n'.join(pubkey_list))
1867 vbox.addWidget(keys)
1868 vbox.addLayout(close_button(d))
1873 def show_private_key(self, address, password):
1874 if not address: return
1876 pk_list = self.wallet.get_private_key(address, password)
1877 except Exception as e:
1878 traceback.print_exc(file=sys.stdout)
1879 self.show_message(str(e))
1883 d.setMinimumSize(600, 200)
1885 vbox = QVBoxLayout()
1886 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1887 vbox.addWidget( QLabel(_("Private key") + ':'))
1889 keys.setReadOnly(True)
1890 keys.setText('\n'.join(pk_list))
1891 vbox.addWidget(keys)
1892 vbox.addLayout(close_button(d))
1898 def do_sign(self, address, message, signature, password):
1899 message = unicode(message.toPlainText())
1900 message = message.encode('utf-8')
1902 sig = self.wallet.sign_message(str(address.text()), message, password)
1903 signature.setText(sig)
1904 except Exception as e:
1905 self.show_message(str(e))
1907 def do_verify(self, address, message, signature):
1908 message = unicode(message.toPlainText())
1909 message = message.encode('utf-8')
1910 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1911 self.show_message(_("Signature verified"))
1913 self.show_message(_("Error: wrong signature"))
1916 def sign_verify_message(self, address=''):
1919 d.setWindowTitle(_('Sign/verify Message'))
1920 d.setMinimumSize(410, 290)
1922 layout = QGridLayout(d)
1924 message_e = QTextEdit()
1925 layout.addWidget(QLabel(_('Message')), 1, 0)
1926 layout.addWidget(message_e, 1, 1)
1927 layout.setRowStretch(2,3)
1929 address_e = QLineEdit()
1930 address_e.setText(address)
1931 layout.addWidget(QLabel(_('Address')), 2, 0)
1932 layout.addWidget(address_e, 2, 1)
1934 signature_e = QTextEdit()
1935 layout.addWidget(QLabel(_('Signature')), 3, 0)
1936 layout.addWidget(signature_e, 3, 1)
1937 layout.setRowStretch(3,1)
1939 hbox = QHBoxLayout()
1941 b = QPushButton(_("Sign"))
1942 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1945 b = QPushButton(_("Verify"))
1946 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1949 b = QPushButton(_("Close"))
1950 b.clicked.connect(d.accept)
1952 layout.addLayout(hbox, 4, 1)
1957 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1959 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1960 message_e.setText(decrypted)
1961 except Exception as e:
1962 self.show_message(str(e))
1965 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1966 message = unicode(message_e.toPlainText())
1967 message = message.encode('utf-8')
1969 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1970 encrypted_e.setText(encrypted)
1971 except Exception as e:
1972 self.show_message(str(e))
1976 def encrypt_message(self, address = ''):
1979 d.setWindowTitle(_('Encrypt/decrypt Message'))
1980 d.setMinimumSize(610, 490)
1982 layout = QGridLayout(d)
1984 message_e = QTextEdit()
1985 layout.addWidget(QLabel(_('Message')), 1, 0)
1986 layout.addWidget(message_e, 1, 1)
1987 layout.setRowStretch(2,3)
1989 pubkey_e = QLineEdit()
1991 pubkey = self.wallet.get_public_keys(address)[0]
1992 pubkey_e.setText(pubkey)
1993 layout.addWidget(QLabel(_('Public key')), 2, 0)
1994 layout.addWidget(pubkey_e, 2, 1)
1996 encrypted_e = QTextEdit()
1997 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1998 layout.addWidget(encrypted_e, 3, 1)
1999 layout.setRowStretch(3,1)
2001 hbox = QHBoxLayout()
2002 b = QPushButton(_("Encrypt"))
2003 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2006 b = QPushButton(_("Decrypt"))
2007 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2010 b = QPushButton(_("Close"))
2011 b.clicked.connect(d.accept)
2014 layout.addLayout(hbox, 4, 1)
2018 def question(self, msg):
2019 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2021 def show_message(self, msg):
2022 QMessageBox.information(self, _('Message'), msg, _('OK'))
2024 def password_dialog(self, msg=None):
2027 d.setWindowTitle(_("Enter Password"))
2032 vbox = QVBoxLayout()
2034 msg = _('Please enter your password')
2035 vbox.addWidget(QLabel(msg))
2037 grid = QGridLayout()
2039 grid.addWidget(QLabel(_('Password')), 1, 0)
2040 grid.addWidget(pw, 1, 1)
2041 vbox.addLayout(grid)
2043 vbox.addLayout(ok_cancel_buttons(d))
2046 run_hook('password_dialog', pw, grid, 1)
2047 if not d.exec_(): return
2048 return unicode(pw.text())
2057 def tx_from_text(self, txt):
2058 "json or raw hexadecimal"
2067 return Transaction.deserialize(txt)
2069 traceback.print_exc(file=sys.stdout)
2070 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2074 tx_dict = json.loads(str(txt))
2075 assert "hex" in tx_dict.keys()
2076 tx = Transaction.deserialize(tx_dict["hex"])
2077 #if tx_dict.has_key("input_info"):
2078 # input_info = json.loads(tx_dict['input_info'])
2079 # tx.add_input_info(input_info)
2082 traceback.print_exc(file=sys.stdout)
2083 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2087 def read_tx_from_file(self):
2088 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2092 with open(fileName, "r") as f:
2093 file_content = f.read()
2094 except (ValueError, IOError, os.error), reason:
2095 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2097 return self.tx_from_text(file_content)
2101 def sign_raw_transaction(self, tx, password):
2103 self.wallet.signrawtransaction(tx, [], password)
2104 except Exception as e:
2105 traceback.print_exc(file=sys.stdout)
2106 QMessageBox.warning(self, _("Error"), str(e))
2108 def do_process_from_text(self):
2109 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2112 tx = self.tx_from_text(text)
2114 self.show_transaction(tx)
2116 def do_process_from_file(self):
2117 tx = self.read_tx_from_file()
2119 self.show_transaction(tx)
2121 def do_process_from_txid(self):
2122 from electrum import transaction
2123 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2125 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2127 tx = transaction.Transaction.deserialize(r)
2129 self.show_transaction(tx)
2131 self.show_message("unknown transaction")
2133 def do_process_from_csvReader(self, csvReader):
2138 for position, row in enumerate(csvReader):
2140 if not is_address(address):
2141 errors.append((position, address))
2143 amount = Decimal(row[1])
2144 amount = int(100000000*amount)
2145 outputs.append(('address', address, amount))
2146 except (ValueError, IOError, os.error), reason:
2147 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2151 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2152 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2156 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2157 except Exception as e:
2158 self.show_message(str(e))
2161 self.show_transaction(tx)
2163 def do_process_from_csv_file(self):
2164 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2168 with open(fileName, "r") as f:
2169 csvReader = csv.reader(f)
2170 self.do_process_from_csvReader(csvReader)
2171 except (ValueError, IOError, os.error), reason:
2172 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2175 def do_process_from_csv_text(self):
2176 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2177 + _("Format: address, amount. One output per line"), _("Load CSV"))
2180 f = StringIO.StringIO(text)
2181 csvReader = csv.reader(f)
2182 self.do_process_from_csvReader(csvReader)
2187 def export_privkeys_dialog(self, password):
2188 if self.wallet.is_watching_only():
2189 self.show_message(_("This is a watching-only wallet"))
2193 d.setWindowTitle(_('Private keys'))
2194 d.setMinimumSize(850, 300)
2195 vbox = QVBoxLayout(d)
2197 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2198 _("Exposing a single private key can compromise your entire wallet!"),
2199 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2200 vbox.addWidget(QLabel(msg))
2206 defaultname = 'electrum-private-keys.csv'
2207 select_msg = _('Select file to export your private keys to')
2208 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2209 vbox.addLayout(hbox)
2211 h, b = ok_cancel_buttons2(d, _('Export'))
2216 addresses = self.wallet.addresses(True)
2218 def privkeys_thread():
2219 for addr in addresses:
2223 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2224 d.emit(SIGNAL('computing_privkeys'))
2225 d.emit(SIGNAL('show_privkeys'))
2227 def show_privkeys():
2228 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2232 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2233 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2234 threading.Thread(target=privkeys_thread).start()
2240 filename = filename_e.text()
2245 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2246 except (IOError, os.error), reason:
2247 export_error_label = _("Electrum was unable to produce a private key-export.")
2248 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2250 except Exception as e:
2251 self.show_message(str(e))
2254 self.show_message(_("Private keys exported."))
2257 def do_export_privkeys(self, fileName, pklist, is_csv):
2258 with open(fileName, "w+") as f:
2260 transaction = csv.writer(f)
2261 transaction.writerow(["address", "private_key"])
2262 for addr, pk in pklist.items():
2263 transaction.writerow(["%34s"%addr,pk])
2266 f.write(json.dumps(pklist, indent = 4))
2269 def do_import_labels(self):
2270 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2271 if not labelsFile: return
2273 f = open(labelsFile, 'r')
2276 for key, value in json.loads(data).items():
2277 self.wallet.set_label(key, value)
2278 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2279 except (IOError, os.error), reason:
2280 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2283 def do_export_labels(self):
2284 labels = self.wallet.labels
2286 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2288 with open(fileName, 'w+') as f:
2289 json.dump(labels, f)
2290 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2291 except (IOError, os.error), reason:
2292 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2295 def export_history_dialog(self):
2298 d.setWindowTitle(_('Export History'))
2299 d.setMinimumSize(400, 200)
2300 vbox = QVBoxLayout(d)
2302 defaultname = os.path.expanduser('~/electrum-history.csv')
2303 select_msg = _('Select file to export your wallet transactions to')
2305 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2306 vbox.addLayout(hbox)
2310 h, b = ok_cancel_buttons2(d, _('Export'))
2315 filename = filename_e.text()
2320 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2321 except (IOError, os.error), reason:
2322 export_error_label = _("Electrum was unable to produce a transaction export.")
2323 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2326 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2329 def do_export_history(self, wallet, fileName, is_csv):
2330 history = wallet.get_tx_history()
2332 for item in history:
2333 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2335 if timestamp is not None:
2337 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2338 except [RuntimeError, TypeError, NameError] as reason:
2339 time_string = "unknown"
2342 time_string = "unknown"
2344 time_string = "pending"
2346 if value is not None:
2347 value_string = format_satoshis(value, True)
2352 fee_string = format_satoshis(fee, True)
2357 label, is_default_label = wallet.get_label(tx_hash)
2358 label = label.encode('utf-8')
2362 balance_string = format_satoshis(balance, False)
2364 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2366 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2368 with open(fileName, "w+") as f:
2370 transaction = csv.writer(f)
2371 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2373 transaction.writerow(line)
2376 f.write(json.dumps(lines, indent = 4))
2379 def sweep_key_dialog(self):
2381 d.setWindowTitle(_('Sweep private keys'))
2382 d.setMinimumSize(600, 300)
2384 vbox = QVBoxLayout(d)
2385 vbox.addWidget(QLabel(_("Enter private keys")))
2387 keys_e = QTextEdit()
2388 keys_e.setTabChangesFocus(True)
2389 vbox.addWidget(keys_e)
2391 h, address_e = address_field(self.wallet.addresses())
2395 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2396 vbox.addLayout(hbox)
2397 button.setEnabled(False)
2400 addr = str(address_e.text())
2401 if bitcoin.is_address(addr):
2405 pk = str(keys_e.toPlainText()).strip()
2406 if Wallet.is_private_key(pk):
2409 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2410 keys_e.textChanged.connect(f)
2411 address_e.textChanged.connect(f)
2415 fee = self.wallet.fee
2416 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2417 self.show_transaction(tx)
2421 def do_import_privkey(self, password):
2422 if not self.wallet.has_imported_keys():
2423 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2424 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2425 + _('Are you sure you understand what you are doing?'), 3, 4)
2428 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2431 text = str(text).split()
2436 addr = self.wallet.import_key(key, password)
2437 except Exception as e:
2443 addrlist.append(addr)
2445 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2447 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2448 self.update_address_tab()
2449 self.update_history_tab()
2452 def settings_dialog(self):
2454 d.setWindowTitle(_('Electrum Settings'))
2456 vbox = QVBoxLayout()
2457 grid = QGridLayout()
2458 grid.setColumnStretch(0,1)
2460 nz_label = QLabel(_('Display zeros') + ':')
2461 grid.addWidget(nz_label, 0, 0)
2462 nz_e = AmountEdit(None,True)
2463 nz_e.setText("%d"% self.num_zeros)
2464 grid.addWidget(nz_e, 0, 1)
2465 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2466 grid.addWidget(HelpButton(msg), 0, 2)
2467 if not self.config.is_modifiable('num_zeros'):
2468 for w in [nz_e, nz_label]: w.setEnabled(False)
2470 lang_label=QLabel(_('Language') + ':')
2471 grid.addWidget(lang_label, 1, 0)
2472 lang_combo = QComboBox()
2473 from electrum.i18n import languages
2474 lang_combo.addItems(languages.values())
2476 index = languages.keys().index(self.config.get("language",''))
2479 lang_combo.setCurrentIndex(index)
2480 grid.addWidget(lang_combo, 1, 1)
2481 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2482 if not self.config.is_modifiable('language'):
2483 for w in [lang_combo, lang_label]: w.setEnabled(False)
2486 fee_label = QLabel(_('Transaction fee') + ':')
2487 grid.addWidget(fee_label, 2, 0)
2488 fee_e = BTCAmountEdit(self.get_decimal_point)
2489 fee_e.setAmount(self.wallet.fee)
2490 grid.addWidget(fee_e, 2, 1)
2491 msg = _('Fee per kilobyte of transaction.') + '\n' \
2492 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2493 grid.addWidget(HelpButton(msg), 2, 2)
2494 if not self.config.is_modifiable('fee_per_kb'):
2495 for w in [fee_e, fee_label]: w.setEnabled(False)
2497 units = ['BTC', 'mBTC', 'bits']
2498 unit_label = QLabel(_('Base unit') + ':')
2499 grid.addWidget(unit_label, 3, 0)
2500 unit_combo = QComboBox()
2501 unit_combo.addItems(units)
2502 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2503 grid.addWidget(unit_combo, 3, 1)
2504 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2505 + '\n1BTC=1000mBTC.\n' \
2506 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2508 usechange_cb = QCheckBox(_('Use change addresses'))
2509 usechange_cb.setChecked(self.wallet.use_change)
2510 grid.addWidget(usechange_cb, 4, 0)
2511 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2512 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2514 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2515 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2516 grid.addWidget(block_ex_label, 5, 0)
2517 block_ex_combo = QComboBox()
2518 block_ex_combo.addItems(block_explorers)
2519 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2520 grid.addWidget(block_ex_combo, 5, 1)
2521 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2523 show_tx = self.config.get('show_before_broadcast', False)
2524 showtx_cb = QCheckBox(_('Show before broadcast'))
2525 showtx_cb.setChecked(show_tx)
2526 grid.addWidget(showtx_cb, 6, 0)
2527 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2529 vbox.addLayout(grid)
2531 vbox.addLayout(ok_cancel_buttons(d))
2535 if not d.exec_(): return
2537 fee = fee_e.get_amount()
2539 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2542 self.wallet.set_fee(fee)
2544 nz = unicode(nz_e.text())
2549 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2552 if self.num_zeros != nz:
2554 self.config.set_key('num_zeros', nz, True)
2555 self.update_history_tab()
2556 self.update_address_tab()
2558 usechange_result = usechange_cb.isChecked()
2559 if self.wallet.use_change != usechange_result:
2560 self.wallet.use_change = usechange_result
2561 self.wallet.storage.put('use_change', self.wallet.use_change)
2563 if showtx_cb.isChecked() != show_tx:
2564 self.config.set_key('show_before_broadcast', not show_tx)
2566 unit_result = units[unit_combo.currentIndex()]
2567 if self.base_unit() != unit_result:
2568 if unit_result == 'BTC':
2569 self.decimal_point = 8
2570 elif unit_result == 'mBTC':
2571 self.decimal_point = 5
2572 elif unit_result == 'bits':
2573 self.decimal_point = 2
2575 raise Exception('Unknown base unit')
2576 self.config.set_key('decimal_point', self.decimal_point, True)
2577 self.update_history_tab()
2578 self.update_status()
2580 need_restart = False
2582 lang_request = languages.keys()[lang_combo.currentIndex()]
2583 if lang_request != self.config.get('language'):
2584 self.config.set_key("language", lang_request, True)
2587 be_result = block_explorers[block_ex_combo.currentIndex()]
2588 self.config.set_key('block_explorer', be_result, True)
2590 run_hook('close_settings_dialog')
2593 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2596 def run_network_dialog(self):
2597 if not self.network:
2599 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2601 def closeEvent(self, event):
2603 self.config.set_key("is_maximized", self.isMaximized())
2604 if not self.isMaximized():
2606 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2607 self.save_column_widths()
2608 self.config.set_key("console-history", self.console.history[-50:], True)
2609 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2613 def plugins_dialog(self):
2614 from electrum.plugins import plugins
2617 d.setWindowTitle(_('Electrum Plugins'))
2620 vbox = QVBoxLayout(d)
2623 scroll = QScrollArea()
2624 scroll.setEnabled(True)
2625 scroll.setWidgetResizable(True)
2626 scroll.setMinimumSize(400,250)
2627 vbox.addWidget(scroll)
2631 w.setMinimumHeight(len(plugins)*35)
2633 grid = QGridLayout()
2634 grid.setColumnStretch(0,1)
2637 def do_toggle(cb, p, w):
2640 if w: w.setEnabled(r)
2642 def mk_toggle(cb, p, w):
2643 return lambda: do_toggle(cb,p,w)
2645 for i, p in enumerate(plugins):
2647 cb = QCheckBox(p.fullname())
2648 cb.setDisabled(not p.is_available())
2649 cb.setChecked(p.is_enabled())
2650 grid.addWidget(cb, i, 0)
2651 if p.requires_settings():
2652 w = p.settings_widget(self)
2653 w.setEnabled( p.is_enabled() )
2654 grid.addWidget(w, i, 1)
2657 cb.clicked.connect(mk_toggle(cb,p,w))
2658 grid.addWidget(HelpButton(p.description()), i, 2)
2660 print_msg(_("Error: cannot display plugin"), p)
2661 traceback.print_exc(file=sys.stdout)
2662 grid.setRowStretch(i+1,1)
2664 vbox.addLayout(close_button(d))
2669 def show_account_details(self, k):
2670 account = self.wallet.accounts[k]
2673 d.setWindowTitle(_('Account Details'))
2676 vbox = QVBoxLayout(d)
2677 name = self.wallet.get_account_name(k)
2678 label = QLabel('Name: ' + name)
2679 vbox.addWidget(label)
2681 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2683 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2685 vbox.addWidget(QLabel(_('Master Public Key:')))
2688 text.setReadOnly(True)
2689 text.setMaximumHeight(170)
2690 vbox.addWidget(text)
2692 mpk_text = '\n'.join( account.get_master_pubkeys() )
2693 text.setText(mpk_text)
2695 vbox.addLayout(close_button(d))