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.paymentrequest import PR_UNPAID, PR_PAID
47 from electrum import bmp, pyqrnative
49 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
50 from network_dialog import NetworkDialog
51 from qrcodewidget import QRCodeWidget
53 from decimal import Decimal
61 if platform.system() == 'Windows':
62 MONOSPACE_FONT = 'Lucida Console'
63 elif platform.system() == 'Darwin':
64 MONOSPACE_FONT = 'Monaco'
66 MONOSPACE_FONT = 'monospace'
68 from electrum import ELECTRUM_VERSION
81 class StatusBarButton(QPushButton):
82 def __init__(self, icon, tooltip, func):
83 QPushButton.__init__(self, icon, '')
84 self.setToolTip(tooltip)
86 self.setMaximumWidth(25)
87 self.clicked.connect(func)
89 self.setIconSize(QSize(25,25))
91 def keyPressEvent(self, e):
92 if e.key() == QtCore.Qt.Key_Return:
104 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
106 class ElectrumWindow(QMainWindow):
110 def __init__(self, config, network, gui_object):
111 QMainWindow.__init__(self)
114 self.network = network
115 self.gui_object = gui_object
116 self.tray = gui_object.tray
117 self.go_lite = gui_object.go_lite
120 self.create_status_bar()
121 self.need_update = threading.Event()
123 self.decimal_point = config.get('decimal_point', 5)
124 self.num_zeros = int(config.get('num_zeros',0))
127 set_language(config.get('language'))
129 self.funds_error = False
130 self.completions = QStringListModel()
132 self.tabs = tabs = QTabWidget(self)
133 self.column_widths = self.config.get("column_widths_2", default_column_widths )
134 tabs.addTab(self.create_history_tab(), _('History') )
135 tabs.addTab(self.create_send_tab(), _('Send') )
136 tabs.addTab(self.create_receive_tab(), _('Receive') )
137 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
138 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
139 tabs.addTab(self.create_console_tab(), _('Console') )
140 tabs.setMinimumSize(600, 400)
141 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
142 self.setCentralWidget(tabs)
144 g = self.config.get("winpos-qt",[100, 100, 840, 400])
145 self.setGeometry(g[0], g[1], g[2], g[3])
146 if self.config.get("is_maximized"):
149 self.setWindowIcon(QIcon(":icons/electrum.png"))
152 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
153 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
154 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
155 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
156 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
158 for i in range(tabs.count()):
159 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
161 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
162 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
163 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
164 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
165 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
167 self.history_list.setFocus(True)
171 self.network.register_callback('updated', lambda: self.need_update.set())
172 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
173 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
174 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
175 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
177 # set initial message
178 self.console.showMessage(self.network.banner)
183 def update_account_selector(self):
185 accounts = self.wallet.get_account_names()
186 self.account_selector.clear()
187 if len(accounts) > 1:
188 self.account_selector.addItems([_("All accounts")] + accounts.values())
189 self.account_selector.setCurrentIndex(0)
190 self.account_selector.show()
192 self.account_selector.hide()
195 def load_wallet(self, wallet):
199 self.update_wallet_format()
201 self.invoices = self.wallet.storage.get('invoices', {})
202 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
203 self.current_account = self.wallet.storage.get("current_account", None)
204 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
205 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
206 self.setWindowTitle( title )
208 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
209 self.notify_transactions()
210 self.update_account_selector()
212 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
213 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
214 self.password_menu.setEnabled(not self.wallet.is_watching_only())
215 self.seed_menu.setEnabled(self.wallet.has_seed())
216 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
217 self.import_menu.setEnabled(self.wallet.can_import())
219 self.update_lock_icon()
220 self.update_buttons_on_seed()
221 self.update_console()
223 run_hook('load_wallet', wallet)
226 def update_wallet_format(self):
227 # convert old-format imported keys
228 if self.wallet.imported_keys:
229 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
231 self.wallet.convert_imported_keys(password)
233 self.show_message("error")
236 def open_wallet(self):
237 wallet_folder = self.wallet.storage.path
238 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
242 storage = WalletStorage({'wallet_path': filename})
243 if not storage.file_exists:
244 self.show_message("file not found "+ filename)
247 self.wallet.stop_threads()
250 wallet = Wallet(storage)
251 wallet.start_threads(self.network)
253 self.load_wallet(wallet)
257 def backup_wallet(self):
259 path = self.wallet.storage.path
260 wallet_folder = os.path.dirname(path)
261 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
265 new_path = os.path.join(wallet_folder, filename)
268 shutil.copy2(path, new_path)
269 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
270 except (IOError, os.error), reason:
271 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
274 def new_wallet(self):
277 wallet_folder = os.path.dirname(self.wallet.storage.path)
278 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
281 filename = os.path.join(wallet_folder, filename)
283 storage = WalletStorage({'wallet_path': filename})
284 if storage.file_exists:
285 QMessageBox.critical(None, "Error", _("File exists"))
288 wizard = installwizard.InstallWizard(self.config, self.network, storage)
289 wallet = wizard.run('new')
291 self.load_wallet(wallet)
295 def init_menubar(self):
298 file_menu = menubar.addMenu(_("&File"))
299 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
300 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
301 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
302 file_menu.addAction(_("&Quit"), self.close)
304 wallet_menu = menubar.addMenu(_("&Wallet"))
305 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
306 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
308 wallet_menu.addSeparator()
310 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
311 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
312 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
314 wallet_menu.addSeparator()
315 labels_menu = wallet_menu.addMenu(_("&Labels"))
316 labels_menu.addAction(_("&Import"), self.do_import_labels)
317 labels_menu.addAction(_("&Export"), self.do_export_labels)
319 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
320 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
321 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
322 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
323 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
325 tools_menu = menubar.addMenu(_("&Tools"))
327 # Settings / Preferences are all reserved keywords in OSX using this as work around
328 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
329 tools_menu.addAction(_("&Network"), self.run_network_dialog)
330 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
331 tools_menu.addSeparator()
332 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
333 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
334 tools_menu.addSeparator()
336 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
337 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
338 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
340 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
341 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
342 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
343 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
345 help_menu = menubar.addMenu(_("&Help"))
346 help_menu.addAction(_("&About"), self.show_about)
347 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
348 help_menu.addSeparator()
349 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
350 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
352 self.setMenuBar(menubar)
354 def show_about(self):
355 QMessageBox.about(self, "Electrum",
356 _("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."))
358 def show_report_bug(self):
359 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
360 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
363 def notify_transactions(self):
364 if not self.network or not self.network.is_connected():
367 print_error("Notifying GUI")
368 if len(self.network.pending_transactions_for_notifications) > 0:
369 # Combine the transactions if there are more then three
370 tx_amount = len(self.network.pending_transactions_for_notifications)
373 for tx in self.network.pending_transactions_for_notifications:
374 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
378 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
379 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
381 self.network.pending_transactions_for_notifications = []
383 for tx in self.network.pending_transactions_for_notifications:
385 self.network.pending_transactions_for_notifications.remove(tx)
386 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
388 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
390 def notify(self, message):
391 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
395 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
396 def getOpenFileName(self, title, filter = ""):
397 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
398 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
399 if fileName and directory != os.path.dirname(fileName):
400 self.config.set_key('io_dir', os.path.dirname(fileName), True)
403 def getSaveFileName(self, title, filename, filter = ""):
404 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
405 path = os.path.join( directory, filename )
406 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
407 if fileName and directory != os.path.dirname(fileName):
408 self.config.set_key('io_dir', os.path.dirname(fileName), True)
412 QMainWindow.close(self)
413 run_hook('close_main_window')
415 def connect_slots(self, sender):
416 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
417 self.previous_payto_e=''
419 def timer_actions(self):
420 if self.need_update.is_set():
422 self.need_update.clear()
423 run_hook('timer_actions')
425 def format_amount(self, x, is_diff=False, whitespaces=False):
426 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
429 def get_decimal_point(self):
430 return self.decimal_point
434 assert self.decimal_point in [5,8]
435 return "BTC" if self.decimal_point == 8 else "mBTC"
438 def update_status(self):
439 if self.network is None or not self.network.is_running():
441 icon = QIcon(":icons/status_disconnected.png")
443 elif self.network.is_connected():
444 if not self.wallet.up_to_date:
445 text = _("Synchronizing...")
446 icon = QIcon(":icons/status_waiting.png")
447 elif self.network.server_lag > 1:
448 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
449 icon = QIcon(":icons/status_lagging.png")
451 c, u = self.wallet.get_account_balance(self.current_account)
452 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
453 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
455 # append fiat balance and price from exchange rate plugin
457 run_hook('get_fiat_status_text', c+u, r)
462 self.tray.setToolTip(text)
463 icon = QIcon(":icons/status_connected.png")
465 text = _("Not connected")
466 icon = QIcon(":icons/status_disconnected.png")
468 self.balance_label.setText(text)
469 self.status_button.setIcon( icon )
472 def update_wallet(self):
474 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
475 self.update_history_tab()
476 self.update_receive_tab()
477 self.update_contacts_tab()
478 self.update_completions()
479 self.update_invoices_tab()
482 def create_history_tab(self):
483 self.history_list = l = MyTreeWidget(self)
485 for i,width in enumerate(self.column_widths['history']):
486 l.setColumnWidth(i, width)
487 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
488 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
489 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
491 l.customContextMenuRequested.connect(self.create_history_menu)
495 def create_history_menu(self, position):
496 self.history_list.selectedIndexes()
497 item = self.history_list.currentItem()
498 be = self.config.get('block_explorer', 'Blockchain.info')
499 if be == 'Blockchain.info':
500 block_explorer = 'https://blockchain.info/tx/'
501 elif be == 'Blockr.io':
502 block_explorer = 'https://blockr.io/tx/info/'
503 elif be == 'Insight.is':
504 block_explorer = 'http://live.insight.is/tx/'
506 tx_hash = str(item.data(0, Qt.UserRole).toString())
507 if not tx_hash: return
509 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
510 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
511 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
512 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
513 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
516 def show_transaction(self, tx):
517 import transaction_dialog
518 d = transaction_dialog.TxDialog(tx, self)
521 def tx_label_clicked(self, item, column):
522 if column==2 and item.isSelected():
524 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
525 self.history_list.editItem( item, column )
526 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
529 def tx_label_changed(self, item, column):
533 tx_hash = str(item.data(0, Qt.UserRole).toString())
534 tx = self.wallet.transactions.get(tx_hash)
535 text = unicode( item.text(2) )
536 self.wallet.set_label(tx_hash, text)
538 item.setForeground(2, QBrush(QColor('black')))
540 text = self.wallet.get_default_label(tx_hash)
541 item.setText(2, text)
542 item.setForeground(2, QBrush(QColor('gray')))
546 def edit_label(self, is_recv):
547 l = self.receive_list if is_recv else self.contacts_list
548 item = l.currentItem()
549 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
550 l.editItem( item, 1 )
551 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
555 def address_label_clicked(self, item, column, l, column_addr, column_label):
556 if column == column_label and item.isSelected():
557 is_editable = item.data(0, 32).toBool()
560 addr = unicode( item.text(column_addr) )
561 label = unicode( item.text(column_label) )
562 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
563 l.editItem( item, column )
564 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 def address_label_changed(self, item, column, l, column_addr, column_label):
568 if column == column_label:
569 addr = unicode( item.text(column_addr) )
570 text = unicode( item.text(column_label) )
571 is_editable = item.data(0, 32).toBool()
575 changed = self.wallet.set_label(addr, text)
577 self.update_history_tab()
578 self.update_completions()
580 self.current_item_changed(item)
582 run_hook('item_changed', item, column)
585 def current_item_changed(self, a):
586 run_hook('current_item_changed', a)
590 def update_history_tab(self):
592 self.history_list.clear()
593 for item in self.wallet.get_tx_history(self.current_account):
594 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
595 time_str = _("unknown")
598 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
600 time_str = _("error")
603 time_str = 'unverified'
604 icon = QIcon(":icons/unconfirmed.png")
607 icon = QIcon(":icons/unconfirmed.png")
609 icon = QIcon(":icons/clock%d.png"%conf)
611 icon = QIcon(":icons/confirmed.png")
613 if value is not None:
614 v_str = self.format_amount(value, True, whitespaces=True)
618 balance_str = self.format_amount(balance, whitespaces=True)
621 label, is_default_label = self.wallet.get_label(tx_hash)
623 label = _('Pruned transaction outputs')
624 is_default_label = False
626 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
627 item.setFont(2, QFont(MONOSPACE_FONT))
628 item.setFont(3, QFont(MONOSPACE_FONT))
629 item.setFont(4, QFont(MONOSPACE_FONT))
631 item.setForeground(3, QBrush(QColor("#BC1E1E")))
633 item.setData(0, Qt.UserRole, tx_hash)
634 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
636 item.setForeground(2, QBrush(QColor('grey')))
638 item.setIcon(0, icon)
639 self.history_list.insertTopLevelItem(0,item)
642 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
643 run_hook('history_tab_update')
646 def create_send_tab(self):
649 self.send_grid = grid = QGridLayout(w)
651 grid.setColumnMinimumWidth(3,300)
652 grid.setColumnStretch(5,1)
653 grid.setRowStretch(8, 1)
655 from paytoedit import PayToEdit
656 self.amount_e = BTCAmountEdit(self.get_decimal_point)
657 self.payto_e = PayToEdit(self.amount_e)
658 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)'))
659 grid.addWidget(QLabel(_('Pay to')), 1, 0)
660 grid.addWidget(self.payto_e, 1, 1, 1, 3)
661 grid.addWidget(self.payto_help, 1, 4)
663 completer = QCompleter()
664 completer.setCaseSensitivity(False)
665 self.payto_e.setCompleter(completer)
666 completer.setModel(self.completions)
668 self.message_e = MyLineEdit()
669 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.'))
670 grid.addWidget(QLabel(_('Description')), 2, 0)
671 grid.addWidget(self.message_e, 2, 1, 1, 3)
672 grid.addWidget(self.message_help, 2, 4)
674 self.from_label = QLabel(_('From'))
675 grid.addWidget(self.from_label, 3, 0)
676 self.from_list = MyTreeWidget(self)
677 self.from_list.setColumnCount(2)
678 self.from_list.setColumnWidth(0, 350)
679 self.from_list.setColumnWidth(1, 50)
680 self.from_list.setHeaderHidden(True)
681 self.from_list.setMaximumHeight(80)
682 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
683 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
684 grid.addWidget(self.from_list, 3, 1, 1, 3)
685 self.set_pay_from([])
687 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
688 + _('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.') \
689 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
690 grid.addWidget(QLabel(_('Amount')), 4, 0)
691 grid.addWidget(self.amount_e, 4, 1, 1, 2)
692 grid.addWidget(self.amount_help, 4, 3)
694 self.fee_e = BTCAmountEdit(self.get_decimal_point)
695 grid.addWidget(QLabel(_('Fee')), 5, 0)
696 grid.addWidget(self.fee_e, 5, 1, 1, 2)
697 grid.addWidget(HelpButton(
698 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
699 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
700 + _('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)
702 self.send_button = EnterButton(_("Send"), self.do_send)
703 grid.addWidget(self.send_button, 6, 1)
705 b = EnterButton(_("Clear"), self.do_clear)
706 grid.addWidget(b, 6, 2)
708 self.payto_sig = QLabel('')
709 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
711 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
712 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
715 def entry_changed( is_fee ):
716 self.funds_error = False
718 if self.amount_e.is_shortcut:
719 self.amount_e.is_shortcut = False
720 sendable = self.get_sendable_balance()
721 # there is only one output because we are completely spending inputs
722 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
723 fee = self.wallet.estimated_fee(inputs, 1)
725 self.amount_e.setAmount(amount)
726 self.fee_e.setAmount(fee)
729 amount = self.amount_e.get_amount()
730 fee = self.fee_e.get_amount()
732 if not is_fee: fee = None
735 # assume that there will be 2 outputs (one for change)
736 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
738 self.fee_e.setAmount(fee)
741 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
745 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
746 self.funds_error = True
747 text = _( "Not enough funds" )
748 c, u = self.wallet.get_frozen_balance()
749 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
751 self.statusBar().showMessage(text)
752 self.amount_e.setPalette(palette)
753 self.fee_e.setPalette(palette)
755 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
756 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
758 run_hook('create_send_tab', grid)
761 def from_list_delete(self, item):
762 i = self.from_list.indexOfTopLevelItem(item)
764 self.redraw_from_list()
766 def from_list_menu(self, position):
767 item = self.from_list.itemAt(position)
769 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
770 menu.exec_(self.from_list.viewport().mapToGlobal(position))
772 def set_pay_from(self, domain = None):
773 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
774 self.redraw_from_list()
776 def redraw_from_list(self):
777 self.from_list.clear()
778 self.from_label.setHidden(len(self.pay_from) == 0)
779 self.from_list.setHidden(len(self.pay_from) == 0)
782 h = x.get('prevout_hash')
783 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
785 for item in self.pay_from:
786 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
788 def update_completions(self):
790 for addr,label in self.wallet.labels.items():
791 if addr in self.wallet.addressbook:
792 l.append( label + ' <' + addr + '>')
794 run_hook('update_completions', l)
795 self.completions.setStringList(l)
799 return lambda s, *args: s.do_protect(func, args)
803 label = unicode( self.message_e.text() )
805 if self.gui_object.payment_request:
806 outputs = self.gui_object.payment_request.get_outputs()
808 outputs = self.payto_e.get_outputs()
811 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
814 for addr, x in outputs:
815 if addr is None or not bitcoin.is_address(addr):
816 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
819 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
822 amount = sum(map(lambda x:x[1], outputs))
824 fee = self.fee_e.get_amount()
826 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
829 confirm_amount = self.config.get('confirm_amount', 100000000)
830 if amount >= confirm_amount:
831 o = '\n'.join(map(lambda x:x[0], outputs))
832 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
835 confirm_fee = self.config.get('confirm_fee', 100000)
836 if fee >= confirm_fee:
837 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()}):
840 self.send_tx(outputs, fee, label)
845 def send_tx(self, outputs, fee, label, password):
846 self.send_button.setDisabled(True)
848 # first, create an unsigned tx
849 coins = self.get_coins()
851 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
853 except Exception as e:
854 traceback.print_exc(file=sys.stdout)
855 self.show_message(str(e))
856 self.send_button.setDisabled(False)
859 # call hook to see if plugin needs gui interaction
860 run_hook('send_tx', tx)
866 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
867 self.wallet.sign_transaction(tx, keypairs, password)
868 return tx, fee, label
870 def sign_done(tx, fee, label):
872 self.show_message(tx.error)
873 self.send_button.setDisabled(False)
875 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
876 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
877 self.send_button.setDisabled(False)
880 self.wallet.set_label(tx.hash(), label)
882 if not tx.is_complete() or self.config.get('show_before_broadcast'):
883 self.show_transaction(tx)
885 self.send_button.setDisabled(False)
888 self.broadcast_transaction(tx)
890 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
891 self.waiting_dialog.start()
895 def broadcast_transaction(self, tx):
897 def broadcast_thread():
898 pr = self.gui_object.payment_request
900 return self.wallet.sendtx(tx)
903 self.gui_object.payment_request = None
904 return False, _("Payment request has expired")
906 status, msg = self.wallet.sendtx(tx)
910 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_PAID, tx.hash())
911 self.wallet.storage.put('invoices', self.invoices)
912 self.update_invoices_tab()
913 self.gui_object.payment_request = None
914 refund_address = self.wallet.addresses()[0]
915 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
921 def broadcast_done(status, msg):
923 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
926 QMessageBox.warning(self, _('Error'), msg, _('OK'))
927 self.send_button.setDisabled(False)
929 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
930 self.waiting_dialog.start()
934 def prepare_for_payment_request(self):
935 self.tabs.setCurrentIndex(1)
936 self.payto_e.is_pr = True
937 for e in [self.payto_e, self.amount_e, self.message_e]:
939 for h in [self.payto_help, self.amount_help, self.message_help]:
941 self.payto_e.setText(_("please wait..."))
944 def payment_request_ok(self):
945 pr = self.gui_object.payment_request
947 if pr_id not in self.invoices:
948 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_UNPAID, None)
949 self.wallet.storage.put('invoices', self.invoices)
950 self.update_invoices_tab()
952 print_error('invoice already in list')
954 status = self.invoices[pr_id][3]
955 if status == PR_PAID:
957 self.show_message("invoice already paid")
958 self.gui_object.payment_request = None
961 self.payto_help.show()
962 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
964 self.payto_e.setGreen()
965 self.payto_e.setText(pr.domain)
966 self.amount_e.setText(self.format_amount(pr.get_amount()))
967 self.message_e.setText(pr.get_memo())
969 def payment_request_error(self):
971 self.show_message(self.gui_object.payment_request.error)
972 self.gui_object.payment_request = None
974 def set_send(self, address, amount, label, message):
976 if label and self.wallet.labels.get(address) != label:
977 if self.question('Give label "%s" to address %s ?'%(label,address)):
978 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
979 self.wallet.addressbook.append(address)
980 self.wallet.set_label(address, label)
982 self.tabs.setCurrentIndex(1)
983 label = self.wallet.labels.get(address)
984 m_addr = label + ' <'+ address +'>' if label else address
985 self.payto_e.setText(m_addr)
987 self.message_e.setText(message)
989 self.amount_e.setText(amount)
993 self.payto_e.is_pr = False
994 self.payto_sig.setVisible(False)
995 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
999 for h in [self.payto_help, self.amount_help, self.message_help]:
1002 self.payto_help.set_alt(None)
1003 self.set_pay_from([])
1004 self.update_status()
1008 def set_addrs_frozen(self,addrs,freeze):
1010 if not addr: continue
1011 if addr in self.wallet.frozen_addresses and not freeze:
1012 self.wallet.unfreeze(addr)
1013 elif addr not in self.wallet.frozen_addresses and freeze:
1014 self.wallet.freeze(addr)
1015 self.update_receive_tab()
1019 def create_list_tab(self, headers):
1020 "generic tab creation method"
1021 l = MyTreeWidget(self)
1022 l.setColumnCount( len(headers) )
1023 l.setHeaderLabels( headers )
1026 vbox = QVBoxLayout()
1033 vbox.addWidget(buttons)
1038 def create_receive_tab(self):
1039 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1040 l.setContextMenuPolicy(Qt.CustomContextMenu)
1041 l.customContextMenuRequested.connect(self.create_receive_menu)
1042 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1043 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1044 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1045 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1046 self.receive_list = l
1052 def save_column_widths(self):
1053 self.column_widths["receive"] = []
1054 for i in range(self.receive_list.columnCount() -1):
1055 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1057 self.column_widths["history"] = []
1058 for i in range(self.history_list.columnCount() - 1):
1059 self.column_widths["history"].append(self.history_list.columnWidth(i))
1061 self.column_widths["contacts"] = []
1062 for i in range(self.contacts_list.columnCount() - 1):
1063 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1065 self.config.set_key("column_widths_2", self.column_widths, True)
1068 def create_contacts_tab(self):
1069 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1070 l.setContextMenuPolicy(Qt.CustomContextMenu)
1071 l.customContextMenuRequested.connect(self.create_contact_menu)
1072 for i,width in enumerate(self.column_widths['contacts']):
1073 l.setColumnWidth(i, width)
1075 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1076 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1077 self.contacts_list = l
1081 def create_invoices_tab(self):
1082 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1084 h.setStretchLastSection(False)
1085 h.setResizeMode(1, QHeaderView.Stretch)
1086 l.setContextMenuPolicy(Qt.CustomContextMenu)
1087 l.customContextMenuRequested.connect(self.create_invoice_menu)
1088 self.invoices_list = l
1091 def update_invoices_tab(self):
1092 invoices = self.wallet.storage.get('invoices', {})
1093 l = self.invoices_list
1095 for key, value in invoices.items():
1097 domain, memo, amount, status, tx_hash = value
1101 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1102 l.addTopLevelItem(item)
1104 l.setCurrentItem(l.topLevelItem(0))
1108 def delete_imported_key(self, addr):
1109 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1110 self.wallet.delete_imported_key(addr)
1111 self.update_receive_tab()
1112 self.update_history_tab()
1114 def edit_account_label(self, k):
1115 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1117 label = unicode(text)
1118 self.wallet.set_label(k,label)
1119 self.update_receive_tab()
1121 def account_set_expanded(self, item, k, b):
1123 self.accounts_expanded[k] = b
1125 def create_account_menu(self, position, k, item):
1127 if item.isExpanded():
1128 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1130 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1131 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1132 if self.wallet.seed_version > 4:
1133 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1134 if self.wallet.account_is_pending(k):
1135 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1136 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1138 def delete_pending_account(self, k):
1139 self.wallet.delete_pending_account(k)
1140 self.update_receive_tab()
1142 def create_receive_menu(self, position):
1143 # fixme: this function apparently has a side effect.
1144 # if it is not called the menu pops up several times
1145 #self.receive_list.selectedIndexes()
1147 selected = self.receive_list.selectedItems()
1148 multi_select = len(selected) > 1
1149 addrs = [unicode(item.text(0)) for item in selected]
1150 if not multi_select:
1151 item = self.receive_list.itemAt(position)
1155 if not is_valid(addr):
1156 k = str(item.data(0,32).toString())
1158 self.create_account_menu(position, k, item)
1160 item.setExpanded(not item.isExpanded())
1164 if not multi_select:
1165 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1166 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1167 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1168 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1169 if not self.wallet.is_watching_only():
1170 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1171 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1172 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1173 if self.wallet.is_imported(addr):
1174 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1176 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1177 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1178 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1179 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1181 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1182 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1184 run_hook('receive_menu', menu, addrs)
1185 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1188 def get_sendable_balance(self):
1189 return sum(map(lambda x:x['value'], self.get_coins()))
1192 def get_coins(self):
1194 return self.pay_from
1196 domain = self.wallet.get_account_addresses(self.current_account)
1197 for i in self.wallet.frozen_addresses:
1198 if i in domain: domain.remove(i)
1199 return self.wallet.get_unspent_coins(domain)
1202 def send_from_addresses(self, addrs):
1203 self.set_pay_from( addrs )
1204 self.tabs.setCurrentIndex(1)
1207 def payto(self, addr):
1209 label = self.wallet.labels.get(addr)
1210 m_addr = label + ' <' + addr + '>' if label else addr
1211 self.tabs.setCurrentIndex(1)
1212 self.payto_e.setText(m_addr)
1213 self.amount_e.setFocus()
1216 def delete_contact(self, x):
1217 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1218 self.wallet.delete_contact(x)
1219 self.wallet.set_label(x, None)
1220 self.update_history_tab()
1221 self.update_contacts_tab()
1222 self.update_completions()
1225 def create_contact_menu(self, position):
1226 item = self.contacts_list.itemAt(position)
1229 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1231 addr = unicode(item.text(0))
1232 label = unicode(item.text(1))
1233 is_editable = item.data(0,32).toBool()
1234 payto_addr = item.data(0,33).toString()
1235 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1236 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1237 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1239 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1240 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1242 run_hook('create_contact_menu', menu, item)
1243 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1245 def delete_invoice(self, key):
1246 self.invoices.pop(key)
1247 self.wallet.storage.put('invoices', self.invoices)
1248 self.update_invoices_tab()
1250 def show_invoice(self, key):
1251 from electrum.paymentrequest import PaymentRequest
1252 domain, memo, value, status, tx_hash = self.invoices[key]
1253 pr = PaymentRequest(self.config)
1257 self.show_pr_details(pr)
1259 def show_pr_details(self, pr):
1260 msg = 'Domain: ' + pr.domain
1261 msg += '\nStatus: ' + pr.get_status()
1262 msg += '\nMemo: ' + pr.get_memo()
1263 msg += '\nPayment URL: ' + pr.payment_url
1264 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1265 QMessageBox.information(self, 'Invoice', msg , 'OK')
1267 def do_pay_invoice(self, key):
1268 from electrum.paymentrequest import PaymentRequest
1269 domain, memo, value, status, tx_hash = self.invoices[key]
1270 pr = PaymentRequest(self.config)
1273 self.gui_object.payment_request = pr
1274 self.prepare_for_payment_request()
1276 self.payment_request_ok()
1278 self.payment_request_error()
1281 def create_invoice_menu(self, position):
1282 item = self.invoices_list.itemAt(position)
1285 k = self.invoices_list.indexOfTopLevelItem(item)
1286 key = self.invoices.keys()[k]
1287 domain, memo, value, status, tx_hash = self.invoices[key]
1289 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1290 if status == PR_UNPAID:
1291 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1292 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1293 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1296 def update_receive_item(self, item):
1297 item.setFont(0, QFont(MONOSPACE_FONT))
1298 address = str(item.data(0,0).toString())
1299 label = self.wallet.labels.get(address,'')
1300 item.setData(1,0,label)
1301 item.setData(0,32, True) # is editable
1303 run_hook('update_receive_item', address, item)
1305 if not self.wallet.is_mine(address): return
1307 c, u = self.wallet.get_addr_balance(address)
1308 balance = self.format_amount(c + u)
1309 item.setData(2,0,balance)
1311 if address in self.wallet.frozen_addresses:
1312 item.setBackgroundColor(0, QColor('lightblue'))
1315 def update_receive_tab(self):
1316 l = self.receive_list
1317 # extend the syntax for consistency
1318 l.addChild = l.addTopLevelItem
1319 l.insertChild = l.insertTopLevelItem
1322 for i,width in enumerate(self.column_widths['receive']):
1323 l.setColumnWidth(i, width)
1325 accounts = self.wallet.get_accounts()
1326 if self.current_account is None:
1327 account_items = sorted(accounts.items())
1329 account_items = [(self.current_account, accounts.get(self.current_account))]
1332 for k, account in account_items:
1334 if len(accounts) > 1:
1335 name = self.wallet.get_account_name(k)
1336 c,u = self.wallet.get_account_balance(k)
1337 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1338 l.addTopLevelItem(account_item)
1339 account_item.setExpanded(self.accounts_expanded.get(k, True))
1340 account_item.setData(0, 32, k)
1344 sequences = [0,1] if account.has_change() else [0]
1345 for is_change in sequences:
1346 if len(sequences) > 1:
1347 name = _("Receiving") if not is_change else _("Change")
1348 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1349 account_item.addChild(seq_item)
1351 seq_item.setExpanded(True)
1353 seq_item = account_item
1355 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1361 for address in account.get_addresses(is_change):
1363 num, is_used = self.wallet.is_used(address)
1366 if gap > self.wallet.gap_limit:
1371 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1372 self.update_receive_item(item)
1374 item.setBackgroundColor(1, QColor('red'))
1378 seq_item.insertChild(0,used_item)
1380 used_item.addChild(item)
1382 seq_item.addChild(item)
1384 # we use column 1 because column 0 may be hidden
1385 l.setCurrentItem(l.topLevelItem(0),1)
1388 def update_contacts_tab(self):
1389 l = self.contacts_list
1392 for address in self.wallet.addressbook:
1393 label = self.wallet.labels.get(address,'')
1394 n = self.wallet.get_num_tx(address)
1395 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1396 item.setFont(0, QFont(MONOSPACE_FONT))
1397 # 32 = label can be edited (bool)
1398 item.setData(0,32, True)
1400 item.setData(0,33, address)
1401 l.addTopLevelItem(item)
1403 run_hook('update_contacts_tab', l)
1404 l.setCurrentItem(l.topLevelItem(0))
1408 def create_console_tab(self):
1409 from console import Console
1410 self.console = console = Console()
1414 def update_console(self):
1415 console = self.console
1416 console.history = self.config.get("console-history",[])
1417 console.history_index = len(console.history)
1419 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1420 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1422 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1424 def mkfunc(f, method):
1425 return lambda *args: apply( f, (method, args, self.password_dialog ))
1427 if m[0]=='_' or m in ['network','wallet']: continue
1428 methods[m] = mkfunc(c._run, m)
1430 console.updateNamespace(methods)
1433 def change_account(self,s):
1434 if s == _("All accounts"):
1435 self.current_account = None
1437 accounts = self.wallet.get_account_names()
1438 for k, v in accounts.items():
1440 self.current_account = k
1441 self.update_history_tab()
1442 self.update_status()
1443 self.update_receive_tab()
1445 def create_status_bar(self):
1448 sb.setFixedHeight(35)
1449 qtVersion = qVersion()
1451 self.balance_label = QLabel("")
1452 sb.addWidget(self.balance_label)
1454 from version_getter import UpdateLabel
1455 self.updatelabel = UpdateLabel(self.config, sb)
1457 self.account_selector = QComboBox()
1458 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1459 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1460 sb.addPermanentWidget(self.account_selector)
1462 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1463 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1465 self.lock_icon = QIcon()
1466 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1467 sb.addPermanentWidget( self.password_button )
1469 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1470 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1471 sb.addPermanentWidget( self.seed_button )
1472 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1473 sb.addPermanentWidget( self.status_button )
1475 run_hook('create_status_bar', (sb,))
1477 self.setStatusBar(sb)
1480 def update_lock_icon(self):
1481 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1482 self.password_button.setIcon( icon )
1485 def update_buttons_on_seed(self):
1486 if self.wallet.has_seed():
1487 self.seed_button.show()
1489 self.seed_button.hide()
1491 if not self.wallet.is_watching_only():
1492 self.password_button.show()
1493 self.send_button.setText(_("Send"))
1495 self.password_button.hide()
1496 self.send_button.setText(_("Create unsigned transaction"))
1499 def change_password_dialog(self):
1500 from password_dialog import PasswordDialog
1501 d = PasswordDialog(self.wallet, self)
1503 self.update_lock_icon()
1506 def new_contact_dialog(self):
1509 d.setWindowTitle(_("New Contact"))
1510 vbox = QVBoxLayout(d)
1511 vbox.addWidget(QLabel(_('New Contact')+':'))
1513 grid = QGridLayout()
1516 grid.addWidget(QLabel(_("Address")), 1, 0)
1517 grid.addWidget(line1, 1, 1)
1518 grid.addWidget(QLabel(_("Name")), 2, 0)
1519 grid.addWidget(line2, 2, 1)
1521 vbox.addLayout(grid)
1522 vbox.addLayout(ok_cancel_buttons(d))
1527 address = str(line1.text())
1528 label = unicode(line2.text())
1530 if not is_valid(address):
1531 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1534 self.wallet.add_contact(address)
1536 self.wallet.set_label(address, label)
1538 self.update_contacts_tab()
1539 self.update_history_tab()
1540 self.update_completions()
1541 self.tabs.setCurrentIndex(3)
1545 def new_account_dialog(self, password):
1547 dialog = QDialog(self)
1549 dialog.setWindowTitle(_("New Account"))
1551 vbox = QVBoxLayout()
1552 vbox.addWidget(QLabel(_('Account name')+':'))
1555 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1556 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1561 vbox.addLayout(ok_cancel_buttons(dialog))
1562 dialog.setLayout(vbox)
1566 name = str(e.text())
1569 self.wallet.create_pending_account(name, password)
1570 self.update_receive_tab()
1571 self.tabs.setCurrentIndex(2)
1576 def show_master_public_keys(self):
1578 dialog = QDialog(self)
1580 dialog.setWindowTitle(_("Master Public Keys"))
1582 main_layout = QGridLayout()
1583 mpk_dict = self.wallet.get_master_public_keys()
1585 for key, value in mpk_dict.items():
1586 main_layout.addWidget(QLabel(key), i, 0)
1587 mpk_text = QTextEdit()
1588 mpk_text.setReadOnly(True)
1589 mpk_text.setMaximumHeight(170)
1590 mpk_text.setText(value)
1591 main_layout.addWidget(mpk_text, i + 1, 0)
1594 vbox = QVBoxLayout()
1595 vbox.addLayout(main_layout)
1596 vbox.addLayout(close_button(dialog))
1598 dialog.setLayout(vbox)
1603 def show_seed_dialog(self, password):
1604 if not self.wallet.has_seed():
1605 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1609 mnemonic = self.wallet.get_mnemonic(password)
1611 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1613 from seed_dialog import SeedDialog
1614 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1619 def show_qrcode(self, data, title = _("QR code")):
1623 d.setWindowTitle(title)
1624 d.setMinimumSize(270, 300)
1625 vbox = QVBoxLayout()
1626 qrw = QRCodeWidget(data)
1627 vbox.addWidget(qrw, 1)
1628 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1629 hbox = QHBoxLayout()
1632 filename = os.path.join(self.config.path, "qrcode.bmp")
1635 bmp.save_qrcode(qrw.qr, filename)
1636 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1638 def copy_to_clipboard():
1639 bmp.save_qrcode(qrw.qr, filename)
1640 self.app.clipboard().setImage(QImage(filename))
1641 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1643 b = QPushButton(_("Copy"))
1645 b.clicked.connect(copy_to_clipboard)
1647 b = QPushButton(_("Save"))
1649 b.clicked.connect(print_qr)
1651 b = QPushButton(_("Close"))
1653 b.clicked.connect(d.accept)
1656 vbox.addLayout(hbox)
1661 def do_protect(self, func, args):
1662 if self.wallet.use_encryption:
1663 password = self.password_dialog()
1669 if args != (False,):
1670 args = (self,) + args + (password,)
1672 args = (self,password)
1676 def show_public_keys(self, address):
1677 if not address: return
1679 pubkey_list = self.wallet.get_public_keys(address)
1680 except Exception as e:
1681 traceback.print_exc(file=sys.stdout)
1682 self.show_message(str(e))
1686 d.setMinimumSize(600, 200)
1688 vbox = QVBoxLayout()
1689 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1690 vbox.addWidget( QLabel(_("Public key") + ':'))
1692 keys.setReadOnly(True)
1693 keys.setText('\n'.join(pubkey_list))
1694 vbox.addWidget(keys)
1695 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1696 vbox.addLayout(close_button(d))
1701 def show_private_key(self, address, password):
1702 if not address: return
1704 pk_list = self.wallet.get_private_key(address, password)
1705 except Exception as e:
1706 traceback.print_exc(file=sys.stdout)
1707 self.show_message(str(e))
1711 d.setMinimumSize(600, 200)
1713 vbox = QVBoxLayout()
1714 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1715 vbox.addWidget( QLabel(_("Private key") + ':'))
1717 keys.setReadOnly(True)
1718 keys.setText('\n'.join(pk_list))
1719 vbox.addWidget(keys)
1720 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1721 vbox.addLayout(close_button(d))
1727 def do_sign(self, address, message, signature, password):
1728 message = unicode(message.toPlainText())
1729 message = message.encode('utf-8')
1731 sig = self.wallet.sign_message(str(address.text()), message, password)
1732 signature.setText(sig)
1733 except Exception as e:
1734 self.show_message(str(e))
1736 def do_verify(self, address, message, signature):
1737 message = unicode(message.toPlainText())
1738 message = message.encode('utf-8')
1739 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1740 self.show_message(_("Signature verified"))
1742 self.show_message(_("Error: wrong signature"))
1745 def sign_verify_message(self, address=''):
1748 d.setWindowTitle(_('Sign/verify Message'))
1749 d.setMinimumSize(410, 290)
1751 layout = QGridLayout(d)
1753 message_e = QTextEdit()
1754 layout.addWidget(QLabel(_('Message')), 1, 0)
1755 layout.addWidget(message_e, 1, 1)
1756 layout.setRowStretch(2,3)
1758 address_e = QLineEdit()
1759 address_e.setText(address)
1760 layout.addWidget(QLabel(_('Address')), 2, 0)
1761 layout.addWidget(address_e, 2, 1)
1763 signature_e = QTextEdit()
1764 layout.addWidget(QLabel(_('Signature')), 3, 0)
1765 layout.addWidget(signature_e, 3, 1)
1766 layout.setRowStretch(3,1)
1768 hbox = QHBoxLayout()
1770 b = QPushButton(_("Sign"))
1771 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1774 b = QPushButton(_("Verify"))
1775 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1778 b = QPushButton(_("Close"))
1779 b.clicked.connect(d.accept)
1781 layout.addLayout(hbox, 4, 1)
1786 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1788 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1789 message_e.setText(decrypted)
1790 except Exception as e:
1791 self.show_message(str(e))
1794 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1795 message = unicode(message_e.toPlainText())
1796 message = message.encode('utf-8')
1798 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1799 encrypted_e.setText(encrypted)
1800 except Exception as e:
1801 self.show_message(str(e))
1805 def encrypt_message(self, address = ''):
1808 d.setWindowTitle(_('Encrypt/decrypt Message'))
1809 d.setMinimumSize(610, 490)
1811 layout = QGridLayout(d)
1813 message_e = QTextEdit()
1814 layout.addWidget(QLabel(_('Message')), 1, 0)
1815 layout.addWidget(message_e, 1, 1)
1816 layout.setRowStretch(2,3)
1818 pubkey_e = QLineEdit()
1820 pubkey = self.wallet.getpubkeys(address)[0]
1821 pubkey_e.setText(pubkey)
1822 layout.addWidget(QLabel(_('Public key')), 2, 0)
1823 layout.addWidget(pubkey_e, 2, 1)
1825 encrypted_e = QTextEdit()
1826 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1827 layout.addWidget(encrypted_e, 3, 1)
1828 layout.setRowStretch(3,1)
1830 hbox = QHBoxLayout()
1831 b = QPushButton(_("Encrypt"))
1832 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1835 b = QPushButton(_("Decrypt"))
1836 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1839 b = QPushButton(_("Close"))
1840 b.clicked.connect(d.accept)
1843 layout.addLayout(hbox, 4, 1)
1847 def question(self, msg):
1848 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1850 def show_message(self, msg):
1851 QMessageBox.information(self, _('Message'), msg, _('OK'))
1853 def password_dialog(self, msg=None):
1856 d.setWindowTitle(_("Enter Password"))
1861 vbox = QVBoxLayout()
1863 msg = _('Please enter your password')
1864 vbox.addWidget(QLabel(msg))
1866 grid = QGridLayout()
1868 grid.addWidget(QLabel(_('Password')), 1, 0)
1869 grid.addWidget(pw, 1, 1)
1870 vbox.addLayout(grid)
1872 vbox.addLayout(ok_cancel_buttons(d))
1875 run_hook('password_dialog', pw, grid, 1)
1876 if not d.exec_(): return
1877 return unicode(pw.text())
1886 def tx_from_text(self, txt):
1887 "json or raw hexadecimal"
1890 tx = Transaction(txt)
1896 tx_dict = json.loads(str(txt))
1897 assert "hex" in tx_dict.keys()
1898 tx = Transaction(tx_dict["hex"])
1899 if tx_dict.has_key("input_info"):
1900 input_info = json.loads(tx_dict['input_info'])
1901 tx.add_input_info(input_info)
1904 traceback.print_exc(file=sys.stdout)
1907 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1911 def read_tx_from_file(self):
1912 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1916 with open(fileName, "r") as f:
1917 file_content = f.read()
1918 except (ValueError, IOError, os.error), reason:
1919 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1921 return self.tx_from_text(file_content)
1925 def sign_raw_transaction(self, tx, input_info, password):
1926 self.wallet.signrawtransaction(tx, input_info, [], password)
1928 def do_process_from_text(self):
1929 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1932 tx = self.tx_from_text(text)
1934 self.show_transaction(tx)
1936 def do_process_from_file(self):
1937 tx = self.read_tx_from_file()
1939 self.show_transaction(tx)
1941 def do_process_from_txid(self):
1942 from electrum import transaction
1943 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1945 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1947 tx = transaction.Transaction(r)
1949 self.show_transaction(tx)
1951 self.show_message("unknown transaction")
1953 def do_process_from_csvReader(self, csvReader):
1958 for position, row in enumerate(csvReader):
1960 if not is_valid(address):
1961 errors.append((position, address))
1963 amount = Decimal(row[1])
1964 amount = int(100000000*amount)
1965 outputs.append((address, amount))
1966 except (ValueError, IOError, os.error), reason:
1967 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1971 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1972 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1976 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1977 except Exception as e:
1978 self.show_message(str(e))
1981 self.show_transaction(tx)
1983 def do_process_from_csv_file(self):
1984 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1988 with open(fileName, "r") as f:
1989 csvReader = csv.reader(f)
1990 self.do_process_from_csvReader(csvReader)
1991 except (ValueError, IOError, os.error), reason:
1992 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1995 def do_process_from_csv_text(self):
1996 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1997 + _("Format: address, amount. One output per line"), _("Load CSV"))
2000 f = StringIO.StringIO(text)
2001 csvReader = csv.reader(f)
2002 self.do_process_from_csvReader(csvReader)
2007 def export_privkeys_dialog(self, password):
2008 if self.wallet.is_watching_only():
2009 self.show_message(_("This is a watching-only wallet"))
2013 d.setWindowTitle(_('Private keys'))
2014 d.setMinimumSize(850, 300)
2015 vbox = QVBoxLayout(d)
2017 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2018 _("Exposing a single private key can compromise your entire wallet!"),
2019 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2020 vbox.addWidget(QLabel(msg))
2026 defaultname = 'electrum-private-keys.csv'
2027 select_msg = _('Select file to export your private keys to')
2028 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2029 vbox.addLayout(hbox)
2031 h, b = ok_cancel_buttons2(d, _('Export'))
2036 addresses = self.wallet.addresses(True)
2038 def privkeys_thread():
2039 for addr in addresses:
2043 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2044 d.emit(SIGNAL('computing_privkeys'))
2045 d.emit(SIGNAL('show_privkeys'))
2047 def show_privkeys():
2048 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2052 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2053 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2054 threading.Thread(target=privkeys_thread).start()
2060 filename = filename_e.text()
2065 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2066 except (IOError, os.error), reason:
2067 export_error_label = _("Electrum was unable to produce a private key-export.")
2068 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2070 except Exception as e:
2071 self.show_message(str(e))
2074 self.show_message(_("Private keys exported."))
2077 def do_export_privkeys(self, fileName, pklist, is_csv):
2078 with open(fileName, "w+") as f:
2080 transaction = csv.writer(f)
2081 transaction.writerow(["address", "private_key"])
2082 for addr, pk in pklist.items():
2083 transaction.writerow(["%34s"%addr,pk])
2086 f.write(json.dumps(pklist, indent = 4))
2089 def do_import_labels(self):
2090 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2091 if not labelsFile: return
2093 f = open(labelsFile, 'r')
2096 for key, value in json.loads(data).items():
2097 self.wallet.set_label(key, value)
2098 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2099 except (IOError, os.error), reason:
2100 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2103 def do_export_labels(self):
2104 labels = self.wallet.labels
2106 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2108 with open(fileName, 'w+') as f:
2109 json.dump(labels, f)
2110 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2111 except (IOError, os.error), reason:
2112 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2115 def export_history_dialog(self):
2118 d.setWindowTitle(_('Export History'))
2119 d.setMinimumSize(400, 200)
2120 vbox = QVBoxLayout(d)
2122 defaultname = os.path.expanduser('~/electrum-history.csv')
2123 select_msg = _('Select file to export your wallet transactions to')
2125 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2126 vbox.addLayout(hbox)
2130 h, b = ok_cancel_buttons2(d, _('Export'))
2135 filename = filename_e.text()
2140 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2141 except (IOError, os.error), reason:
2142 export_error_label = _("Electrum was unable to produce a transaction export.")
2143 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2146 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2149 def do_export_history(self, wallet, fileName, is_csv):
2150 history = wallet.get_tx_history()
2152 for item in history:
2153 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2155 if timestamp is not None:
2157 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2158 except [RuntimeError, TypeError, NameError] as reason:
2159 time_string = "unknown"
2162 time_string = "unknown"
2164 time_string = "pending"
2166 if value is not None:
2167 value_string = format_satoshis(value, True)
2172 fee_string = format_satoshis(fee, True)
2177 label, is_default_label = wallet.get_label(tx_hash)
2178 label = label.encode('utf-8')
2182 balance_string = format_satoshis(balance, False)
2184 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2186 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2188 with open(fileName, "w+") as f:
2190 transaction = csv.writer(f)
2191 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2193 transaction.writerow(line)
2196 f.write(json.dumps(lines, indent = 4))
2199 def sweep_key_dialog(self):
2201 d.setWindowTitle(_('Sweep private keys'))
2202 d.setMinimumSize(600, 300)
2204 vbox = QVBoxLayout(d)
2205 vbox.addWidget(QLabel(_("Enter private keys")))
2207 keys_e = QTextEdit()
2208 keys_e.setTabChangesFocus(True)
2209 vbox.addWidget(keys_e)
2211 h, address_e = address_field(self.wallet.addresses())
2215 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2216 vbox.addLayout(hbox)
2217 button.setEnabled(False)
2220 addr = str(address_e.text())
2221 if bitcoin.is_address(addr):
2225 pk = str(keys_e.toPlainText()).strip()
2226 if Wallet.is_private_key(pk):
2229 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2230 keys_e.textChanged.connect(f)
2231 address_e.textChanged.connect(f)
2235 fee = self.wallet.fee
2236 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2237 self.show_transaction(tx)
2241 def do_import_privkey(self, password):
2242 if not self.wallet.has_imported_keys():
2243 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2244 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2245 + _('Are you sure you understand what you are doing?'), 3, 4)
2248 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2251 text = str(text).split()
2256 addr = self.wallet.import_key(key, password)
2257 except Exception as e:
2263 addrlist.append(addr)
2265 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2267 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2268 self.update_receive_tab()
2269 self.update_history_tab()
2272 def settings_dialog(self):
2274 d.setWindowTitle(_('Electrum Settings'))
2276 vbox = QVBoxLayout()
2277 grid = QGridLayout()
2278 grid.setColumnStretch(0,1)
2280 nz_label = QLabel(_('Display zeros') + ':')
2281 grid.addWidget(nz_label, 0, 0)
2282 nz_e = AmountEdit(None,True)
2283 nz_e.setText("%d"% self.num_zeros)
2284 grid.addWidget(nz_e, 0, 1)
2285 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2286 grid.addWidget(HelpButton(msg), 0, 2)
2287 if not self.config.is_modifiable('num_zeros'):
2288 for w in [nz_e, nz_label]: w.setEnabled(False)
2290 lang_label=QLabel(_('Language') + ':')
2291 grid.addWidget(lang_label, 1, 0)
2292 lang_combo = QComboBox()
2293 from electrum.i18n import languages
2294 lang_combo.addItems(languages.values())
2296 index = languages.keys().index(self.config.get("language",''))
2299 lang_combo.setCurrentIndex(index)
2300 grid.addWidget(lang_combo, 1, 1)
2301 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2302 if not self.config.is_modifiable('language'):
2303 for w in [lang_combo, lang_label]: w.setEnabled(False)
2306 fee_label = QLabel(_('Transaction fee') + ':')
2307 grid.addWidget(fee_label, 2, 0)
2308 fee_e = BTCAmountEdit(self.get_decimal_point)
2309 fee_e.setAmount(self.wallet.fee)
2310 grid.addWidget(fee_e, 2, 1)
2311 msg = _('Fee per kilobyte of transaction.') + '\n' \
2312 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2313 grid.addWidget(HelpButton(msg), 2, 2)
2314 if not self.config.is_modifiable('fee_per_kb'):
2315 for w in [fee_e, fee_label]: w.setEnabled(False)
2317 units = ['BTC', 'mBTC']
2318 unit_label = QLabel(_('Base unit') + ':')
2319 grid.addWidget(unit_label, 3, 0)
2320 unit_combo = QComboBox()
2321 unit_combo.addItems(units)
2322 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2323 grid.addWidget(unit_combo, 3, 1)
2324 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2325 + '\n1BTC=1000mBTC.\n' \
2326 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2328 usechange_cb = QCheckBox(_('Use change addresses'))
2329 usechange_cb.setChecked(self.wallet.use_change)
2330 grid.addWidget(usechange_cb, 4, 0)
2331 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2332 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2334 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2335 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2336 grid.addWidget(block_ex_label, 5, 0)
2337 block_ex_combo = QComboBox()
2338 block_ex_combo.addItems(block_explorers)
2339 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2340 grid.addWidget(block_ex_combo, 5, 1)
2341 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2343 show_tx = self.config.get('show_before_broadcast', False)
2344 showtx_cb = QCheckBox(_('Show before broadcast'))
2345 showtx_cb.setChecked(show_tx)
2346 grid.addWidget(showtx_cb, 6, 0)
2347 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2349 vbox.addLayout(grid)
2351 vbox.addLayout(ok_cancel_buttons(d))
2355 if not d.exec_(): return
2357 fee = fee_e.get_amount()
2359 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2362 self.wallet.set_fee(fee)
2364 nz = unicode(nz_e.text())
2369 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2372 if self.num_zeros != nz:
2374 self.config.set_key('num_zeros', nz, True)
2375 self.update_history_tab()
2376 self.update_receive_tab()
2378 usechange_result = usechange_cb.isChecked()
2379 if self.wallet.use_change != usechange_result:
2380 self.wallet.use_change = usechange_result
2381 self.wallet.storage.put('use_change', self.wallet.use_change)
2383 if showtx_cb.isChecked() != show_tx:
2384 self.config.set_key('show_before_broadcast', not show_tx)
2386 unit_result = units[unit_combo.currentIndex()]
2387 if self.base_unit() != unit_result:
2388 self.decimal_point = 8 if unit_result == 'BTC' else 5
2389 self.config.set_key('decimal_point', self.decimal_point, True)
2390 self.update_history_tab()
2391 self.update_status()
2393 need_restart = False
2395 lang_request = languages.keys()[lang_combo.currentIndex()]
2396 if lang_request != self.config.get('language'):
2397 self.config.set_key("language", lang_request, True)
2400 be_result = block_explorers[block_ex_combo.currentIndex()]
2401 self.config.set_key('block_explorer', be_result, True)
2403 run_hook('close_settings_dialog')
2406 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2409 def run_network_dialog(self):
2410 if not self.network:
2412 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2414 def closeEvent(self, event):
2416 self.config.set_key("is_maximized", self.isMaximized())
2417 if not self.isMaximized():
2419 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2420 self.save_column_widths()
2421 self.config.set_key("console-history", self.console.history[-50:], True)
2422 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2426 def plugins_dialog(self):
2427 from electrum.plugins import plugins
2430 d.setWindowTitle(_('Electrum Plugins'))
2433 vbox = QVBoxLayout(d)
2436 scroll = QScrollArea()
2437 scroll.setEnabled(True)
2438 scroll.setWidgetResizable(True)
2439 scroll.setMinimumSize(400,250)
2440 vbox.addWidget(scroll)
2444 w.setMinimumHeight(len(plugins)*35)
2446 grid = QGridLayout()
2447 grid.setColumnStretch(0,1)
2450 def do_toggle(cb, p, w):
2453 if w: w.setEnabled(r)
2455 def mk_toggle(cb, p, w):
2456 return lambda: do_toggle(cb,p,w)
2458 for i, p in enumerate(plugins):
2460 cb = QCheckBox(p.fullname())
2461 cb.setDisabled(not p.is_available())
2462 cb.setChecked(p.is_enabled())
2463 grid.addWidget(cb, i, 0)
2464 if p.requires_settings():
2465 w = p.settings_widget(self)
2466 w.setEnabled( p.is_enabled() )
2467 grid.addWidget(w, i, 1)
2470 cb.clicked.connect(mk_toggle(cb,p,w))
2471 grid.addWidget(HelpButton(p.description()), i, 2)
2473 print_msg(_("Error: cannot display plugin"), p)
2474 traceback.print_exc(file=sys.stdout)
2475 grid.setRowStretch(i+1,1)
2477 vbox.addLayout(close_button(d))
2482 def show_account_details(self, k):
2483 account = self.wallet.accounts[k]
2486 d.setWindowTitle(_('Account Details'))
2489 vbox = QVBoxLayout(d)
2490 name = self.wallet.get_account_name(k)
2491 label = QLabel('Name: ' + name)
2492 vbox.addWidget(label)
2494 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2496 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2498 vbox.addWidget(QLabel(_('Master Public Key:')))
2501 text.setReadOnly(True)
2502 text.setMaximumHeight(170)
2503 vbox.addWidget(text)
2505 mpk_text = '\n'.join( account.get_master_pubkeys() )
2506 text.setText(mpk_text)
2508 vbox.addLayout(close_button(d))