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
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
67 # status of payment requests
70 PR_SENT = 2 # sent but not propagated
71 PR_PAID = 3 # send and propagated
72 PR_ERROR = 4 # could not parse
75 from electrum import ELECTRUM_VERSION
90 class StatusBarButton(QPushButton):
91 def __init__(self, icon, tooltip, func):
92 QPushButton.__init__(self, icon, '')
93 self.setToolTip(tooltip)
95 self.setMaximumWidth(25)
96 self.clicked.connect(func)
98 self.setIconSize(QSize(25,25))
100 def keyPressEvent(self, e):
101 if e.key() == QtCore.Qt.Key_Return:
113 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
115 class ElectrumWindow(QMainWindow):
119 def __init__(self, config, network, gui_object):
120 QMainWindow.__init__(self)
123 self.network = network
124 self.gui_object = gui_object
125 self.tray = gui_object.tray
126 self.go_lite = gui_object.go_lite
129 self.create_status_bar()
130 self.need_update = threading.Event()
132 self.decimal_point = config.get('decimal_point', 5)
133 self.num_zeros = int(config.get('num_zeros',0))
136 set_language(config.get('language'))
138 self.funds_error = False
139 self.completions = QStringListModel()
141 self.tabs = tabs = QTabWidget(self)
142 self.column_widths = self.config.get("column_widths_2", default_column_widths )
143 tabs.addTab(self.create_history_tab(), _('History') )
144 tabs.addTab(self.create_send_tab(), _('Send') )
145 tabs.addTab(self.create_receive_tab(), _('Receive') )
146 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
147 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
148 tabs.addTab(self.create_console_tab(), _('Console') )
149 tabs.setMinimumSize(600, 400)
150 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
151 self.setCentralWidget(tabs)
153 g = self.config.get("winpos-qt",[100, 100, 840, 400])
154 self.setGeometry(g[0], g[1], g[2], g[3])
155 if self.config.get("is_maximized"):
158 self.setWindowIcon(QIcon(":icons/electrum.png"))
161 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
163 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
164 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
165 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
167 for i in range(tabs.count()):
168 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
170 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
171 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
172 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
173 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
174 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
176 self.history_list.setFocus(True)
180 self.network.register_callback('updated', lambda: self.need_update.set())
181 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
182 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
184 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
186 # set initial message
187 self.console.showMessage(self.network.banner)
192 def update_account_selector(self):
194 accounts = self.wallet.get_account_names()
195 self.account_selector.clear()
196 if len(accounts) > 1:
197 self.account_selector.addItems([_("All accounts")] + accounts.values())
198 self.account_selector.setCurrentIndex(0)
199 self.account_selector.show()
201 self.account_selector.hide()
204 def load_wallet(self, wallet):
208 self.update_wallet_format()
210 self.invoices = self.wallet.storage.get('invoices', {})
211 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
212 self.current_account = self.wallet.storage.get("current_account", None)
213 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
214 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
215 self.setWindowTitle( title )
217 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
218 self.notify_transactions()
219 self.update_account_selector()
221 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
222 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
223 self.password_menu.setEnabled(not self.wallet.is_watching_only())
224 self.seed_menu.setEnabled(self.wallet.has_seed())
225 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
226 self.import_menu.setEnabled(self.wallet.can_import())
228 self.update_lock_icon()
229 self.update_buttons_on_seed()
230 self.update_console()
232 run_hook('load_wallet', wallet)
235 def update_wallet_format(self):
236 # convert old-format imported keys
237 if self.wallet.imported_keys:
238 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
240 self.wallet.convert_imported_keys(password)
242 self.show_message("error")
245 def open_wallet(self):
246 wallet_folder = self.wallet.storage.path
247 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
251 storage = WalletStorage({'wallet_path': filename})
252 if not storage.file_exists:
253 self.show_message("file not found "+ filename)
256 self.wallet.stop_threads()
259 wallet = Wallet(storage)
260 wallet.start_threads(self.network)
262 self.load_wallet(wallet)
266 def backup_wallet(self):
268 path = self.wallet.storage.path
269 wallet_folder = os.path.dirname(path)
270 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
274 new_path = os.path.join(wallet_folder, filename)
277 shutil.copy2(path, new_path)
278 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
279 except (IOError, os.error), reason:
280 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
283 def new_wallet(self):
286 wallet_folder = os.path.dirname(self.wallet.storage.path)
287 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
290 filename = os.path.join(wallet_folder, filename)
292 storage = WalletStorage({'wallet_path': filename})
293 if storage.file_exists:
294 QMessageBox.critical(None, "Error", _("File exists"))
297 wizard = installwizard.InstallWizard(self.config, self.network, storage)
298 wallet = wizard.run('new')
300 self.load_wallet(wallet)
304 def init_menubar(self):
307 file_menu = menubar.addMenu(_("&File"))
308 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
309 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
310 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
311 file_menu.addAction(_("&Quit"), self.close)
313 wallet_menu = menubar.addMenu(_("&Wallet"))
314 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
315 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
317 wallet_menu.addSeparator()
319 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
320 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
321 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
323 wallet_menu.addSeparator()
324 labels_menu = wallet_menu.addMenu(_("&Labels"))
325 labels_menu.addAction(_("&Import"), self.do_import_labels)
326 labels_menu.addAction(_("&Export"), self.do_export_labels)
328 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
329 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
330 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
331 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
332 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
334 tools_menu = menubar.addMenu(_("&Tools"))
336 # Settings / Preferences are all reserved keywords in OSX using this as work around
337 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
338 tools_menu.addAction(_("&Network"), self.run_network_dialog)
339 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
340 tools_menu.addSeparator()
341 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
342 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
343 tools_menu.addSeparator()
345 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
346 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
347 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
349 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
350 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
351 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
352 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
353 self.raw_transaction_menu = raw_transaction_menu
355 help_menu = menubar.addMenu(_("&Help"))
356 help_menu.addAction(_("&About"), self.show_about)
357 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
358 help_menu.addSeparator()
359 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
360 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
362 self.setMenuBar(menubar)
364 def show_about(self):
365 QMessageBox.about(self, "Electrum",
366 _("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."))
368 def show_report_bug(self):
369 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
370 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
373 def notify_transactions(self):
374 if not self.network or not self.network.is_connected():
377 print_error("Notifying GUI")
378 if len(self.network.pending_transactions_for_notifications) > 0:
379 # Combine the transactions if there are more then three
380 tx_amount = len(self.network.pending_transactions_for_notifications)
383 for tx in self.network.pending_transactions_for_notifications:
384 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
388 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
389 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
391 self.network.pending_transactions_for_notifications = []
393 for tx in self.network.pending_transactions_for_notifications:
395 self.network.pending_transactions_for_notifications.remove(tx)
396 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
398 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
400 def notify(self, message):
401 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
405 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
406 def getOpenFileName(self, title, filter = ""):
407 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
408 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
409 if fileName and directory != os.path.dirname(fileName):
410 self.config.set_key('io_dir', os.path.dirname(fileName), True)
413 def getSaveFileName(self, title, filename, filter = ""):
414 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
415 path = os.path.join( directory, filename )
416 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
417 if fileName and directory != os.path.dirname(fileName):
418 self.config.set_key('io_dir', os.path.dirname(fileName), True)
422 QMainWindow.close(self)
423 run_hook('close_main_window')
425 def connect_slots(self, sender):
426 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
427 self.previous_payto_e=''
429 def timer_actions(self):
430 if self.need_update.is_set():
432 self.need_update.clear()
433 run_hook('timer_actions')
435 def format_amount(self, x, is_diff=False, whitespaces=False):
436 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
439 def get_decimal_point(self):
440 return self.decimal_point
444 assert self.decimal_point in [5,8]
445 return "BTC" if self.decimal_point == 8 else "mBTC"
448 def update_status(self):
449 if self.network is None or not self.network.is_running():
451 icon = QIcon(":icons/status_disconnected.png")
453 elif self.network.is_connected():
454 if not self.wallet.up_to_date:
455 text = _("Synchronizing...")
456 icon = QIcon(":icons/status_waiting.png")
457 elif self.network.server_lag > 1:
458 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
459 icon = QIcon(":icons/status_lagging.png")
461 c, u = self.wallet.get_account_balance(self.current_account)
462 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
463 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
465 # append fiat balance and price from exchange rate plugin
467 run_hook('get_fiat_status_text', c+u, r)
472 self.tray.setToolTip(text)
473 icon = QIcon(":icons/status_connected.png")
475 text = _("Not connected")
476 icon = QIcon(":icons/status_disconnected.png")
478 self.balance_label.setText(text)
479 self.status_button.setIcon( icon )
482 def update_wallet(self):
484 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
485 self.update_history_tab()
486 self.update_receive_tab()
487 self.update_contacts_tab()
488 self.update_completions()
489 self.update_invoices_tab()
492 def create_history_tab(self):
493 self.history_list = l = MyTreeWidget(self)
495 for i,width in enumerate(self.column_widths['history']):
496 l.setColumnWidth(i, width)
497 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
498 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
499 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
501 l.customContextMenuRequested.connect(self.create_history_menu)
505 def create_history_menu(self, position):
506 self.history_list.selectedIndexes()
507 item = self.history_list.currentItem()
508 be = self.config.get('block_explorer', 'Blockchain.info')
509 if be == 'Blockchain.info':
510 block_explorer = 'https://blockchain.info/tx/'
511 elif be == 'Blockr.io':
512 block_explorer = 'https://blockr.io/tx/info/'
513 elif be == 'Insight.is':
514 block_explorer = 'http://live.insight.is/tx/'
516 tx_hash = str(item.data(0, Qt.UserRole).toString())
517 if not tx_hash: return
519 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
520 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
521 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
522 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
523 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
526 def show_transaction(self, tx):
527 import transaction_dialog
528 d = transaction_dialog.TxDialog(tx, self)
531 def tx_label_clicked(self, item, column):
532 if column==2 and item.isSelected():
534 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
535 self.history_list.editItem( item, column )
536 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
539 def tx_label_changed(self, item, column):
543 tx_hash = str(item.data(0, Qt.UserRole).toString())
544 tx = self.wallet.transactions.get(tx_hash)
545 text = unicode( item.text(2) )
546 self.wallet.set_label(tx_hash, text)
548 item.setForeground(2, QBrush(QColor('black')))
550 text = self.wallet.get_default_label(tx_hash)
551 item.setText(2, text)
552 item.setForeground(2, QBrush(QColor('gray')))
556 def edit_label(self, is_recv):
557 l = self.receive_list if is_recv else self.contacts_list
558 item = l.currentItem()
559 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
560 l.editItem( item, 1 )
561 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
565 def address_label_clicked(self, item, column, l, column_addr, column_label):
566 if column == column_label and item.isSelected():
567 is_editable = item.data(0, 32).toBool()
570 addr = unicode( item.text(column_addr) )
571 label = unicode( item.text(column_label) )
572 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
573 l.editItem( item, column )
574 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
577 def address_label_changed(self, item, column, l, column_addr, column_label):
578 if column == column_label:
579 addr = unicode( item.text(column_addr) )
580 text = unicode( item.text(column_label) )
581 is_editable = item.data(0, 32).toBool()
585 changed = self.wallet.set_label(addr, text)
587 self.update_history_tab()
588 self.update_completions()
590 self.current_item_changed(item)
592 run_hook('item_changed', item, column)
595 def current_item_changed(self, a):
596 run_hook('current_item_changed', a)
600 def update_history_tab(self):
602 self.history_list.clear()
603 for item in self.wallet.get_tx_history(self.current_account):
604 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
605 time_str = _("unknown")
608 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
610 time_str = _("error")
613 time_str = 'unverified'
614 icon = QIcon(":icons/unconfirmed.png")
617 icon = QIcon(":icons/unconfirmed.png")
619 icon = QIcon(":icons/clock%d.png"%conf)
621 icon = QIcon(":icons/confirmed.png")
623 if value is not None:
624 v_str = self.format_amount(value, True, whitespaces=True)
628 balance_str = self.format_amount(balance, whitespaces=True)
631 label, is_default_label = self.wallet.get_label(tx_hash)
633 label = _('Pruned transaction outputs')
634 is_default_label = False
636 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
637 item.setFont(2, QFont(MONOSPACE_FONT))
638 item.setFont(3, QFont(MONOSPACE_FONT))
639 item.setFont(4, QFont(MONOSPACE_FONT))
641 item.setForeground(3, QBrush(QColor("#BC1E1E")))
643 item.setData(0, Qt.UserRole, tx_hash)
644 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
646 item.setForeground(2, QBrush(QColor('grey')))
648 item.setIcon(0, icon)
649 self.history_list.insertTopLevelItem(0,item)
652 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
653 run_hook('history_tab_update')
656 def create_send_tab(self):
659 self.send_grid = grid = QGridLayout(w)
661 grid.setColumnMinimumWidth(3,300)
662 grid.setColumnStretch(5,1)
663 grid.setRowStretch(8, 1)
665 from paytoedit import PayToEdit
666 self.amount_e = BTCAmountEdit(self.get_decimal_point)
667 self.payto_e = PayToEdit(self.amount_e)
668 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)'))
669 grid.addWidget(QLabel(_('Pay to')), 1, 0)
670 grid.addWidget(self.payto_e, 1, 1, 1, 3)
671 grid.addWidget(self.payto_help, 1, 4)
673 completer = QCompleter()
674 completer.setCaseSensitivity(False)
675 self.payto_e.setCompleter(completer)
676 completer.setModel(self.completions)
678 self.message_e = MyLineEdit()
679 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.'))
680 grid.addWidget(QLabel(_('Description')), 2, 0)
681 grid.addWidget(self.message_e, 2, 1, 1, 3)
682 grid.addWidget(self.message_help, 2, 4)
684 self.from_label = QLabel(_('From'))
685 grid.addWidget(self.from_label, 3, 0)
686 self.from_list = MyTreeWidget(self)
687 self.from_list.setColumnCount(2)
688 self.from_list.setColumnWidth(0, 350)
689 self.from_list.setColumnWidth(1, 50)
690 self.from_list.setHeaderHidden(True)
691 self.from_list.setMaximumHeight(80)
692 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
693 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
694 grid.addWidget(self.from_list, 3, 1, 1, 3)
695 self.set_pay_from([])
697 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
698 + _('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.') \
699 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
700 grid.addWidget(QLabel(_('Amount')), 4, 0)
701 grid.addWidget(self.amount_e, 4, 1, 1, 2)
702 grid.addWidget(self.amount_help, 4, 3)
704 self.fee_e = BTCAmountEdit(self.get_decimal_point)
705 grid.addWidget(QLabel(_('Fee')), 5, 0)
706 grid.addWidget(self.fee_e, 5, 1, 1, 2)
707 grid.addWidget(HelpButton(
708 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
709 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
710 + _('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)
712 self.send_button = EnterButton(_("Send"), self.do_send)
713 grid.addWidget(self.send_button, 6, 1)
715 b = EnterButton(_("Clear"), self.do_clear)
716 grid.addWidget(b, 6, 2)
718 self.payto_sig = QLabel('')
719 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
721 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
722 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
725 def entry_changed( is_fee ):
726 self.funds_error = False
728 if self.amount_e.is_shortcut:
729 self.amount_e.is_shortcut = False
730 sendable = self.get_sendable_balance()
731 # there is only one output because we are completely spending inputs
732 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
733 fee = self.wallet.estimated_fee(inputs, 1)
735 self.amount_e.setAmount(amount)
736 self.fee_e.setAmount(fee)
739 amount = self.amount_e.get_amount()
740 fee = self.fee_e.get_amount()
742 if not is_fee: fee = None
745 # assume that there will be 2 outputs (one for change)
746 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
748 self.fee_e.setAmount(fee)
751 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
755 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
756 self.funds_error = True
757 text = _( "Not enough funds" )
758 c, u = self.wallet.get_frozen_balance()
759 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
761 self.statusBar().showMessage(text)
762 self.amount_e.setPalette(palette)
763 self.fee_e.setPalette(palette)
765 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
766 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
768 run_hook('create_send_tab', grid)
771 def from_list_delete(self, item):
772 i = self.from_list.indexOfTopLevelItem(item)
774 self.redraw_from_list()
776 def from_list_menu(self, position):
777 item = self.from_list.itemAt(position)
779 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
780 menu.exec_(self.from_list.viewport().mapToGlobal(position))
782 def set_pay_from(self, domain = None):
783 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
784 self.redraw_from_list()
786 def redraw_from_list(self):
787 self.from_list.clear()
788 self.from_label.setHidden(len(self.pay_from) == 0)
789 self.from_list.setHidden(len(self.pay_from) == 0)
792 h = x.get('prevout_hash')
793 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
795 for item in self.pay_from:
796 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
798 def update_completions(self):
800 for addr,label in self.wallet.labels.items():
801 if addr in self.wallet.addressbook:
802 l.append( label + ' <' + addr + '>')
804 run_hook('update_completions', l)
805 self.completions.setStringList(l)
809 return lambda s, *args: s.do_protect(func, args)
812 def read_send_tab(self):
814 if self.payment_request and self.payment_request.has_expired():
815 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
818 label = unicode( self.message_e.text() )
820 if self.payment_request:
821 outputs = self.payment_request.get_outputs()
823 outputs = self.payto_e.get_outputs()
826 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
829 for addr, x in outputs:
830 if addr is None or not bitcoin.is_address(addr):
831 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
834 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
837 amount = sum(map(lambda x:x[1], outputs))
839 fee = self.fee_e.get_amount()
841 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
844 confirm_amount = self.config.get('confirm_amount', 100000000)
845 if amount >= confirm_amount:
846 o = '\n'.join(map(lambda x:x[0], outputs))
847 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
850 confirm_fee = self.config.get('confirm_fee', 100000)
851 if fee >= confirm_fee:
852 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()}):
855 coins = self.get_coins()
856 return outputs, fee, label, coins
860 r = self.read_send_tab()
863 outputs, fee, label, coins = r
864 self.send_tx(outputs, fee, label, coins)
868 def send_tx(self, outputs, fee, label, coins, password):
869 self.send_button.setDisabled(True)
871 # first, create an unsigned tx
873 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
875 except Exception as e:
876 traceback.print_exc(file=sys.stdout)
877 self.show_message(str(e))
878 self.send_button.setDisabled(False)
881 # call hook to see if plugin needs gui interaction
882 run_hook('send_tx', tx)
888 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
889 self.wallet.sign_transaction(tx, keypairs, password)
890 return tx, fee, label
892 def sign_done(tx, fee, label):
894 self.show_message(tx.error)
895 self.send_button.setDisabled(False)
897 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
898 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
899 self.send_button.setDisabled(False)
902 self.wallet.set_label(tx.hash(), label)
904 if not tx.is_complete() or self.config.get('show_before_broadcast'):
905 self.show_transaction(tx)
907 self.send_button.setDisabled(False)
910 self.broadcast_transaction(tx)
912 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
913 self.waiting_dialog.start()
917 def broadcast_transaction(self, tx):
919 def broadcast_thread():
920 pr = self.payment_request
922 return self.wallet.sendtx(tx)
925 self.payment_request = None
926 return False, _("Payment request has expired")
928 status, msg = self.wallet.sendtx(tx)
932 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
933 self.wallet.storage.put('invoices', self.invoices)
934 self.update_invoices_tab()
935 self.payment_request = None
936 refund_address = self.wallet.addresses()[0]
937 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
943 def broadcast_done(status, msg):
945 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
948 QMessageBox.warning(self, _('Error'), msg, _('OK'))
949 self.send_button.setDisabled(False)
951 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
952 self.waiting_dialog.start()
956 def prepare_for_payment_request(self):
957 self.tabs.setCurrentIndex(1)
958 self.payto_e.is_pr = True
959 for e in [self.payto_e, self.amount_e, self.message_e]:
961 for h in [self.payto_help, self.amount_help, self.message_help]:
963 self.payto_e.setText(_("please wait..."))
966 def payment_request_ok(self):
967 pr = self.payment_request
969 if pr_id not in self.invoices:
970 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
971 self.wallet.storage.put('invoices', self.invoices)
972 self.update_invoices_tab()
974 print_error('invoice already in list')
976 status = self.invoices[pr_id][3]
977 if status == PR_PAID:
979 self.show_message("invoice already paid")
980 self.payment_request = None
983 self.payto_help.show()
984 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
986 if not pr.has_expired():
987 self.payto_e.setGreen()
989 self.payto_e.setExpired()
991 self.payto_e.setText(pr.domain)
992 self.amount_e.setText(self.format_amount(pr.get_amount()))
993 self.message_e.setText(pr.get_memo())
995 def payment_request_error(self):
997 self.show_message(self.payment_request.error)
998 self.payment_request = None
1000 def pay_from_URI(self,URI):
1003 address, amount, label, message, request_url = util.parse_URI(URI)
1005 address, amount, label, message, request_url = util.parse_URI(URI)
1006 except Exception as e:
1007 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1010 self.tabs.setCurrentIndex(1)
1014 if self.wallet.labels.get(address) != label:
1015 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1016 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1017 self.wallet.addressbook.append(address)
1018 self.wallet.set_label(address, label)
1020 label = self.wallet.labels.get(address)
1022 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1024 self.message_e.setText(message)
1026 self.amount_e.setAmount(amount)
1029 from electrum import paymentrequest
1030 def payment_request():
1031 self.payment_request = paymentrequest.PaymentRequest(self.config)
1032 self.payment_request.read(request_url)
1033 if self.payment_request.verify():
1034 self.emit(SIGNAL('payment_request_ok'))
1036 self.emit(SIGNAL('payment_request_error'))
1038 self.pr_thread = threading.Thread(target=payment_request).start()
1039 self.prepare_for_payment_request()
1044 self.payto_e.is_pr = False
1045 self.payto_sig.setVisible(False)
1046 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1050 for h in [self.payto_help, self.amount_help, self.message_help]:
1053 self.payto_help.set_alt(None)
1054 self.set_pay_from([])
1055 self.update_status()
1059 def set_addrs_frozen(self,addrs,freeze):
1061 if not addr: continue
1062 if addr in self.wallet.frozen_addresses and not freeze:
1063 self.wallet.unfreeze(addr)
1064 elif addr not in self.wallet.frozen_addresses and freeze:
1065 self.wallet.freeze(addr)
1066 self.update_receive_tab()
1070 def create_list_tab(self, headers):
1071 "generic tab creation method"
1072 l = MyTreeWidget(self)
1073 l.setColumnCount( len(headers) )
1074 l.setHeaderLabels( headers )
1077 vbox = QVBoxLayout()
1084 vbox.addWidget(buttons)
1089 def create_receive_tab(self):
1090 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1091 for i,width in enumerate(self.column_widths['receive']):
1092 l.setColumnWidth(i, width)
1093 l.setContextMenuPolicy(Qt.CustomContextMenu)
1094 l.customContextMenuRequested.connect(self.create_receive_menu)
1095 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1096 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1097 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1098 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1099 self.receive_list = l
1105 def save_column_widths(self):
1106 self.column_widths["receive"] = []
1107 for i in range(self.receive_list.columnCount() -1):
1108 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1110 self.column_widths["history"] = []
1111 for i in range(self.history_list.columnCount() - 1):
1112 self.column_widths["history"].append(self.history_list.columnWidth(i))
1114 self.column_widths["contacts"] = []
1115 for i in range(self.contacts_list.columnCount() - 1):
1116 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1118 self.config.set_key("column_widths_2", self.column_widths, True)
1121 def create_contacts_tab(self):
1122 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1123 l.setContextMenuPolicy(Qt.CustomContextMenu)
1124 l.customContextMenuRequested.connect(self.create_contact_menu)
1125 for i,width in enumerate(self.column_widths['contacts']):
1126 l.setColumnWidth(i, width)
1127 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1128 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1129 self.contacts_list = l
1133 def create_invoices_tab(self):
1134 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1136 h.setStretchLastSection(False)
1137 h.setResizeMode(1, QHeaderView.Stretch)
1138 l.setContextMenuPolicy(Qt.CustomContextMenu)
1139 l.customContextMenuRequested.connect(self.create_invoice_menu)
1140 self.invoices_list = l
1143 def update_invoices_tab(self):
1144 invoices = self.wallet.storage.get('invoices', {})
1145 l = self.invoices_list
1147 for key, value in invoices.items():
1149 domain, memo, amount, expiration_date, status, tx_hash = value
1153 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1155 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1156 l.addTopLevelItem(item)
1158 l.setCurrentItem(l.topLevelItem(0))
1162 def delete_imported_key(self, addr):
1163 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1164 self.wallet.delete_imported_key(addr)
1165 self.update_receive_tab()
1166 self.update_history_tab()
1168 def edit_account_label(self, k):
1169 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1171 label = unicode(text)
1172 self.wallet.set_label(k,label)
1173 self.update_receive_tab()
1175 def account_set_expanded(self, item, k, b):
1177 self.accounts_expanded[k] = b
1179 def create_account_menu(self, position, k, item):
1181 if item.isExpanded():
1182 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1184 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1185 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1186 if self.wallet.seed_version > 4:
1187 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1188 if self.wallet.account_is_pending(k):
1189 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1190 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1192 def delete_pending_account(self, k):
1193 self.wallet.delete_pending_account(k)
1194 self.update_receive_tab()
1196 def create_receive_menu(self, position):
1197 # fixme: this function apparently has a side effect.
1198 # if it is not called the menu pops up several times
1199 #self.receive_list.selectedIndexes()
1201 selected = self.receive_list.selectedItems()
1202 multi_select = len(selected) > 1
1203 addrs = [unicode(item.text(0)) for item in selected]
1204 if not multi_select:
1205 item = self.receive_list.itemAt(position)
1209 if not is_valid(addr):
1210 k = str(item.data(0,32).toString())
1212 self.create_account_menu(position, k, item)
1214 item.setExpanded(not item.isExpanded())
1218 if not multi_select:
1219 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1220 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1221 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1222 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1223 if not self.wallet.is_watching_only():
1224 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1225 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1226 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1227 if self.wallet.is_imported(addr):
1228 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1230 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1231 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1232 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1233 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1235 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1236 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1238 run_hook('receive_menu', menu, addrs)
1239 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1242 def get_sendable_balance(self):
1243 return sum(map(lambda x:x['value'], self.get_coins()))
1246 def get_coins(self):
1248 return self.pay_from
1250 domain = self.wallet.get_account_addresses(self.current_account)
1251 for i in self.wallet.frozen_addresses:
1252 if i in domain: domain.remove(i)
1253 return self.wallet.get_unspent_coins(domain)
1256 def send_from_addresses(self, addrs):
1257 self.set_pay_from( addrs )
1258 self.tabs.setCurrentIndex(1)
1261 def payto(self, addr):
1263 label = self.wallet.labels.get(addr)
1264 m_addr = label + ' <' + addr + '>' if label else addr
1265 self.tabs.setCurrentIndex(1)
1266 self.payto_e.setText(m_addr)
1267 self.amount_e.setFocus()
1270 def delete_contact(self, x):
1271 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1272 self.wallet.delete_contact(x)
1273 self.wallet.set_label(x, None)
1274 self.update_history_tab()
1275 self.update_contacts_tab()
1276 self.update_completions()
1279 def create_contact_menu(self, position):
1280 item = self.contacts_list.itemAt(position)
1283 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1285 addr = unicode(item.text(0))
1286 label = unicode(item.text(1))
1287 is_editable = item.data(0,32).toBool()
1288 payto_addr = item.data(0,33).toString()
1289 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1290 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1291 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1293 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1294 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1296 run_hook('create_contact_menu', menu, item)
1297 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1299 def delete_invoice(self, key):
1300 self.invoices.pop(key)
1301 self.wallet.storage.put('invoices', self.invoices)
1302 self.update_invoices_tab()
1304 def show_invoice(self, key):
1305 from electrum.paymentrequest import PaymentRequest
1306 domain, memo, value, status, tx_hash = self.invoices[key]
1307 pr = PaymentRequest(self.config)
1311 self.show_pr_details(pr)
1313 def show_pr_details(self, pr):
1314 msg = 'Domain: ' + pr.domain
1315 msg += '\nStatus: ' + pr.get_status()
1316 msg += '\nMemo: ' + pr.get_memo()
1317 msg += '\nPayment URL: ' + pr.payment_url
1318 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1319 QMessageBox.information(self, 'Invoice', msg , 'OK')
1321 def do_pay_invoice(self, key):
1322 from electrum.paymentrequest import PaymentRequest
1323 domain, memo, value, status, tx_hash = self.invoices[key]
1324 pr = PaymentRequest(self.config)
1327 self.payment_request = pr
1328 self.prepare_for_payment_request()
1330 self.payment_request_ok()
1332 self.payment_request_error()
1335 def create_invoice_menu(self, position):
1336 item = self.invoices_list.itemAt(position)
1339 k = self.invoices_list.indexOfTopLevelItem(item)
1340 key = self.invoices.keys()[k]
1341 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1343 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1344 if status == PR_UNPAID:
1345 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1346 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1347 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1350 def update_receive_item(self, item):
1351 item.setFont(0, QFont(MONOSPACE_FONT))
1352 address = str(item.data(0,0).toString())
1353 label = self.wallet.labels.get(address,'')
1354 item.setData(1,0,label)
1355 item.setData(0,32, True) # is editable
1357 run_hook('update_receive_item', address, item)
1359 if not self.wallet.is_mine(address): return
1361 c, u = self.wallet.get_addr_balance(address)
1362 balance = self.format_amount(c + u)
1363 item.setData(2,0,balance)
1365 if address in self.wallet.frozen_addresses:
1366 item.setBackgroundColor(0, QColor('lightblue'))
1369 def update_receive_tab(self):
1370 l = self.receive_list
1371 # extend the syntax for consistency
1372 l.addChild = l.addTopLevelItem
1373 l.insertChild = l.insertTopLevelItem
1377 accounts = self.wallet.get_accounts()
1378 if self.current_account is None:
1379 account_items = sorted(accounts.items())
1381 account_items = [(self.current_account, accounts.get(self.current_account))]
1384 for k, account in account_items:
1386 if len(accounts) > 1:
1387 name = self.wallet.get_account_name(k)
1388 c,u = self.wallet.get_account_balance(k)
1389 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1390 l.addTopLevelItem(account_item)
1391 account_item.setExpanded(self.accounts_expanded.get(k, True))
1392 account_item.setData(0, 32, k)
1396 sequences = [0,1] if account.has_change() else [0]
1397 for is_change in sequences:
1398 if len(sequences) > 1:
1399 name = _("Receiving") if not is_change else _("Change")
1400 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1401 account_item.addChild(seq_item)
1403 seq_item.setExpanded(True)
1405 seq_item = account_item
1407 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1413 for address in account.get_addresses(is_change):
1415 num, is_used = self.wallet.is_used(address)
1418 if gap > self.wallet.gap_limit:
1423 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1424 self.update_receive_item(item)
1426 item.setBackgroundColor(1, QColor('red'))
1430 seq_item.insertChild(0,used_item)
1432 used_item.addChild(item)
1434 seq_item.addChild(item)
1436 # we use column 1 because column 0 may be hidden
1437 l.setCurrentItem(l.topLevelItem(0),1)
1440 def update_contacts_tab(self):
1441 l = self.contacts_list
1444 for address in self.wallet.addressbook:
1445 label = self.wallet.labels.get(address,'')
1446 n = self.wallet.get_num_tx(address)
1447 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1448 item.setFont(0, QFont(MONOSPACE_FONT))
1449 # 32 = label can be edited (bool)
1450 item.setData(0,32, True)
1452 item.setData(0,33, address)
1453 l.addTopLevelItem(item)
1455 run_hook('update_contacts_tab', l)
1456 l.setCurrentItem(l.topLevelItem(0))
1460 def create_console_tab(self):
1461 from console import Console
1462 self.console = console = Console()
1466 def update_console(self):
1467 console = self.console
1468 console.history = self.config.get("console-history",[])
1469 console.history_index = len(console.history)
1471 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1472 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1474 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1476 def mkfunc(f, method):
1477 return lambda *args: apply( f, (method, args, self.password_dialog ))
1479 if m[0]=='_' or m in ['network','wallet']: continue
1480 methods[m] = mkfunc(c._run, m)
1482 console.updateNamespace(methods)
1485 def change_account(self,s):
1486 if s == _("All accounts"):
1487 self.current_account = None
1489 accounts = self.wallet.get_account_names()
1490 for k, v in accounts.items():
1492 self.current_account = k
1493 self.update_history_tab()
1494 self.update_status()
1495 self.update_receive_tab()
1497 def create_status_bar(self):
1500 sb.setFixedHeight(35)
1501 qtVersion = qVersion()
1503 self.balance_label = QLabel("")
1504 sb.addWidget(self.balance_label)
1506 from version_getter import UpdateLabel
1507 self.updatelabel = UpdateLabel(self.config, sb)
1509 self.account_selector = QComboBox()
1510 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1511 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1512 sb.addPermanentWidget(self.account_selector)
1514 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1515 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1517 self.lock_icon = QIcon()
1518 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1519 sb.addPermanentWidget( self.password_button )
1521 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1522 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1523 sb.addPermanentWidget( self.seed_button )
1524 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1525 sb.addPermanentWidget( self.status_button )
1527 run_hook('create_status_bar', (sb,))
1529 self.setStatusBar(sb)
1532 def update_lock_icon(self):
1533 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1534 self.password_button.setIcon( icon )
1537 def update_buttons_on_seed(self):
1538 if self.wallet.has_seed():
1539 self.seed_button.show()
1541 self.seed_button.hide()
1543 if not self.wallet.is_watching_only():
1544 self.password_button.show()
1545 self.send_button.setText(_("Send"))
1547 self.password_button.hide()
1548 self.send_button.setText(_("Create unsigned transaction"))
1551 def change_password_dialog(self):
1552 from password_dialog import PasswordDialog
1553 d = PasswordDialog(self.wallet, self)
1555 self.update_lock_icon()
1558 def new_contact_dialog(self):
1561 d.setWindowTitle(_("New Contact"))
1562 vbox = QVBoxLayout(d)
1563 vbox.addWidget(QLabel(_('New Contact')+':'))
1565 grid = QGridLayout()
1568 grid.addWidget(QLabel(_("Address")), 1, 0)
1569 grid.addWidget(line1, 1, 1)
1570 grid.addWidget(QLabel(_("Name")), 2, 0)
1571 grid.addWidget(line2, 2, 1)
1573 vbox.addLayout(grid)
1574 vbox.addLayout(ok_cancel_buttons(d))
1579 address = str(line1.text())
1580 label = unicode(line2.text())
1582 if not is_valid(address):
1583 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1586 self.wallet.add_contact(address)
1588 self.wallet.set_label(address, label)
1590 self.update_contacts_tab()
1591 self.update_history_tab()
1592 self.update_completions()
1593 self.tabs.setCurrentIndex(3)
1597 def new_account_dialog(self, password):
1599 dialog = QDialog(self)
1601 dialog.setWindowTitle(_("New Account"))
1603 vbox = QVBoxLayout()
1604 vbox.addWidget(QLabel(_('Account name')+':'))
1607 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1608 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1613 vbox.addLayout(ok_cancel_buttons(dialog))
1614 dialog.setLayout(vbox)
1618 name = str(e.text())
1621 self.wallet.create_pending_account(name, password)
1622 self.update_receive_tab()
1623 self.tabs.setCurrentIndex(2)
1628 def show_master_public_keys(self):
1630 dialog = QDialog(self)
1632 dialog.setWindowTitle(_("Master Public Keys"))
1634 main_layout = QGridLayout()
1635 mpk_dict = self.wallet.get_master_public_keys()
1637 for key, value in mpk_dict.items():
1638 main_layout.addWidget(QLabel(key), i, 0)
1639 mpk_text = QTextEdit()
1640 mpk_text.setReadOnly(True)
1641 mpk_text.setMaximumHeight(170)
1642 mpk_text.setText(value)
1643 main_layout.addWidget(mpk_text, i + 1, 0)
1646 vbox = QVBoxLayout()
1647 vbox.addLayout(main_layout)
1648 vbox.addLayout(close_button(dialog))
1650 dialog.setLayout(vbox)
1655 def show_seed_dialog(self, password):
1656 if not self.wallet.has_seed():
1657 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1661 mnemonic = self.wallet.get_mnemonic(password)
1663 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1665 from seed_dialog import SeedDialog
1666 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1671 def show_qrcode(self, data, title = _("QR code")):
1675 d.setWindowTitle(title)
1676 d.setMinimumSize(270, 300)
1677 vbox = QVBoxLayout()
1678 qrw = QRCodeWidget(data)
1679 vbox.addWidget(qrw, 1)
1680 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1681 hbox = QHBoxLayout()
1684 filename = os.path.join(self.config.path, "qrcode.bmp")
1687 bmp.save_qrcode(qrw.qr, filename)
1688 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1690 def copy_to_clipboard():
1691 bmp.save_qrcode(qrw.qr, filename)
1692 self.app.clipboard().setImage(QImage(filename))
1693 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1695 b = QPushButton(_("Copy"))
1697 b.clicked.connect(copy_to_clipboard)
1699 b = QPushButton(_("Save"))
1701 b.clicked.connect(print_qr)
1703 b = QPushButton(_("Close"))
1705 b.clicked.connect(d.accept)
1708 vbox.addLayout(hbox)
1713 def do_protect(self, func, args):
1714 if self.wallet.use_encryption:
1715 password = self.password_dialog()
1721 if args != (False,):
1722 args = (self,) + args + (password,)
1724 args = (self,password)
1728 def show_public_keys(self, address):
1729 if not address: return
1731 pubkey_list = self.wallet.get_public_keys(address)
1732 except Exception as e:
1733 traceback.print_exc(file=sys.stdout)
1734 self.show_message(str(e))
1738 d.setMinimumSize(600, 200)
1740 vbox = QVBoxLayout()
1741 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1742 vbox.addWidget( QLabel(_("Public key") + ':'))
1744 keys.setReadOnly(True)
1745 keys.setText('\n'.join(pubkey_list))
1746 vbox.addWidget(keys)
1747 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1748 vbox.addLayout(close_button(d))
1753 def show_private_key(self, address, password):
1754 if not address: return
1756 pk_list = self.wallet.get_private_key(address, password)
1757 except Exception as e:
1758 traceback.print_exc(file=sys.stdout)
1759 self.show_message(str(e))
1763 d.setMinimumSize(600, 200)
1765 vbox = QVBoxLayout()
1766 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1767 vbox.addWidget( QLabel(_("Private key") + ':'))
1769 keys.setReadOnly(True)
1770 keys.setText('\n'.join(pk_list))
1771 vbox.addWidget(keys)
1772 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1773 vbox.addLayout(close_button(d))
1779 def do_sign(self, address, message, signature, password):
1780 message = unicode(message.toPlainText())
1781 message = message.encode('utf-8')
1783 sig = self.wallet.sign_message(str(address.text()), message, password)
1784 signature.setText(sig)
1785 except Exception as e:
1786 self.show_message(str(e))
1788 def do_verify(self, address, message, signature):
1789 message = unicode(message.toPlainText())
1790 message = message.encode('utf-8')
1791 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1792 self.show_message(_("Signature verified"))
1794 self.show_message(_("Error: wrong signature"))
1797 def sign_verify_message(self, address=''):
1800 d.setWindowTitle(_('Sign/verify Message'))
1801 d.setMinimumSize(410, 290)
1803 layout = QGridLayout(d)
1805 message_e = QTextEdit()
1806 layout.addWidget(QLabel(_('Message')), 1, 0)
1807 layout.addWidget(message_e, 1, 1)
1808 layout.setRowStretch(2,3)
1810 address_e = QLineEdit()
1811 address_e.setText(address)
1812 layout.addWidget(QLabel(_('Address')), 2, 0)
1813 layout.addWidget(address_e, 2, 1)
1815 signature_e = QTextEdit()
1816 layout.addWidget(QLabel(_('Signature')), 3, 0)
1817 layout.addWidget(signature_e, 3, 1)
1818 layout.setRowStretch(3,1)
1820 hbox = QHBoxLayout()
1822 b = QPushButton(_("Sign"))
1823 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1826 b = QPushButton(_("Verify"))
1827 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1830 b = QPushButton(_("Close"))
1831 b.clicked.connect(d.accept)
1833 layout.addLayout(hbox, 4, 1)
1838 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1840 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1841 message_e.setText(decrypted)
1842 except Exception as e:
1843 self.show_message(str(e))
1846 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1847 message = unicode(message_e.toPlainText())
1848 message = message.encode('utf-8')
1850 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1851 encrypted_e.setText(encrypted)
1852 except Exception as e:
1853 self.show_message(str(e))
1857 def encrypt_message(self, address = ''):
1860 d.setWindowTitle(_('Encrypt/decrypt Message'))
1861 d.setMinimumSize(610, 490)
1863 layout = QGridLayout(d)
1865 message_e = QTextEdit()
1866 layout.addWidget(QLabel(_('Message')), 1, 0)
1867 layout.addWidget(message_e, 1, 1)
1868 layout.setRowStretch(2,3)
1870 pubkey_e = QLineEdit()
1872 pubkey = self.wallet.getpubkeys(address)[0]
1873 pubkey_e.setText(pubkey)
1874 layout.addWidget(QLabel(_('Public key')), 2, 0)
1875 layout.addWidget(pubkey_e, 2, 1)
1877 encrypted_e = QTextEdit()
1878 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1879 layout.addWidget(encrypted_e, 3, 1)
1880 layout.setRowStretch(3,1)
1882 hbox = QHBoxLayout()
1883 b = QPushButton(_("Encrypt"))
1884 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1887 b = QPushButton(_("Decrypt"))
1888 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1891 b = QPushButton(_("Close"))
1892 b.clicked.connect(d.accept)
1895 layout.addLayout(hbox, 4, 1)
1899 def question(self, msg):
1900 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1902 def show_message(self, msg):
1903 QMessageBox.information(self, _('Message'), msg, _('OK'))
1905 def password_dialog(self, msg=None):
1908 d.setWindowTitle(_("Enter Password"))
1913 vbox = QVBoxLayout()
1915 msg = _('Please enter your password')
1916 vbox.addWidget(QLabel(msg))
1918 grid = QGridLayout()
1920 grid.addWidget(QLabel(_('Password')), 1, 0)
1921 grid.addWidget(pw, 1, 1)
1922 vbox.addLayout(grid)
1924 vbox.addLayout(ok_cancel_buttons(d))
1927 run_hook('password_dialog', pw, grid, 1)
1928 if not d.exec_(): return
1929 return unicode(pw.text())
1938 def tx_from_text(self, txt):
1939 "json or raw hexadecimal"
1942 tx = Transaction(txt)
1948 tx_dict = json.loads(str(txt))
1949 assert "hex" in tx_dict.keys()
1950 tx = Transaction(tx_dict["hex"])
1951 if tx_dict.has_key("input_info"):
1952 input_info = json.loads(tx_dict['input_info'])
1953 tx.add_input_info(input_info)
1956 traceback.print_exc(file=sys.stdout)
1959 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1963 def read_tx_from_file(self):
1964 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1968 with open(fileName, "r") as f:
1969 file_content = f.read()
1970 except (ValueError, IOError, os.error), reason:
1971 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1973 return self.tx_from_text(file_content)
1977 def sign_raw_transaction(self, tx, input_info, password):
1978 self.wallet.signrawtransaction(tx, input_info, [], password)
1980 def do_process_from_text(self):
1981 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1984 tx = self.tx_from_text(text)
1986 self.show_transaction(tx)
1988 def do_process_from_file(self):
1989 tx = self.read_tx_from_file()
1991 self.show_transaction(tx)
1993 def do_process_from_txid(self):
1994 from electrum import transaction
1995 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1997 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1999 tx = transaction.Transaction(r)
2001 self.show_transaction(tx)
2003 self.show_message("unknown transaction")
2005 def do_process_from_csvReader(self, csvReader):
2010 for position, row in enumerate(csvReader):
2012 if not is_valid(address):
2013 errors.append((position, address))
2015 amount = Decimal(row[1])
2016 amount = int(100000000*amount)
2017 outputs.append((address, amount))
2018 except (ValueError, IOError, os.error), reason:
2019 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2023 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2024 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2028 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2029 except Exception as e:
2030 self.show_message(str(e))
2033 self.show_transaction(tx)
2035 def do_process_from_csv_file(self):
2036 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2040 with open(fileName, "r") as f:
2041 csvReader = csv.reader(f)
2042 self.do_process_from_csvReader(csvReader)
2043 except (ValueError, IOError, os.error), reason:
2044 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2047 def do_process_from_csv_text(self):
2048 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2049 + _("Format: address, amount. One output per line"), _("Load CSV"))
2052 f = StringIO.StringIO(text)
2053 csvReader = csv.reader(f)
2054 self.do_process_from_csvReader(csvReader)
2059 def export_privkeys_dialog(self, password):
2060 if self.wallet.is_watching_only():
2061 self.show_message(_("This is a watching-only wallet"))
2065 d.setWindowTitle(_('Private keys'))
2066 d.setMinimumSize(850, 300)
2067 vbox = QVBoxLayout(d)
2069 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2070 _("Exposing a single private key can compromise your entire wallet!"),
2071 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2072 vbox.addWidget(QLabel(msg))
2078 defaultname = 'electrum-private-keys.csv'
2079 select_msg = _('Select file to export your private keys to')
2080 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2081 vbox.addLayout(hbox)
2083 h, b = ok_cancel_buttons2(d, _('Export'))
2088 addresses = self.wallet.addresses(True)
2090 def privkeys_thread():
2091 for addr in addresses:
2095 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2096 d.emit(SIGNAL('computing_privkeys'))
2097 d.emit(SIGNAL('show_privkeys'))
2099 def show_privkeys():
2100 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2104 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2105 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2106 threading.Thread(target=privkeys_thread).start()
2112 filename = filename_e.text()
2117 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2118 except (IOError, os.error), reason:
2119 export_error_label = _("Electrum was unable to produce a private key-export.")
2120 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2122 except Exception as e:
2123 self.show_message(str(e))
2126 self.show_message(_("Private keys exported."))
2129 def do_export_privkeys(self, fileName, pklist, is_csv):
2130 with open(fileName, "w+") as f:
2132 transaction = csv.writer(f)
2133 transaction.writerow(["address", "private_key"])
2134 for addr, pk in pklist.items():
2135 transaction.writerow(["%34s"%addr,pk])
2138 f.write(json.dumps(pklist, indent = 4))
2141 def do_import_labels(self):
2142 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2143 if not labelsFile: return
2145 f = open(labelsFile, 'r')
2148 for key, value in json.loads(data).items():
2149 self.wallet.set_label(key, value)
2150 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2151 except (IOError, os.error), reason:
2152 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2155 def do_export_labels(self):
2156 labels = self.wallet.labels
2158 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2160 with open(fileName, 'w+') as f:
2161 json.dump(labels, f)
2162 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2163 except (IOError, os.error), reason:
2164 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2167 def export_history_dialog(self):
2170 d.setWindowTitle(_('Export History'))
2171 d.setMinimumSize(400, 200)
2172 vbox = QVBoxLayout(d)
2174 defaultname = os.path.expanduser('~/electrum-history.csv')
2175 select_msg = _('Select file to export your wallet transactions to')
2177 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2178 vbox.addLayout(hbox)
2182 h, b = ok_cancel_buttons2(d, _('Export'))
2187 filename = filename_e.text()
2192 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2193 except (IOError, os.error), reason:
2194 export_error_label = _("Electrum was unable to produce a transaction export.")
2195 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2198 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2201 def do_export_history(self, wallet, fileName, is_csv):
2202 history = wallet.get_tx_history()
2204 for item in history:
2205 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2207 if timestamp is not None:
2209 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2210 except [RuntimeError, TypeError, NameError] as reason:
2211 time_string = "unknown"
2214 time_string = "unknown"
2216 time_string = "pending"
2218 if value is not None:
2219 value_string = format_satoshis(value, True)
2224 fee_string = format_satoshis(fee, True)
2229 label, is_default_label = wallet.get_label(tx_hash)
2230 label = label.encode('utf-8')
2234 balance_string = format_satoshis(balance, False)
2236 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2238 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2240 with open(fileName, "w+") as f:
2242 transaction = csv.writer(f)
2243 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2245 transaction.writerow(line)
2248 f.write(json.dumps(lines, indent = 4))
2251 def sweep_key_dialog(self):
2253 d.setWindowTitle(_('Sweep private keys'))
2254 d.setMinimumSize(600, 300)
2256 vbox = QVBoxLayout(d)
2257 vbox.addWidget(QLabel(_("Enter private keys")))
2259 keys_e = QTextEdit()
2260 keys_e.setTabChangesFocus(True)
2261 vbox.addWidget(keys_e)
2263 h, address_e = address_field(self.wallet.addresses())
2267 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2268 vbox.addLayout(hbox)
2269 button.setEnabled(False)
2272 addr = str(address_e.text())
2273 if bitcoin.is_address(addr):
2277 pk = str(keys_e.toPlainText()).strip()
2278 if Wallet.is_private_key(pk):
2281 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2282 keys_e.textChanged.connect(f)
2283 address_e.textChanged.connect(f)
2287 fee = self.wallet.fee
2288 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2289 self.show_transaction(tx)
2293 def do_import_privkey(self, password):
2294 if not self.wallet.has_imported_keys():
2295 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2296 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2297 + _('Are you sure you understand what you are doing?'), 3, 4)
2300 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2303 text = str(text).split()
2308 addr = self.wallet.import_key(key, password)
2309 except Exception as e:
2315 addrlist.append(addr)
2317 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2319 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2320 self.update_receive_tab()
2321 self.update_history_tab()
2324 def settings_dialog(self):
2326 d.setWindowTitle(_('Electrum Settings'))
2328 vbox = QVBoxLayout()
2329 grid = QGridLayout()
2330 grid.setColumnStretch(0,1)
2332 nz_label = QLabel(_('Display zeros') + ':')
2333 grid.addWidget(nz_label, 0, 0)
2334 nz_e = AmountEdit(None,True)
2335 nz_e.setText("%d"% self.num_zeros)
2336 grid.addWidget(nz_e, 0, 1)
2337 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2338 grid.addWidget(HelpButton(msg), 0, 2)
2339 if not self.config.is_modifiable('num_zeros'):
2340 for w in [nz_e, nz_label]: w.setEnabled(False)
2342 lang_label=QLabel(_('Language') + ':')
2343 grid.addWidget(lang_label, 1, 0)
2344 lang_combo = QComboBox()
2345 from electrum.i18n import languages
2346 lang_combo.addItems(languages.values())
2348 index = languages.keys().index(self.config.get("language",''))
2351 lang_combo.setCurrentIndex(index)
2352 grid.addWidget(lang_combo, 1, 1)
2353 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2354 if not self.config.is_modifiable('language'):
2355 for w in [lang_combo, lang_label]: w.setEnabled(False)
2358 fee_label = QLabel(_('Transaction fee') + ':')
2359 grid.addWidget(fee_label, 2, 0)
2360 fee_e = BTCAmountEdit(self.get_decimal_point)
2361 fee_e.setAmount(self.wallet.fee)
2362 grid.addWidget(fee_e, 2, 1)
2363 msg = _('Fee per kilobyte of transaction.') + '\n' \
2364 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2365 grid.addWidget(HelpButton(msg), 2, 2)
2366 if not self.config.is_modifiable('fee_per_kb'):
2367 for w in [fee_e, fee_label]: w.setEnabled(False)
2369 units = ['BTC', 'mBTC']
2370 unit_label = QLabel(_('Base unit') + ':')
2371 grid.addWidget(unit_label, 3, 0)
2372 unit_combo = QComboBox()
2373 unit_combo.addItems(units)
2374 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2375 grid.addWidget(unit_combo, 3, 1)
2376 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2377 + '\n1BTC=1000mBTC.\n' \
2378 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2380 usechange_cb = QCheckBox(_('Use change addresses'))
2381 usechange_cb.setChecked(self.wallet.use_change)
2382 grid.addWidget(usechange_cb, 4, 0)
2383 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2384 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2386 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2387 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2388 grid.addWidget(block_ex_label, 5, 0)
2389 block_ex_combo = QComboBox()
2390 block_ex_combo.addItems(block_explorers)
2391 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2392 grid.addWidget(block_ex_combo, 5, 1)
2393 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2395 show_tx = self.config.get('show_before_broadcast', False)
2396 showtx_cb = QCheckBox(_('Show before broadcast'))
2397 showtx_cb.setChecked(show_tx)
2398 grid.addWidget(showtx_cb, 6, 0)
2399 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2401 vbox.addLayout(grid)
2403 vbox.addLayout(ok_cancel_buttons(d))
2407 if not d.exec_(): return
2409 fee = fee_e.get_amount()
2411 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2414 self.wallet.set_fee(fee)
2416 nz = unicode(nz_e.text())
2421 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2424 if self.num_zeros != nz:
2426 self.config.set_key('num_zeros', nz, True)
2427 self.update_history_tab()
2428 self.update_receive_tab()
2430 usechange_result = usechange_cb.isChecked()
2431 if self.wallet.use_change != usechange_result:
2432 self.wallet.use_change = usechange_result
2433 self.wallet.storage.put('use_change', self.wallet.use_change)
2435 if showtx_cb.isChecked() != show_tx:
2436 self.config.set_key('show_before_broadcast', not show_tx)
2438 unit_result = units[unit_combo.currentIndex()]
2439 if self.base_unit() != unit_result:
2440 self.decimal_point = 8 if unit_result == 'BTC' else 5
2441 self.config.set_key('decimal_point', self.decimal_point, True)
2442 self.update_history_tab()
2443 self.update_status()
2445 need_restart = False
2447 lang_request = languages.keys()[lang_combo.currentIndex()]
2448 if lang_request != self.config.get('language'):
2449 self.config.set_key("language", lang_request, True)
2452 be_result = block_explorers[block_ex_combo.currentIndex()]
2453 self.config.set_key('block_explorer', be_result, True)
2455 run_hook('close_settings_dialog')
2458 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2461 def run_network_dialog(self):
2462 if not self.network:
2464 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2466 def closeEvent(self, event):
2468 self.config.set_key("is_maximized", self.isMaximized())
2469 if not self.isMaximized():
2471 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2472 self.save_column_widths()
2473 self.config.set_key("console-history", self.console.history[-50:], True)
2474 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2478 def plugins_dialog(self):
2479 from electrum.plugins import plugins
2482 d.setWindowTitle(_('Electrum Plugins'))
2485 vbox = QVBoxLayout(d)
2488 scroll = QScrollArea()
2489 scroll.setEnabled(True)
2490 scroll.setWidgetResizable(True)
2491 scroll.setMinimumSize(400,250)
2492 vbox.addWidget(scroll)
2496 w.setMinimumHeight(len(plugins)*35)
2498 grid = QGridLayout()
2499 grid.setColumnStretch(0,1)
2502 def do_toggle(cb, p, w):
2505 if w: w.setEnabled(r)
2507 def mk_toggle(cb, p, w):
2508 return lambda: do_toggle(cb,p,w)
2510 for i, p in enumerate(plugins):
2512 cb = QCheckBox(p.fullname())
2513 cb.setDisabled(not p.is_available())
2514 cb.setChecked(p.is_enabled())
2515 grid.addWidget(cb, i, 0)
2516 if p.requires_settings():
2517 w = p.settings_widget(self)
2518 w.setEnabled( p.is_enabled() )
2519 grid.addWidget(w, i, 1)
2522 cb.clicked.connect(mk_toggle(cb,p,w))
2523 grid.addWidget(HelpButton(p.description()), i, 2)
2525 print_msg(_("Error: cannot display plugin"), p)
2526 traceback.print_exc(file=sys.stdout)
2527 grid.setRowStretch(i+1,1)
2529 vbox.addLayout(close_button(d))
2534 def show_account_details(self, k):
2535 account = self.wallet.accounts[k]
2538 d.setWindowTitle(_('Account Details'))
2541 vbox = QVBoxLayout(d)
2542 name = self.wallet.get_account_name(k)
2543 label = QLabel('Name: ' + name)
2544 vbox.addWidget(label)
2546 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2548 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2550 vbox.addWidget(QLabel(_('Master Public Key:')))
2553 text.setReadOnly(True)
2554 text.setMaximumHeight(170)
2555 vbox.addWidget(text)
2557 mpk_text = '\n'.join( account.get_master_pubkeys() )
2558 text.setText(mpk_text)
2560 vbox.addLayout(close_button(d))