3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
38 from electrum.wallet import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget, QRDialog
49 from qrtextedit import QRTextEdit
51 from decimal import Decimal
59 if platform.system() == 'Windows':
60 MONOSPACE_FONT = 'Lucida Console'
61 elif platform.system() == 'Darwin':
62 MONOSPACE_FONT = 'Monaco'
64 MONOSPACE_FONT = 'monospace'
68 # status of payment requests
71 PR_SENT = 2 # sent but not propagated
72 PR_PAID = 3 # send and propagated
73 PR_ERROR = 4 # could not parse
76 from electrum import ELECTRUM_VERSION
91 class StatusBarButton(QPushButton):
92 def __init__(self, icon, tooltip, func):
93 QPushButton.__init__(self, icon, '')
94 self.setToolTip(tooltip)
96 self.setMaximumWidth(25)
97 self.clicked.connect(func)
99 self.setIconSize(QSize(25,25))
101 def keyPressEvent(self, e):
102 if e.key() == QtCore.Qt.Key_Return:
114 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
116 class ElectrumWindow(QMainWindow):
120 def __init__(self, config, network, gui_object):
121 QMainWindow.__init__(self)
124 self.network = network
125 self.gui_object = gui_object
126 self.tray = gui_object.tray
127 self.go_lite = gui_object.go_lite
130 self.create_status_bar()
131 self.need_update = threading.Event()
133 self.decimal_point = config.get('decimal_point', 5)
134 self.num_zeros = int(config.get('num_zeros',0))
137 set_language(config.get('language'))
139 self.funds_error = False
140 self.completions = QStringListModel()
142 self.tabs = tabs = QTabWidget(self)
143 self.column_widths = self.config.get("column_widths_2", default_column_widths )
144 tabs.addTab(self.create_history_tab(), _('History') )
145 tabs.addTab(self.create_send_tab(), _('Send') )
146 tabs.addTab(self.create_receive_tab(), _('Receive') )
147 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
148 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
149 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
150 tabs.addTab(self.create_console_tab(), _('Console') )
151 tabs.setMinimumSize(600, 400)
152 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
153 self.setCentralWidget(tabs)
155 g = self.config.get("winpos-qt",[100, 100, 840, 400])
156 self.setGeometry(g[0], g[1], g[2], g[3])
157 if self.config.get("is_maximized"):
160 self.setWindowIcon(QIcon(":icons/electrum.png"))
163 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
164 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
165 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
166 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
167 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
169 for i in range(tabs.count()):
170 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
172 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
173 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
174 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
175 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
176 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
178 self.history_list.setFocus(True)
182 self.network.register_callback('updated', lambda: self.need_update.set())
183 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
184 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
185 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
186 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
188 # set initial message
189 self.console.showMessage(self.network.banner)
192 self.payment_request = None
194 def update_account_selector(self):
196 accounts = self.wallet.get_account_names()
197 self.account_selector.clear()
198 if len(accounts) > 1:
199 self.account_selector.addItems([_("All accounts")] + accounts.values())
200 self.account_selector.setCurrentIndex(0)
201 self.account_selector.show()
203 self.account_selector.hide()
206 def load_wallet(self, wallet):
210 self.update_wallet_format()
212 self.invoices = self.wallet.storage.get('invoices', {})
213 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
214 self.current_account = self.wallet.storage.get("current_account", None)
215 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
216 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
217 self.setWindowTitle( title )
219 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
220 self.notify_transactions()
221 self.update_account_selector()
223 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
224 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
225 self.password_menu.setEnabled(not self.wallet.is_watching_only())
226 self.seed_menu.setEnabled(self.wallet.has_seed())
227 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
228 self.import_menu.setEnabled(self.wallet.can_import())
230 self.update_lock_icon()
231 self.update_buttons_on_seed()
232 self.update_console()
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()
435 run_hook('timer_actions')
437 def format_amount(self, x, is_diff=False, whitespaces=False):
438 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
441 def get_decimal_point(self):
442 return self.decimal_point
446 assert self.decimal_point in [5,8]
447 return "BTC" if self.decimal_point == 8 else "mBTC"
450 def update_status(self):
451 if self.network is None or not self.network.is_running():
453 icon = QIcon(":icons/status_disconnected.png")
455 elif self.network.is_connected():
456 if not self.wallet.up_to_date:
457 text = _("Synchronizing...")
458 icon = QIcon(":icons/status_waiting.png")
459 elif self.network.server_lag > 1:
460 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
461 icon = QIcon(":icons/status_lagging.png")
463 c, u = self.wallet.get_account_balance(self.current_account)
464 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
465 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
467 # append fiat balance and price from exchange rate plugin
469 run_hook('get_fiat_status_text', c+u, r)
474 self.tray.setToolTip(text)
475 icon = QIcon(":icons/status_connected.png")
477 text = _("Not connected")
478 icon = QIcon(":icons/status_disconnected.png")
480 self.balance_label.setText(text)
481 self.status_button.setIcon( icon )
484 def update_wallet(self):
486 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
487 self.update_history_tab()
488 self.update_receive_tab()
489 self.update_address_tab()
490 self.update_contacts_tab()
491 self.update_completions()
492 self.update_invoices_tab()
495 def create_history_tab(self):
496 self.history_list = l = MyTreeWidget(self)
498 for i,width in enumerate(self.column_widths['history']):
499 l.setColumnWidth(i, width)
500 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
501 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
502 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
504 l.customContextMenuRequested.connect(self.create_history_menu)
508 def create_history_menu(self, position):
509 self.history_list.selectedIndexes()
510 item = self.history_list.currentItem()
511 be = self.config.get('block_explorer', 'Blockchain.info')
512 if be == 'Blockchain.info':
513 block_explorer = 'https://blockchain.info/tx/'
514 elif be == 'Blockr.io':
515 block_explorer = 'https://blockr.io/tx/info/'
516 elif be == 'Insight.is':
517 block_explorer = 'http://live.insight.is/tx/'
519 tx_hash = str(item.data(0, Qt.UserRole).toString())
520 if not tx_hash: return
522 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
523 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
524 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
525 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
526 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
529 def show_transaction(self, tx):
530 import transaction_dialog
531 d = transaction_dialog.TxDialog(tx, self)
534 def tx_label_clicked(self, item, column):
535 if column==2 and item.isSelected():
537 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
538 self.history_list.editItem( item, column )
539 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
542 def tx_label_changed(self, item, column):
546 tx_hash = str(item.data(0, Qt.UserRole).toString())
547 tx = self.wallet.transactions.get(tx_hash)
548 text = unicode( item.text(2) )
549 self.wallet.set_label(tx_hash, text)
551 item.setForeground(2, QBrush(QColor('black')))
553 text = self.wallet.get_default_label(tx_hash)
554 item.setText(2, text)
555 item.setForeground(2, QBrush(QColor('gray')))
559 def edit_label(self, is_recv):
560 l = self.receive_list if is_recv else self.contacts_list
561 item = l.currentItem()
562 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
563 l.editItem( item, 1 )
564 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568 def address_label_clicked(self, item, column, l, column_addr, column_label):
569 if column == column_label and item.isSelected():
570 is_editable = item.data(0, 32).toBool()
573 addr = unicode( item.text(column_addr) )
574 label = unicode( item.text(column_label) )
575 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
576 l.editItem( item, column )
577 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
580 def address_label_changed(self, item, column, l, column_addr, column_label):
581 if column == column_label:
582 addr = unicode( item.text(column_addr) )
583 text = unicode( item.text(column_label) )
584 is_editable = item.data(0, 32).toBool()
588 changed = self.wallet.set_label(addr, text)
590 self.update_history_tab()
591 self.update_completions()
593 self.current_item_changed(item)
595 run_hook('item_changed', item, column)
598 def current_item_changed(self, a):
599 run_hook('current_item_changed', a)
603 def update_history_tab(self):
605 self.history_list.clear()
606 for item in self.wallet.get_tx_history(self.current_account):
607 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
608 time_str = _("unknown")
611 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
613 time_str = _("error")
616 time_str = 'unverified'
617 icon = QIcon(":icons/unconfirmed.png")
620 icon = QIcon(":icons/unconfirmed.png")
622 icon = QIcon(":icons/clock%d.png"%conf)
624 icon = QIcon(":icons/confirmed.png")
626 if value is not None:
627 v_str = self.format_amount(value, True, whitespaces=True)
631 balance_str = self.format_amount(balance, whitespaces=True)
634 label, is_default_label = self.wallet.get_label(tx_hash)
636 label = _('Pruned transaction outputs')
637 is_default_label = False
639 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
640 item.setFont(2, QFont(MONOSPACE_FONT))
641 item.setFont(3, QFont(MONOSPACE_FONT))
642 item.setFont(4, QFont(MONOSPACE_FONT))
644 item.setForeground(3, QBrush(QColor("#BC1E1E")))
646 item.setData(0, Qt.UserRole, tx_hash)
647 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
649 item.setForeground(2, QBrush(QColor('grey')))
651 item.setIcon(0, icon)
652 self.history_list.insertTopLevelItem(0,item)
655 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
656 run_hook('history_tab_update')
659 def create_receive_tab(self):
661 grid = QGridLayout(w)
662 grid.setColumnMinimumWidth(2, 300)
663 grid.setColumnStretch(4,1)
664 grid.setRowStretch(4, 1)
666 self.receive_address_e = QLineEdit()
667 self.receive_address_e.setReadOnly(True)
668 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
669 grid.addWidget(self.receive_address_e, 0, 1, 1, 2)
670 self.receive_address_e.textChanged.connect(self.update_receive_qr)
672 self.receive_message_e = QLineEdit()
673 grid.addWidget(QLabel(_('Message')), 1, 0)
674 grid.addWidget(self.receive_message_e, 1, 1, 1, 2)
675 self.receive_message_e.textChanged.connect(self.update_receive_qr)
677 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
678 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
679 grid.addWidget(self.receive_amount_e, 2, 1)
680 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
682 self.receive_qr = QRCodeWidget()
683 grid.addWidget(self.receive_qr, 0, 3, 4, 2)
686 def receive_at(self, addr):
687 if not bitcoin.is_address(addr):
689 self.tabs.setCurrentIndex(2)
690 self.receive_address_e.setText(addr)
692 def update_receive_tab(self):
693 domain = self.wallet.get_account_addresses(self.current_account)
695 self.receive_at(addr)
697 def update_receive_qr(self):
699 addr = str(self.receive_address_e.text())
702 amount = self.receive_amount_e.get_amount()
704 query.append('amount=%s'%format_satoshis(amount))
705 message = str(self.receive_message_e.text())
707 query.append('message=%s'%message)
708 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
709 url = urlparse.urlunparse(p)
712 self.receive_qr.set_addr(url)
713 self.receive_qr.update_qr()
716 def create_send_tab(self):
719 self.send_grid = grid = QGridLayout(w)
721 grid.setColumnMinimumWidth(3,300)
722 grid.setColumnStretch(5,1)
723 grid.setRowStretch(8, 1)
725 from paytoedit import PayToEdit
726 self.amount_e = BTCAmountEdit(self.get_decimal_point)
727 self.payto_e = PayToEdit(self)
728 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)'))
729 grid.addWidget(QLabel(_('Pay to')), 1, 0)
730 grid.addWidget(self.payto_e, 1, 1, 1, 3)
731 grid.addWidget(self.payto_help, 1, 4)
733 completer = QCompleter()
734 completer.setCaseSensitivity(False)
735 self.payto_e.setCompleter(completer)
736 completer.setModel(self.completions)
738 self.message_e = MyLineEdit()
739 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.'))
740 grid.addWidget(QLabel(_('Description')), 2, 0)
741 grid.addWidget(self.message_e, 2, 1, 1, 3)
742 grid.addWidget(self.message_help, 2, 4)
744 self.from_label = QLabel(_('From'))
745 grid.addWidget(self.from_label, 3, 0)
746 self.from_list = MyTreeWidget(self)
747 self.from_list.setColumnCount(2)
748 self.from_list.setColumnWidth(0, 350)
749 self.from_list.setColumnWidth(1, 50)
750 self.from_list.setHeaderHidden(True)
751 self.from_list.setMaximumHeight(80)
752 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
753 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
754 grid.addWidget(self.from_list, 3, 1, 1, 3)
755 self.set_pay_from([])
757 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
758 + _('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.') \
759 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
760 grid.addWidget(QLabel(_('Amount')), 4, 0)
761 grid.addWidget(self.amount_e, 4, 1, 1, 2)
762 grid.addWidget(self.amount_help, 4, 3)
764 self.fee_e = BTCAmountEdit(self.get_decimal_point)
765 grid.addWidget(QLabel(_('Fee')), 5, 0)
766 grid.addWidget(self.fee_e, 5, 1, 1, 2)
767 grid.addWidget(HelpButton(
768 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
769 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
770 + _('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)
772 self.send_button = EnterButton(_("Send"), self.do_send)
773 grid.addWidget(self.send_button, 6, 1)
775 b = EnterButton(_("Clear"), self.do_clear)
776 grid.addWidget(b, 6, 2)
778 self.payto_sig = QLabel('')
779 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
781 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
782 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
785 def entry_changed( is_fee ):
786 self.funds_error = False
788 if self.amount_e.is_shortcut:
789 self.amount_e.is_shortcut = False
790 sendable = self.get_sendable_balance()
791 # there is only one output because we are completely spending inputs
792 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
793 fee = self.wallet.estimated_fee(inputs, 1)
795 self.amount_e.setAmount(amount)
796 self.fee_e.setAmount(fee)
799 amount = self.amount_e.get_amount()
800 fee = self.fee_e.get_amount()
802 if not is_fee: fee = None
805 # assume that there will be 2 outputs (one for change)
806 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
808 self.fee_e.setAmount(fee)
811 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
815 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
816 self.funds_error = True
817 text = _( "Not enough funds" )
818 c, u = self.wallet.get_frozen_balance()
819 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
821 self.statusBar().showMessage(text)
822 self.amount_e.setPalette(palette)
823 self.fee_e.setPalette(palette)
825 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
826 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
828 run_hook('create_send_tab', grid)
831 def from_list_delete(self, item):
832 i = self.from_list.indexOfTopLevelItem(item)
834 self.redraw_from_list()
836 def from_list_menu(self, position):
837 item = self.from_list.itemAt(position)
839 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
840 menu.exec_(self.from_list.viewport().mapToGlobal(position))
842 def set_pay_from(self, domain = None):
843 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
844 self.redraw_from_list()
846 def redraw_from_list(self):
847 self.from_list.clear()
848 self.from_label.setHidden(len(self.pay_from) == 0)
849 self.from_list.setHidden(len(self.pay_from) == 0)
852 h = x.get('prevout_hash')
853 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
855 for item in self.pay_from:
856 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
858 def update_completions(self):
860 for addr,label in self.wallet.labels.items():
861 if addr in self.wallet.addressbook:
862 l.append( label + ' <' + addr + '>')
864 run_hook('update_completions', l)
865 self.completions.setStringList(l)
869 return lambda s, *args: s.do_protect(func, args)
872 def read_send_tab(self):
874 if self.payment_request and self.payment_request.has_expired():
875 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
878 label = unicode( self.message_e.text() )
880 if self.payment_request:
881 outputs = self.payment_request.get_outputs()
883 outputs = self.payto_e.get_outputs()
886 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
889 for addr, x in outputs:
890 if addr is None or not bitcoin.is_address(addr):
891 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
894 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
897 amount = sum(map(lambda x:x[1], outputs))
899 fee = self.fee_e.get_amount()
901 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
904 confirm_amount = self.config.get('confirm_amount', 100000000)
905 if amount >= confirm_amount:
906 o = '\n'.join(map(lambda x:x[0], outputs))
907 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
910 confirm_fee = self.config.get('confirm_fee', 100000)
911 if fee >= confirm_fee:
912 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()}):
915 coins = self.get_coins()
916 return outputs, fee, label, coins
920 r = self.read_send_tab()
923 outputs, fee, label, coins = r
924 self.send_tx(outputs, fee, label, coins)
928 def send_tx(self, outputs, fee, label, coins, password):
929 self.send_button.setDisabled(True)
931 # first, create an unsigned tx
933 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
935 except Exception as e:
936 traceback.print_exc(file=sys.stdout)
937 self.show_message(str(e))
938 self.send_button.setDisabled(False)
941 # call hook to see if plugin needs gui interaction
942 run_hook('send_tx', tx)
948 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
949 self.wallet.sign_transaction(tx, keypairs, password)
950 return tx, fee, label
952 def sign_done(tx, fee, label):
954 self.show_message(tx.error)
955 self.send_button.setDisabled(False)
957 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
958 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
959 self.send_button.setDisabled(False)
962 self.wallet.set_label(tx.hash(), label)
964 if not tx.is_complete() or self.config.get('show_before_broadcast'):
965 self.show_transaction(tx)
967 self.send_button.setDisabled(False)
970 self.broadcast_transaction(tx)
972 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
973 self.waiting_dialog.start()
977 def broadcast_transaction(self, tx):
979 def broadcast_thread():
980 pr = self.payment_request
982 return self.wallet.sendtx(tx)
985 self.payment_request = None
986 return False, _("Payment request has expired")
988 status, msg = self.wallet.sendtx(tx)
992 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
993 self.wallet.storage.put('invoices', self.invoices)
994 self.update_invoices_tab()
995 self.payment_request = None
996 refund_address = self.wallet.addresses()[0]
997 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1003 def broadcast_done(status, msg):
1005 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1008 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1009 self.send_button.setDisabled(False)
1011 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1012 self.waiting_dialog.start()
1016 def prepare_for_payment_request(self):
1017 self.tabs.setCurrentIndex(1)
1018 self.payto_e.is_pr = True
1019 for e in [self.payto_e, self.amount_e, self.message_e]:
1021 for h in [self.payto_help, self.amount_help, self.message_help]:
1023 self.payto_e.setText(_("please wait..."))
1026 def payment_request_ok(self):
1027 pr = self.payment_request
1029 if pr_id not in self.invoices:
1030 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1031 self.wallet.storage.put('invoices', self.invoices)
1032 self.update_invoices_tab()
1034 print_error('invoice already in list')
1036 status = self.invoices[pr_id][4]
1037 if status == PR_PAID:
1039 self.show_message("invoice already paid")
1040 self.payment_request = None
1043 self.payto_help.show()
1044 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1046 if not pr.has_expired():
1047 self.payto_e.setGreen()
1049 self.payto_e.setExpired()
1051 self.payto_e.setText(pr.domain)
1052 self.amount_e.setText(self.format_amount(pr.get_amount()))
1053 self.message_e.setText(pr.get_memo())
1055 def payment_request_error(self):
1057 self.show_message(self.payment_request.error)
1058 self.payment_request = None
1060 def pay_from_URI(self,URI):
1063 address, amount, label, message, request_url = util.parse_URI(URI)
1065 address, amount, label, message, request_url = util.parse_URI(URI)
1066 except Exception as e:
1067 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1070 self.tabs.setCurrentIndex(1)
1074 if self.wallet.labels.get(address) != label:
1075 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1076 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1077 self.wallet.addressbook.append(address)
1078 self.wallet.set_label(address, label)
1080 label = self.wallet.labels.get(address)
1082 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1084 self.message_e.setText(message)
1086 self.amount_e.setAmount(amount)
1089 from electrum import paymentrequest
1090 def payment_request():
1091 self.payment_request = paymentrequest.PaymentRequest(self.config)
1092 self.payment_request.read(request_url)
1093 if self.payment_request.verify():
1094 self.emit(SIGNAL('payment_request_ok'))
1096 self.emit(SIGNAL('payment_request_error'))
1098 self.pr_thread = threading.Thread(target=payment_request).start()
1099 self.prepare_for_payment_request()
1104 self.payto_e.is_pr = False
1105 self.payto_sig.setVisible(False)
1106 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1110 for h in [self.payto_help, self.amount_help, self.message_help]:
1113 self.payto_help.set_alt(None)
1114 self.set_pay_from([])
1115 self.update_status()
1119 def set_addrs_frozen(self,addrs,freeze):
1121 if not addr: continue
1122 if addr in self.wallet.frozen_addresses and not freeze:
1123 self.wallet.unfreeze(addr)
1124 elif addr not in self.wallet.frozen_addresses and freeze:
1125 self.wallet.freeze(addr)
1126 self.update_address_tab()
1130 def create_list_tab(self, headers):
1131 "generic tab creation method"
1132 l = MyTreeWidget(self)
1133 l.setColumnCount( len(headers) )
1134 l.setHeaderLabels( headers )
1137 vbox = QVBoxLayout()
1144 vbox.addWidget(buttons)
1149 def create_addresses_tab(self):
1150 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1151 for i,width in enumerate(self.column_widths['receive']):
1152 l.setColumnWidth(i, width)
1153 l.setContextMenuPolicy(Qt.CustomContextMenu)
1154 l.customContextMenuRequested.connect(self.create_receive_menu)
1155 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1156 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1157 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1158 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1159 self.receive_list = l
1165 def save_column_widths(self):
1166 self.column_widths["receive"] = []
1167 for i in range(self.receive_list.columnCount() -1):
1168 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1170 self.column_widths["history"] = []
1171 for i in range(self.history_list.columnCount() - 1):
1172 self.column_widths["history"].append(self.history_list.columnWidth(i))
1174 self.column_widths["contacts"] = []
1175 for i in range(self.contacts_list.columnCount() - 1):
1176 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1178 self.config.set_key("column_widths_2", self.column_widths, True)
1181 def create_contacts_tab(self):
1182 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1183 l.setContextMenuPolicy(Qt.CustomContextMenu)
1184 l.customContextMenuRequested.connect(self.create_contact_menu)
1185 for i,width in enumerate(self.column_widths['contacts']):
1186 l.setColumnWidth(i, width)
1187 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1188 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1189 self.contacts_list = l
1193 def create_invoices_tab(self):
1194 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1196 h.setStretchLastSection(False)
1197 h.setResizeMode(1, QHeaderView.Stretch)
1198 l.setContextMenuPolicy(Qt.CustomContextMenu)
1199 l.customContextMenuRequested.connect(self.create_invoice_menu)
1200 self.invoices_list = l
1203 def update_invoices_tab(self):
1204 invoices = self.wallet.storage.get('invoices', {})
1205 l = self.invoices_list
1207 for key, value in invoices.items():
1209 domain, memo, amount, expiration_date, status, tx_hash = value
1213 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1215 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1216 l.addTopLevelItem(item)
1218 l.setCurrentItem(l.topLevelItem(0))
1222 def delete_imported_key(self, addr):
1223 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1224 self.wallet.delete_imported_key(addr)
1225 self.update_address_tab()
1226 self.update_history_tab()
1228 def edit_account_label(self, k):
1229 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1231 label = unicode(text)
1232 self.wallet.set_label(k,label)
1233 self.update_address_tab()
1235 def account_set_expanded(self, item, k, b):
1237 self.accounts_expanded[k] = b
1239 def create_account_menu(self, position, k, item):
1241 if item.isExpanded():
1242 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1244 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1245 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1246 if self.wallet.seed_version > 4:
1247 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1248 if self.wallet.account_is_pending(k):
1249 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1250 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1252 def delete_pending_account(self, k):
1253 self.wallet.delete_pending_account(k)
1254 self.update_address_tab()
1256 def create_receive_menu(self, position):
1257 # fixme: this function apparently has a side effect.
1258 # if it is not called the menu pops up several times
1259 #self.receive_list.selectedIndexes()
1261 selected = self.receive_list.selectedItems()
1262 multi_select = len(selected) > 1
1263 addrs = [unicode(item.text(0)) for item in selected]
1264 if not multi_select:
1265 item = self.receive_list.itemAt(position)
1269 if not is_valid(addr):
1270 k = str(item.data(0,32).toString())
1272 self.create_account_menu(position, k, item)
1274 item.setExpanded(not item.isExpanded())
1278 if not multi_select:
1279 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1280 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1281 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1282 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1283 if not self.wallet.is_watching_only():
1284 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1285 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1286 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1287 if self.wallet.is_imported(addr):
1288 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1290 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1291 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1292 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1293 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1295 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1296 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1298 run_hook('receive_menu', menu, addrs)
1299 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1302 def get_sendable_balance(self):
1303 return sum(map(lambda x:x['value'], self.get_coins()))
1306 def get_coins(self):
1308 return self.pay_from
1310 domain = self.wallet.get_account_addresses(self.current_account)
1311 for i in self.wallet.frozen_addresses:
1312 if i in domain: domain.remove(i)
1313 return self.wallet.get_unspent_coins(domain)
1316 def send_from_addresses(self, addrs):
1317 self.set_pay_from( addrs )
1318 self.tabs.setCurrentIndex(1)
1321 def payto(self, addr):
1323 label = self.wallet.labels.get(addr)
1324 m_addr = label + ' <' + addr + '>' if label else addr
1325 self.tabs.setCurrentIndex(1)
1326 self.payto_e.setText(m_addr)
1327 self.amount_e.setFocus()
1330 def delete_contact(self, x):
1331 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1332 self.wallet.delete_contact(x)
1333 self.wallet.set_label(x, None)
1334 self.update_history_tab()
1335 self.update_contacts_tab()
1336 self.update_completions()
1339 def create_contact_menu(self, position):
1340 item = self.contacts_list.itemAt(position)
1343 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1345 addr = unicode(item.text(0))
1346 label = unicode(item.text(1))
1347 is_editable = item.data(0,32).toBool()
1348 payto_addr = item.data(0,33).toString()
1349 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1350 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1351 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1353 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1354 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1356 run_hook('create_contact_menu', menu, item)
1357 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1359 def delete_invoice(self, key):
1360 self.invoices.pop(key)
1361 self.wallet.storage.put('invoices', self.invoices)
1362 self.update_invoices_tab()
1364 def show_invoice(self, key):
1365 from electrum.paymentrequest import PaymentRequest
1366 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1367 pr = PaymentRequest(self.config)
1371 self.show_pr_details(pr)
1373 def show_pr_details(self, pr):
1374 msg = 'Domain: ' + pr.domain
1375 msg += '\nStatus: ' + pr.get_status()
1376 msg += '\nMemo: ' + pr.get_memo()
1377 msg += '\nPayment URL: ' + pr.payment_url
1378 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1379 QMessageBox.information(self, 'Invoice', msg , 'OK')
1381 def do_pay_invoice(self, key):
1382 from electrum.paymentrequest import PaymentRequest
1383 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1384 pr = PaymentRequest(self.config)
1387 self.payment_request = pr
1388 self.prepare_for_payment_request()
1390 self.payment_request_ok()
1392 self.payment_request_error()
1395 def create_invoice_menu(self, position):
1396 item = self.invoices_list.itemAt(position)
1399 k = self.invoices_list.indexOfTopLevelItem(item)
1400 key = self.invoices.keys()[k]
1401 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1403 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1404 if status == PR_UNPAID:
1405 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1406 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1407 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1410 def update_address_item(self, item):
1411 item.setFont(0, QFont(MONOSPACE_FONT))
1412 address = str(item.data(0,0).toString())
1413 label = self.wallet.labels.get(address,'')
1414 item.setData(1,0,label)
1415 item.setData(0,32, True) # is editable
1417 run_hook('update_address_item', address, item)
1419 if not self.wallet.is_mine(address): return
1421 c, u = self.wallet.get_addr_balance(address)
1422 balance = self.format_amount(c + u)
1423 item.setData(2,0,balance)
1425 if address in self.wallet.frozen_addresses:
1426 item.setBackgroundColor(0, QColor('lightblue'))
1429 def update_address_tab(self):
1430 l = self.receive_list
1431 # extend the syntax for consistency
1432 l.addChild = l.addTopLevelItem
1433 l.insertChild = l.insertTopLevelItem
1437 accounts = self.wallet.get_accounts()
1438 if self.current_account is None:
1439 account_items = sorted(accounts.items())
1441 account_items = [(self.current_account, accounts.get(self.current_account))]
1444 for k, account in account_items:
1446 if len(accounts) > 1:
1447 name = self.wallet.get_account_name(k)
1448 c,u = self.wallet.get_account_balance(k)
1449 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1450 l.addTopLevelItem(account_item)
1451 account_item.setExpanded(self.accounts_expanded.get(k, True))
1452 account_item.setData(0, 32, k)
1456 sequences = [0,1] if account.has_change() else [0]
1457 for is_change in sequences:
1458 if len(sequences) > 1:
1459 name = _("Receiving") if not is_change else _("Change")
1460 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1461 account_item.addChild(seq_item)
1463 seq_item.setExpanded(True)
1465 seq_item = account_item
1467 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1473 for address in account.get_addresses(is_change):
1475 num, is_used = self.wallet.is_used(address)
1478 if gap > self.wallet.gap_limit:
1483 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1484 self.update_address_item(item)
1486 item.setBackgroundColor(1, QColor('red'))
1490 seq_item.insertChild(0,used_item)
1492 used_item.addChild(item)
1494 seq_item.addChild(item)
1496 # we use column 1 because column 0 may be hidden
1497 l.setCurrentItem(l.topLevelItem(0),1)
1500 def update_contacts_tab(self):
1501 l = self.contacts_list
1504 for address in self.wallet.addressbook:
1505 label = self.wallet.labels.get(address,'')
1506 n = self.wallet.get_num_tx(address)
1507 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1508 item.setFont(0, QFont(MONOSPACE_FONT))
1509 # 32 = label can be edited (bool)
1510 item.setData(0,32, True)
1512 item.setData(0,33, address)
1513 l.addTopLevelItem(item)
1515 run_hook('update_contacts_tab', l)
1516 l.setCurrentItem(l.topLevelItem(0))
1520 def create_console_tab(self):
1521 from console import Console
1522 self.console = console = Console()
1526 def update_console(self):
1527 console = self.console
1528 console.history = self.config.get("console-history",[])
1529 console.history_index = len(console.history)
1531 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1532 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1534 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1536 def mkfunc(f, method):
1537 return lambda *args: apply( f, (method, args, self.password_dialog ))
1539 if m[0]=='_' or m in ['network','wallet']: continue
1540 methods[m] = mkfunc(c._run, m)
1542 console.updateNamespace(methods)
1545 def change_account(self,s):
1546 if s == _("All accounts"):
1547 self.current_account = None
1549 accounts = self.wallet.get_account_names()
1550 for k, v in accounts.items():
1552 self.current_account = k
1553 self.update_history_tab()
1554 self.update_status()
1555 self.update_address_tab()
1556 self.update_receive_tab()
1558 def create_status_bar(self):
1561 sb.setFixedHeight(35)
1562 qtVersion = qVersion()
1564 self.balance_label = QLabel("")
1565 sb.addWidget(self.balance_label)
1567 from version_getter import UpdateLabel
1568 self.updatelabel = UpdateLabel(self.config, sb)
1570 self.account_selector = QComboBox()
1571 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1572 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1573 sb.addPermanentWidget(self.account_selector)
1575 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1576 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1578 self.lock_icon = QIcon()
1579 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1580 sb.addPermanentWidget( self.password_button )
1582 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1583 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1584 sb.addPermanentWidget( self.seed_button )
1585 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1586 sb.addPermanentWidget( self.status_button )
1588 run_hook('create_status_bar', (sb,))
1590 self.setStatusBar(sb)
1593 def update_lock_icon(self):
1594 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1595 self.password_button.setIcon( icon )
1598 def update_buttons_on_seed(self):
1599 if self.wallet.has_seed():
1600 self.seed_button.show()
1602 self.seed_button.hide()
1604 if not self.wallet.is_watching_only():
1605 self.password_button.show()
1606 self.send_button.setText(_("Send"))
1608 self.password_button.hide()
1609 self.send_button.setText(_("Create unsigned transaction"))
1612 def change_password_dialog(self):
1613 from password_dialog import PasswordDialog
1614 d = PasswordDialog(self.wallet, self)
1616 self.update_lock_icon()
1619 def new_contact_dialog(self):
1622 d.setWindowTitle(_("New Contact"))
1623 vbox = QVBoxLayout(d)
1624 vbox.addWidget(QLabel(_('New Contact')+':'))
1626 grid = QGridLayout()
1629 grid.addWidget(QLabel(_("Address")), 1, 0)
1630 grid.addWidget(line1, 1, 1)
1631 grid.addWidget(QLabel(_("Name")), 2, 0)
1632 grid.addWidget(line2, 2, 1)
1634 vbox.addLayout(grid)
1635 vbox.addLayout(ok_cancel_buttons(d))
1640 address = str(line1.text())
1641 label = unicode(line2.text())
1643 if not is_valid(address):
1644 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1647 self.wallet.add_contact(address)
1649 self.wallet.set_label(address, label)
1651 self.update_contacts_tab()
1652 self.update_history_tab()
1653 self.update_completions()
1654 self.tabs.setCurrentIndex(3)
1658 def new_account_dialog(self, password):
1660 dialog = QDialog(self)
1662 dialog.setWindowTitle(_("New Account"))
1664 vbox = QVBoxLayout()
1665 vbox.addWidget(QLabel(_('Account name')+':'))
1668 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1669 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1674 vbox.addLayout(ok_cancel_buttons(dialog))
1675 dialog.setLayout(vbox)
1679 name = str(e.text())
1682 self.wallet.create_pending_account(name, password)
1683 self.update_address_tab()
1684 self.tabs.setCurrentIndex(2)
1689 def show_master_public_keys(self):
1691 dialog = QDialog(self)
1693 dialog.setWindowTitle(_("Master Public Keys"))
1695 main_layout = QGridLayout()
1696 mpk_dict = self.wallet.get_master_public_keys()
1698 for key, value in mpk_dict.items():
1699 main_layout.addWidget(QLabel(key), i, 0)
1700 mpk_text = QTextEdit()
1701 mpk_text.setReadOnly(True)
1702 mpk_text.setMaximumHeight(170)
1703 mpk_text.setText(value)
1704 main_layout.addWidget(mpk_text, i + 1, 0)
1707 vbox = QVBoxLayout()
1708 vbox.addLayout(main_layout)
1709 vbox.addLayout(close_button(dialog))
1711 dialog.setLayout(vbox)
1716 def show_seed_dialog(self, password):
1717 if not self.wallet.has_seed():
1718 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1722 mnemonic = self.wallet.get_mnemonic(password)
1724 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1726 from seed_dialog import SeedDialog
1727 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1732 def show_qrcode(self, data, title = _("QR code")):
1735 d = QRDialog(data, self, title)
1739 def do_protect(self, func, args):
1740 if self.wallet.use_encryption:
1741 password = self.password_dialog()
1747 if args != (False,):
1748 args = (self,) + args + (password,)
1750 args = (self,password)
1754 def show_public_keys(self, address):
1755 if not address: return
1757 pubkey_list = self.wallet.get_public_keys(address)
1758 except Exception as e:
1759 traceback.print_exc(file=sys.stdout)
1760 self.show_message(str(e))
1764 d.setMinimumSize(600, 200)
1766 vbox = QVBoxLayout()
1767 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1768 vbox.addWidget( QLabel(_("Public key") + ':'))
1770 keys.setReadOnly(True)
1771 keys.setText('\n'.join(pubkey_list))
1772 vbox.addWidget(keys)
1773 vbox.addLayout(close_button(d))
1778 def show_private_key(self, address, password):
1779 if not address: return
1781 pk_list = self.wallet.get_private_key(address, password)
1782 except Exception as e:
1783 traceback.print_exc(file=sys.stdout)
1784 self.show_message(str(e))
1788 d.setMinimumSize(600, 200)
1790 vbox = QVBoxLayout()
1791 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1792 vbox.addWidget( QLabel(_("Private key") + ':'))
1794 keys.setReadOnly(True)
1795 keys.setText('\n'.join(pk_list))
1796 vbox.addWidget(keys)
1797 vbox.addLayout(close_button(d))
1803 def do_sign(self, address, message, signature, password):
1804 message = unicode(message.toPlainText())
1805 message = message.encode('utf-8')
1807 sig = self.wallet.sign_message(str(address.text()), message, password)
1808 signature.setText(sig)
1809 except Exception as e:
1810 self.show_message(str(e))
1812 def do_verify(self, address, message, signature):
1813 message = unicode(message.toPlainText())
1814 message = message.encode('utf-8')
1815 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1816 self.show_message(_("Signature verified"))
1818 self.show_message(_("Error: wrong signature"))
1821 def sign_verify_message(self, address=''):
1824 d.setWindowTitle(_('Sign/verify Message'))
1825 d.setMinimumSize(410, 290)
1827 layout = QGridLayout(d)
1829 message_e = QTextEdit()
1830 layout.addWidget(QLabel(_('Message')), 1, 0)
1831 layout.addWidget(message_e, 1, 1)
1832 layout.setRowStretch(2,3)
1834 address_e = QLineEdit()
1835 address_e.setText(address)
1836 layout.addWidget(QLabel(_('Address')), 2, 0)
1837 layout.addWidget(address_e, 2, 1)
1839 signature_e = QTextEdit()
1840 layout.addWidget(QLabel(_('Signature')), 3, 0)
1841 layout.addWidget(signature_e, 3, 1)
1842 layout.setRowStretch(3,1)
1844 hbox = QHBoxLayout()
1846 b = QPushButton(_("Sign"))
1847 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1850 b = QPushButton(_("Verify"))
1851 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1854 b = QPushButton(_("Close"))
1855 b.clicked.connect(d.accept)
1857 layout.addLayout(hbox, 4, 1)
1862 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1864 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1865 message_e.setText(decrypted)
1866 except Exception as e:
1867 self.show_message(str(e))
1870 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1871 message = unicode(message_e.toPlainText())
1872 message = message.encode('utf-8')
1874 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1875 encrypted_e.setText(encrypted)
1876 except Exception as e:
1877 self.show_message(str(e))
1881 def encrypt_message(self, address = ''):
1884 d.setWindowTitle(_('Encrypt/decrypt Message'))
1885 d.setMinimumSize(610, 490)
1887 layout = QGridLayout(d)
1889 message_e = QTextEdit()
1890 layout.addWidget(QLabel(_('Message')), 1, 0)
1891 layout.addWidget(message_e, 1, 1)
1892 layout.setRowStretch(2,3)
1894 pubkey_e = QLineEdit()
1896 pubkey = self.wallet.getpubkeys(address)[0]
1897 pubkey_e.setText(pubkey)
1898 layout.addWidget(QLabel(_('Public key')), 2, 0)
1899 layout.addWidget(pubkey_e, 2, 1)
1901 encrypted_e = QTextEdit()
1902 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1903 layout.addWidget(encrypted_e, 3, 1)
1904 layout.setRowStretch(3,1)
1906 hbox = QHBoxLayout()
1907 b = QPushButton(_("Encrypt"))
1908 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1911 b = QPushButton(_("Decrypt"))
1912 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1915 b = QPushButton(_("Close"))
1916 b.clicked.connect(d.accept)
1919 layout.addLayout(hbox, 4, 1)
1923 def question(self, msg):
1924 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1926 def show_message(self, msg):
1927 QMessageBox.information(self, _('Message'), msg, _('OK'))
1929 def password_dialog(self, msg=None):
1932 d.setWindowTitle(_("Enter Password"))
1937 vbox = QVBoxLayout()
1939 msg = _('Please enter your password')
1940 vbox.addWidget(QLabel(msg))
1942 grid = QGridLayout()
1944 grid.addWidget(QLabel(_('Password')), 1, 0)
1945 grid.addWidget(pw, 1, 1)
1946 vbox.addLayout(grid)
1948 vbox.addLayout(ok_cancel_buttons(d))
1951 run_hook('password_dialog', pw, grid, 1)
1952 if not d.exec_(): return
1953 return unicode(pw.text())
1962 def tx_from_text(self, txt):
1963 "json or raw hexadecimal"
1966 tx = Transaction(txt)
1972 tx_dict = json.loads(str(txt))
1973 assert "hex" in tx_dict.keys()
1974 tx = Transaction(tx_dict["hex"])
1975 if tx_dict.has_key("input_info"):
1976 input_info = json.loads(tx_dict['input_info'])
1977 tx.add_input_info(input_info)
1980 traceback.print_exc(file=sys.stdout)
1983 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1987 def read_tx_from_file(self):
1988 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1992 with open(fileName, "r") as f:
1993 file_content = f.read()
1994 except (ValueError, IOError, os.error), reason:
1995 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1997 return self.tx_from_text(file_content)
2001 def sign_raw_transaction(self, tx, input_info, password):
2002 self.wallet.signrawtransaction(tx, input_info, [], password)
2004 def do_process_from_text(self):
2005 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2008 tx = self.tx_from_text(text)
2010 self.show_transaction(tx)
2012 def do_process_from_file(self):
2013 tx = self.read_tx_from_file()
2015 self.show_transaction(tx)
2017 def do_process_from_txid(self):
2018 from electrum import transaction
2019 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2021 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2023 tx = transaction.Transaction(r)
2025 self.show_transaction(tx)
2027 self.show_message("unknown transaction")
2029 def do_process_from_csvReader(self, csvReader):
2034 for position, row in enumerate(csvReader):
2036 if not is_valid(address):
2037 errors.append((position, address))
2039 amount = Decimal(row[1])
2040 amount = int(100000000*amount)
2041 outputs.append((address, amount))
2042 except (ValueError, IOError, os.error), reason:
2043 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2047 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2048 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2052 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2053 except Exception as e:
2054 self.show_message(str(e))
2057 self.show_transaction(tx)
2059 def do_process_from_csv_file(self):
2060 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2064 with open(fileName, "r") as f:
2065 csvReader = csv.reader(f)
2066 self.do_process_from_csvReader(csvReader)
2067 except (ValueError, IOError, os.error), reason:
2068 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2071 def do_process_from_csv_text(self):
2072 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2073 + _("Format: address, amount. One output per line"), _("Load CSV"))
2076 f = StringIO.StringIO(text)
2077 csvReader = csv.reader(f)
2078 self.do_process_from_csvReader(csvReader)
2083 def export_privkeys_dialog(self, password):
2084 if self.wallet.is_watching_only():
2085 self.show_message(_("This is a watching-only wallet"))
2089 d.setWindowTitle(_('Private keys'))
2090 d.setMinimumSize(850, 300)
2091 vbox = QVBoxLayout(d)
2093 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2094 _("Exposing a single private key can compromise your entire wallet!"),
2095 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2096 vbox.addWidget(QLabel(msg))
2102 defaultname = 'electrum-private-keys.csv'
2103 select_msg = _('Select file to export your private keys to')
2104 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2105 vbox.addLayout(hbox)
2107 h, b = ok_cancel_buttons2(d, _('Export'))
2112 addresses = self.wallet.addresses(True)
2114 def privkeys_thread():
2115 for addr in addresses:
2119 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2120 d.emit(SIGNAL('computing_privkeys'))
2121 d.emit(SIGNAL('show_privkeys'))
2123 def show_privkeys():
2124 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2128 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2129 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2130 threading.Thread(target=privkeys_thread).start()
2136 filename = filename_e.text()
2141 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2142 except (IOError, os.error), reason:
2143 export_error_label = _("Electrum was unable to produce a private key-export.")
2144 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2146 except Exception as e:
2147 self.show_message(str(e))
2150 self.show_message(_("Private keys exported."))
2153 def do_export_privkeys(self, fileName, pklist, is_csv):
2154 with open(fileName, "w+") as f:
2156 transaction = csv.writer(f)
2157 transaction.writerow(["address", "private_key"])
2158 for addr, pk in pklist.items():
2159 transaction.writerow(["%34s"%addr,pk])
2162 f.write(json.dumps(pklist, indent = 4))
2165 def do_import_labels(self):
2166 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2167 if not labelsFile: return
2169 f = open(labelsFile, 'r')
2172 for key, value in json.loads(data).items():
2173 self.wallet.set_label(key, value)
2174 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2175 except (IOError, os.error), reason:
2176 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2179 def do_export_labels(self):
2180 labels = self.wallet.labels
2182 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2184 with open(fileName, 'w+') as f:
2185 json.dump(labels, f)
2186 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2187 except (IOError, os.error), reason:
2188 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2191 def export_history_dialog(self):
2194 d.setWindowTitle(_('Export History'))
2195 d.setMinimumSize(400, 200)
2196 vbox = QVBoxLayout(d)
2198 defaultname = os.path.expanduser('~/electrum-history.csv')
2199 select_msg = _('Select file to export your wallet transactions to')
2201 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2202 vbox.addLayout(hbox)
2206 h, b = ok_cancel_buttons2(d, _('Export'))
2211 filename = filename_e.text()
2216 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2217 except (IOError, os.error), reason:
2218 export_error_label = _("Electrum was unable to produce a transaction export.")
2219 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2222 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2225 def do_export_history(self, wallet, fileName, is_csv):
2226 history = wallet.get_tx_history()
2228 for item in history:
2229 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2231 if timestamp is not None:
2233 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2234 except [RuntimeError, TypeError, NameError] as reason:
2235 time_string = "unknown"
2238 time_string = "unknown"
2240 time_string = "pending"
2242 if value is not None:
2243 value_string = format_satoshis(value, True)
2248 fee_string = format_satoshis(fee, True)
2253 label, is_default_label = wallet.get_label(tx_hash)
2254 label = label.encode('utf-8')
2258 balance_string = format_satoshis(balance, False)
2260 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2262 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2264 with open(fileName, "w+") as f:
2266 transaction = csv.writer(f)
2267 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2269 transaction.writerow(line)
2272 f.write(json.dumps(lines, indent = 4))
2275 def sweep_key_dialog(self):
2277 d.setWindowTitle(_('Sweep private keys'))
2278 d.setMinimumSize(600, 300)
2280 vbox = QVBoxLayout(d)
2281 vbox.addWidget(QLabel(_("Enter private keys")))
2283 keys_e = QTextEdit()
2284 keys_e.setTabChangesFocus(True)
2285 vbox.addWidget(keys_e)
2287 h, address_e = address_field(self.wallet.addresses())
2291 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2292 vbox.addLayout(hbox)
2293 button.setEnabled(False)
2296 addr = str(address_e.text())
2297 if bitcoin.is_address(addr):
2301 pk = str(keys_e.toPlainText()).strip()
2302 if Wallet.is_private_key(pk):
2305 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2306 keys_e.textChanged.connect(f)
2307 address_e.textChanged.connect(f)
2311 fee = self.wallet.fee
2312 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2313 self.show_transaction(tx)
2317 def do_import_privkey(self, password):
2318 if not self.wallet.has_imported_keys():
2319 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2320 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2321 + _('Are you sure you understand what you are doing?'), 3, 4)
2324 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2327 text = str(text).split()
2332 addr = self.wallet.import_key(key, password)
2333 except Exception as e:
2339 addrlist.append(addr)
2341 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2343 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2344 self.update_address_tab()
2345 self.update_history_tab()
2348 def settings_dialog(self):
2350 d.setWindowTitle(_('Electrum Settings'))
2352 vbox = QVBoxLayout()
2353 grid = QGridLayout()
2354 grid.setColumnStretch(0,1)
2356 nz_label = QLabel(_('Display zeros') + ':')
2357 grid.addWidget(nz_label, 0, 0)
2358 nz_e = AmountEdit(None,True)
2359 nz_e.setText("%d"% self.num_zeros)
2360 grid.addWidget(nz_e, 0, 1)
2361 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2362 grid.addWidget(HelpButton(msg), 0, 2)
2363 if not self.config.is_modifiable('num_zeros'):
2364 for w in [nz_e, nz_label]: w.setEnabled(False)
2366 lang_label=QLabel(_('Language') + ':')
2367 grid.addWidget(lang_label, 1, 0)
2368 lang_combo = QComboBox()
2369 from electrum.i18n import languages
2370 lang_combo.addItems(languages.values())
2372 index = languages.keys().index(self.config.get("language",''))
2375 lang_combo.setCurrentIndex(index)
2376 grid.addWidget(lang_combo, 1, 1)
2377 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2378 if not self.config.is_modifiable('language'):
2379 for w in [lang_combo, lang_label]: w.setEnabled(False)
2382 fee_label = QLabel(_('Transaction fee') + ':')
2383 grid.addWidget(fee_label, 2, 0)
2384 fee_e = BTCAmountEdit(self.get_decimal_point)
2385 fee_e.setAmount(self.wallet.fee)
2386 grid.addWidget(fee_e, 2, 1)
2387 msg = _('Fee per kilobyte of transaction.') + '\n' \
2388 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2389 grid.addWidget(HelpButton(msg), 2, 2)
2390 if not self.config.is_modifiable('fee_per_kb'):
2391 for w in [fee_e, fee_label]: w.setEnabled(False)
2393 units = ['BTC', 'mBTC']
2394 unit_label = QLabel(_('Base unit') + ':')
2395 grid.addWidget(unit_label, 3, 0)
2396 unit_combo = QComboBox()
2397 unit_combo.addItems(units)
2398 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2399 grid.addWidget(unit_combo, 3, 1)
2400 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2401 + '\n1BTC=1000mBTC.\n' \
2402 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2404 usechange_cb = QCheckBox(_('Use change addresses'))
2405 usechange_cb.setChecked(self.wallet.use_change)
2406 grid.addWidget(usechange_cb, 4, 0)
2407 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2408 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2410 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2411 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2412 grid.addWidget(block_ex_label, 5, 0)
2413 block_ex_combo = QComboBox()
2414 block_ex_combo.addItems(block_explorers)
2415 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2416 grid.addWidget(block_ex_combo, 5, 1)
2417 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2419 show_tx = self.config.get('show_before_broadcast', False)
2420 showtx_cb = QCheckBox(_('Show before broadcast'))
2421 showtx_cb.setChecked(show_tx)
2422 grid.addWidget(showtx_cb, 6, 0)
2423 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2425 vbox.addLayout(grid)
2427 vbox.addLayout(ok_cancel_buttons(d))
2431 if not d.exec_(): return
2433 fee = fee_e.get_amount()
2435 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2438 self.wallet.set_fee(fee)
2440 nz = unicode(nz_e.text())
2445 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2448 if self.num_zeros != nz:
2450 self.config.set_key('num_zeros', nz, True)
2451 self.update_history_tab()
2452 self.update_address_tab()
2454 usechange_result = usechange_cb.isChecked()
2455 if self.wallet.use_change != usechange_result:
2456 self.wallet.use_change = usechange_result
2457 self.wallet.storage.put('use_change', self.wallet.use_change)
2459 if showtx_cb.isChecked() != show_tx:
2460 self.config.set_key('show_before_broadcast', not show_tx)
2462 unit_result = units[unit_combo.currentIndex()]
2463 if self.base_unit() != unit_result:
2464 self.decimal_point = 8 if unit_result == 'BTC' else 5
2465 self.config.set_key('decimal_point', self.decimal_point, True)
2466 self.update_history_tab()
2467 self.update_status()
2469 need_restart = False
2471 lang_request = languages.keys()[lang_combo.currentIndex()]
2472 if lang_request != self.config.get('language'):
2473 self.config.set_key("language", lang_request, True)
2476 be_result = block_explorers[block_ex_combo.currentIndex()]
2477 self.config.set_key('block_explorer', be_result, True)
2479 run_hook('close_settings_dialog')
2482 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2485 def run_network_dialog(self):
2486 if not self.network:
2488 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2490 def closeEvent(self, event):
2492 self.config.set_key("is_maximized", self.isMaximized())
2493 if not self.isMaximized():
2495 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2496 self.save_column_widths()
2497 self.config.set_key("console-history", self.console.history[-50:], True)
2498 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2502 def plugins_dialog(self):
2503 from electrum.plugins import plugins
2506 d.setWindowTitle(_('Electrum Plugins'))
2509 vbox = QVBoxLayout(d)
2512 scroll = QScrollArea()
2513 scroll.setEnabled(True)
2514 scroll.setWidgetResizable(True)
2515 scroll.setMinimumSize(400,250)
2516 vbox.addWidget(scroll)
2520 w.setMinimumHeight(len(plugins)*35)
2522 grid = QGridLayout()
2523 grid.setColumnStretch(0,1)
2526 def do_toggle(cb, p, w):
2529 if w: w.setEnabled(r)
2531 def mk_toggle(cb, p, w):
2532 return lambda: do_toggle(cb,p,w)
2534 for i, p in enumerate(plugins):
2536 cb = QCheckBox(p.fullname())
2537 cb.setDisabled(not p.is_available())
2538 cb.setChecked(p.is_enabled())
2539 grid.addWidget(cb, i, 0)
2540 if p.requires_settings():
2541 w = p.settings_widget(self)
2542 w.setEnabled( p.is_enabled() )
2543 grid.addWidget(w, i, 1)
2546 cb.clicked.connect(mk_toggle(cb,p,w))
2547 grid.addWidget(HelpButton(p.description()), i, 2)
2549 print_msg(_("Error: cannot display plugin"), p)
2550 traceback.print_exc(file=sys.stdout)
2551 grid.setRowStretch(i+1,1)
2553 vbox.addLayout(close_button(d))
2558 def show_account_details(self, k):
2559 account = self.wallet.accounts[k]
2562 d.setWindowTitle(_('Account Details'))
2565 vbox = QVBoxLayout(d)
2566 name = self.wallet.get_account_name(k)
2567 label = QLabel('Name: ' + name)
2568 vbox.addWidget(label)
2570 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2572 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2574 vbox.addWidget(QLabel(_('Master Public Key:')))
2577 text.setReadOnly(True)
2578 text.setMaximumHeight(170)
2579 vbox.addWidget(text)
2581 mpk_text = '\n'.join( account.get_master_pubkeys() )
2582 text.setText(mpk_text)
2584 vbox.addLayout(close_button(d))