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)
802 def read_send_tab(self):
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 coins = self.get_coins()
841 return outputs, fee, label, coins
845 r = self.read_send_tab()
848 outputs, fee, label, coins = r
849 self.send_tx(outputs, fee, label, coins)
853 def send_tx(self, outputs, fee, label, coins, password):
854 self.send_button.setDisabled(True)
856 # first, create an unsigned tx
858 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
860 except Exception as e:
861 traceback.print_exc(file=sys.stdout)
862 self.show_message(str(e))
863 self.send_button.setDisabled(False)
866 # call hook to see if plugin needs gui interaction
867 run_hook('send_tx', tx)
873 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
874 self.wallet.sign_transaction(tx, keypairs, password)
875 return tx, fee, label
877 def sign_done(tx, fee, label):
879 self.show_message(tx.error)
880 self.send_button.setDisabled(False)
882 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
883 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
884 self.send_button.setDisabled(False)
887 self.wallet.set_label(tx.hash(), label)
889 if not tx.is_complete() or self.config.get('show_before_broadcast'):
890 self.show_transaction(tx)
892 self.send_button.setDisabled(False)
895 self.broadcast_transaction(tx)
897 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
898 self.waiting_dialog.start()
902 def broadcast_transaction(self, tx):
904 def broadcast_thread():
905 pr = self.gui_object.payment_request
907 return self.wallet.sendtx(tx)
910 self.gui_object.payment_request = None
911 return False, _("Payment request has expired")
913 status, msg = self.wallet.sendtx(tx)
917 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_PAID, tx.hash())
918 self.wallet.storage.put('invoices', self.invoices)
919 self.update_invoices_tab()
920 self.gui_object.payment_request = None
921 refund_address = self.wallet.addresses()[0]
922 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
928 def broadcast_done(status, msg):
930 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
933 QMessageBox.warning(self, _('Error'), msg, _('OK'))
934 self.send_button.setDisabled(False)
936 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
937 self.waiting_dialog.start()
941 def prepare_for_payment_request(self):
942 self.tabs.setCurrentIndex(1)
943 self.payto_e.is_pr = True
944 for e in [self.payto_e, self.amount_e, self.message_e]:
946 for h in [self.payto_help, self.amount_help, self.message_help]:
948 self.payto_e.setText(_("please wait..."))
951 def payment_request_ok(self):
952 pr = self.gui_object.payment_request
954 if pr_id not in self.invoices:
955 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_UNPAID, None)
956 self.wallet.storage.put('invoices', self.invoices)
957 self.update_invoices_tab()
959 print_error('invoice already in list')
961 status = self.invoices[pr_id][3]
962 if status == PR_PAID:
964 self.show_message("invoice already paid")
965 self.gui_object.payment_request = None
968 self.payto_help.show()
969 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
971 self.payto_e.setGreen()
972 self.payto_e.setText(pr.domain)
973 self.amount_e.setText(self.format_amount(pr.get_amount()))
974 self.message_e.setText(pr.get_memo())
976 def payment_request_error(self):
978 self.show_message(self.gui_object.payment_request.error)
979 self.gui_object.payment_request = None
981 def set_send(self, address, amount, label, message):
983 if label and self.wallet.labels.get(address) != label:
984 if self.question('Give label "%s" to address %s ?'%(label,address)):
985 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
986 self.wallet.addressbook.append(address)
987 self.wallet.set_label(address, label)
989 self.tabs.setCurrentIndex(1)
990 label = self.wallet.labels.get(address)
991 m_addr = label + ' <'+ address +'>' if label else address
992 self.payto_e.setText(m_addr)
994 self.message_e.setText(message)
996 self.amount_e.setText(amount)
1000 self.payto_e.is_pr = False
1001 self.payto_sig.setVisible(False)
1002 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1006 for h in [self.payto_help, self.amount_help, self.message_help]:
1009 self.payto_help.set_alt(None)
1010 self.set_pay_from([])
1011 self.update_status()
1015 def set_addrs_frozen(self,addrs,freeze):
1017 if not addr: continue
1018 if addr in self.wallet.frozen_addresses and not freeze:
1019 self.wallet.unfreeze(addr)
1020 elif addr not in self.wallet.frozen_addresses and freeze:
1021 self.wallet.freeze(addr)
1022 self.update_receive_tab()
1026 def create_list_tab(self, headers):
1027 "generic tab creation method"
1028 l = MyTreeWidget(self)
1029 l.setColumnCount( len(headers) )
1030 l.setHeaderLabels( headers )
1033 vbox = QVBoxLayout()
1040 vbox.addWidget(buttons)
1045 def create_receive_tab(self):
1046 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1047 l.setContextMenuPolicy(Qt.CustomContextMenu)
1048 l.customContextMenuRequested.connect(self.create_receive_menu)
1049 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1050 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1051 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1052 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1053 self.receive_list = l
1059 def save_column_widths(self):
1060 self.column_widths["receive"] = []
1061 for i in range(self.receive_list.columnCount() -1):
1062 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1064 self.column_widths["history"] = []
1065 for i in range(self.history_list.columnCount() - 1):
1066 self.column_widths["history"].append(self.history_list.columnWidth(i))
1068 self.column_widths["contacts"] = []
1069 for i in range(self.contacts_list.columnCount() - 1):
1070 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1072 self.config.set_key("column_widths_2", self.column_widths, True)
1075 def create_contacts_tab(self):
1076 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1077 l.setContextMenuPolicy(Qt.CustomContextMenu)
1078 l.customContextMenuRequested.connect(self.create_contact_menu)
1079 for i,width in enumerate(self.column_widths['contacts']):
1080 l.setColumnWidth(i, width)
1082 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1083 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1084 self.contacts_list = l
1088 def create_invoices_tab(self):
1089 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1091 h.setStretchLastSection(False)
1092 h.setResizeMode(1, QHeaderView.Stretch)
1093 l.setContextMenuPolicy(Qt.CustomContextMenu)
1094 l.customContextMenuRequested.connect(self.create_invoice_menu)
1095 self.invoices_list = l
1098 def update_invoices_tab(self):
1099 invoices = self.wallet.storage.get('invoices', {})
1100 l = self.invoices_list
1102 for key, value in invoices.items():
1104 domain, memo, amount, status, tx_hash = value
1108 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1109 l.addTopLevelItem(item)
1111 l.setCurrentItem(l.topLevelItem(0))
1115 def delete_imported_key(self, addr):
1116 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1117 self.wallet.delete_imported_key(addr)
1118 self.update_receive_tab()
1119 self.update_history_tab()
1121 def edit_account_label(self, k):
1122 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1124 label = unicode(text)
1125 self.wallet.set_label(k,label)
1126 self.update_receive_tab()
1128 def account_set_expanded(self, item, k, b):
1130 self.accounts_expanded[k] = b
1132 def create_account_menu(self, position, k, item):
1134 if item.isExpanded():
1135 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1137 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1138 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1139 if self.wallet.seed_version > 4:
1140 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1141 if self.wallet.account_is_pending(k):
1142 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1143 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1145 def delete_pending_account(self, k):
1146 self.wallet.delete_pending_account(k)
1147 self.update_receive_tab()
1149 def create_receive_menu(self, position):
1150 # fixme: this function apparently has a side effect.
1151 # if it is not called the menu pops up several times
1152 #self.receive_list.selectedIndexes()
1154 selected = self.receive_list.selectedItems()
1155 multi_select = len(selected) > 1
1156 addrs = [unicode(item.text(0)) for item in selected]
1157 if not multi_select:
1158 item = self.receive_list.itemAt(position)
1162 if not is_valid(addr):
1163 k = str(item.data(0,32).toString())
1165 self.create_account_menu(position, k, item)
1167 item.setExpanded(not item.isExpanded())
1171 if not multi_select:
1172 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1173 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1174 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1175 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1176 if not self.wallet.is_watching_only():
1177 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1178 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1179 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1180 if self.wallet.is_imported(addr):
1181 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1183 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1184 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1185 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1186 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1188 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1189 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1191 run_hook('receive_menu', menu, addrs)
1192 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1195 def get_sendable_balance(self):
1196 return sum(map(lambda x:x['value'], self.get_coins()))
1199 def get_coins(self):
1201 return self.pay_from
1203 domain = self.wallet.get_account_addresses(self.current_account)
1204 for i in self.wallet.frozen_addresses:
1205 if i in domain: domain.remove(i)
1206 return self.wallet.get_unspent_coins(domain)
1209 def send_from_addresses(self, addrs):
1210 self.set_pay_from( addrs )
1211 self.tabs.setCurrentIndex(1)
1214 def payto(self, addr):
1216 label = self.wallet.labels.get(addr)
1217 m_addr = label + ' <' + addr + '>' if label else addr
1218 self.tabs.setCurrentIndex(1)
1219 self.payto_e.setText(m_addr)
1220 self.amount_e.setFocus()
1223 def delete_contact(self, x):
1224 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1225 self.wallet.delete_contact(x)
1226 self.wallet.set_label(x, None)
1227 self.update_history_tab()
1228 self.update_contacts_tab()
1229 self.update_completions()
1232 def create_contact_menu(self, position):
1233 item = self.contacts_list.itemAt(position)
1236 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1238 addr = unicode(item.text(0))
1239 label = unicode(item.text(1))
1240 is_editable = item.data(0,32).toBool()
1241 payto_addr = item.data(0,33).toString()
1242 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1243 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1244 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1246 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1247 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1249 run_hook('create_contact_menu', menu, item)
1250 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1252 def delete_invoice(self, key):
1253 self.invoices.pop(key)
1254 self.wallet.storage.put('invoices', self.invoices)
1255 self.update_invoices_tab()
1257 def show_invoice(self, key):
1258 from electrum.paymentrequest import PaymentRequest
1259 domain, memo, value, status, tx_hash = self.invoices[key]
1260 pr = PaymentRequest(self.config)
1264 self.show_pr_details(pr)
1266 def show_pr_details(self, pr):
1267 msg = 'Domain: ' + pr.domain
1268 msg += '\nStatus: ' + pr.get_status()
1269 msg += '\nMemo: ' + pr.get_memo()
1270 msg += '\nPayment URL: ' + pr.payment_url
1271 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1272 QMessageBox.information(self, 'Invoice', msg , 'OK')
1274 def do_pay_invoice(self, key):
1275 from electrum.paymentrequest import PaymentRequest
1276 domain, memo, value, status, tx_hash = self.invoices[key]
1277 pr = PaymentRequest(self.config)
1280 self.gui_object.payment_request = pr
1281 self.prepare_for_payment_request()
1283 self.payment_request_ok()
1285 self.payment_request_error()
1288 def create_invoice_menu(self, position):
1289 item = self.invoices_list.itemAt(position)
1292 k = self.invoices_list.indexOfTopLevelItem(item)
1293 key = self.invoices.keys()[k]
1294 domain, memo, value, status, tx_hash = self.invoices[key]
1296 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1297 if status == PR_UNPAID:
1298 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1299 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1300 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1303 def update_receive_item(self, item):
1304 item.setFont(0, QFont(MONOSPACE_FONT))
1305 address = str(item.data(0,0).toString())
1306 label = self.wallet.labels.get(address,'')
1307 item.setData(1,0,label)
1308 item.setData(0,32, True) # is editable
1310 run_hook('update_receive_item', address, item)
1312 if not self.wallet.is_mine(address): return
1314 c, u = self.wallet.get_addr_balance(address)
1315 balance = self.format_amount(c + u)
1316 item.setData(2,0,balance)
1318 if address in self.wallet.frozen_addresses:
1319 item.setBackgroundColor(0, QColor('lightblue'))
1322 def update_receive_tab(self):
1323 l = self.receive_list
1324 # extend the syntax for consistency
1325 l.addChild = l.addTopLevelItem
1326 l.insertChild = l.insertTopLevelItem
1329 for i,width in enumerate(self.column_widths['receive']):
1330 l.setColumnWidth(i, width)
1332 accounts = self.wallet.get_accounts()
1333 if self.current_account is None:
1334 account_items = sorted(accounts.items())
1336 account_items = [(self.current_account, accounts.get(self.current_account))]
1339 for k, account in account_items:
1341 if len(accounts) > 1:
1342 name = self.wallet.get_account_name(k)
1343 c,u = self.wallet.get_account_balance(k)
1344 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1345 l.addTopLevelItem(account_item)
1346 account_item.setExpanded(self.accounts_expanded.get(k, True))
1347 account_item.setData(0, 32, k)
1351 sequences = [0,1] if account.has_change() else [0]
1352 for is_change in sequences:
1353 if len(sequences) > 1:
1354 name = _("Receiving") if not is_change else _("Change")
1355 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1356 account_item.addChild(seq_item)
1358 seq_item.setExpanded(True)
1360 seq_item = account_item
1362 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1368 for address in account.get_addresses(is_change):
1370 num, is_used = self.wallet.is_used(address)
1373 if gap > self.wallet.gap_limit:
1378 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1379 self.update_receive_item(item)
1381 item.setBackgroundColor(1, QColor('red'))
1385 seq_item.insertChild(0,used_item)
1387 used_item.addChild(item)
1389 seq_item.addChild(item)
1391 # we use column 1 because column 0 may be hidden
1392 l.setCurrentItem(l.topLevelItem(0),1)
1395 def update_contacts_tab(self):
1396 l = self.contacts_list
1399 for address in self.wallet.addressbook:
1400 label = self.wallet.labels.get(address,'')
1401 n = self.wallet.get_num_tx(address)
1402 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1403 item.setFont(0, QFont(MONOSPACE_FONT))
1404 # 32 = label can be edited (bool)
1405 item.setData(0,32, True)
1407 item.setData(0,33, address)
1408 l.addTopLevelItem(item)
1410 run_hook('update_contacts_tab', l)
1411 l.setCurrentItem(l.topLevelItem(0))
1415 def create_console_tab(self):
1416 from console import Console
1417 self.console = console = Console()
1421 def update_console(self):
1422 console = self.console
1423 console.history = self.config.get("console-history",[])
1424 console.history_index = len(console.history)
1426 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1427 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1429 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1431 def mkfunc(f, method):
1432 return lambda *args: apply( f, (method, args, self.password_dialog ))
1434 if m[0]=='_' or m in ['network','wallet']: continue
1435 methods[m] = mkfunc(c._run, m)
1437 console.updateNamespace(methods)
1440 def change_account(self,s):
1441 if s == _("All accounts"):
1442 self.current_account = None
1444 accounts = self.wallet.get_account_names()
1445 for k, v in accounts.items():
1447 self.current_account = k
1448 self.update_history_tab()
1449 self.update_status()
1450 self.update_receive_tab()
1452 def create_status_bar(self):
1455 sb.setFixedHeight(35)
1456 qtVersion = qVersion()
1458 self.balance_label = QLabel("")
1459 sb.addWidget(self.balance_label)
1461 from version_getter import UpdateLabel
1462 self.updatelabel = UpdateLabel(self.config, sb)
1464 self.account_selector = QComboBox()
1465 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1466 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1467 sb.addPermanentWidget(self.account_selector)
1469 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1470 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1472 self.lock_icon = QIcon()
1473 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1474 sb.addPermanentWidget( self.password_button )
1476 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1477 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1478 sb.addPermanentWidget( self.seed_button )
1479 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1480 sb.addPermanentWidget( self.status_button )
1482 run_hook('create_status_bar', (sb,))
1484 self.setStatusBar(sb)
1487 def update_lock_icon(self):
1488 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1489 self.password_button.setIcon( icon )
1492 def update_buttons_on_seed(self):
1493 if self.wallet.has_seed():
1494 self.seed_button.show()
1496 self.seed_button.hide()
1498 if not self.wallet.is_watching_only():
1499 self.password_button.show()
1500 self.send_button.setText(_("Send"))
1502 self.password_button.hide()
1503 self.send_button.setText(_("Create unsigned transaction"))
1506 def change_password_dialog(self):
1507 from password_dialog import PasswordDialog
1508 d = PasswordDialog(self.wallet, self)
1510 self.update_lock_icon()
1513 def new_contact_dialog(self):
1516 d.setWindowTitle(_("New Contact"))
1517 vbox = QVBoxLayout(d)
1518 vbox.addWidget(QLabel(_('New Contact')+':'))
1520 grid = QGridLayout()
1523 grid.addWidget(QLabel(_("Address")), 1, 0)
1524 grid.addWidget(line1, 1, 1)
1525 grid.addWidget(QLabel(_("Name")), 2, 0)
1526 grid.addWidget(line2, 2, 1)
1528 vbox.addLayout(grid)
1529 vbox.addLayout(ok_cancel_buttons(d))
1534 address = str(line1.text())
1535 label = unicode(line2.text())
1537 if not is_valid(address):
1538 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1541 self.wallet.add_contact(address)
1543 self.wallet.set_label(address, label)
1545 self.update_contacts_tab()
1546 self.update_history_tab()
1547 self.update_completions()
1548 self.tabs.setCurrentIndex(3)
1552 def new_account_dialog(self, password):
1554 dialog = QDialog(self)
1556 dialog.setWindowTitle(_("New Account"))
1558 vbox = QVBoxLayout()
1559 vbox.addWidget(QLabel(_('Account name')+':'))
1562 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1563 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1568 vbox.addLayout(ok_cancel_buttons(dialog))
1569 dialog.setLayout(vbox)
1573 name = str(e.text())
1576 self.wallet.create_pending_account(name, password)
1577 self.update_receive_tab()
1578 self.tabs.setCurrentIndex(2)
1583 def show_master_public_keys(self):
1585 dialog = QDialog(self)
1587 dialog.setWindowTitle(_("Master Public Keys"))
1589 main_layout = QGridLayout()
1590 mpk_dict = self.wallet.get_master_public_keys()
1592 for key, value in mpk_dict.items():
1593 main_layout.addWidget(QLabel(key), i, 0)
1594 mpk_text = QTextEdit()
1595 mpk_text.setReadOnly(True)
1596 mpk_text.setMaximumHeight(170)
1597 mpk_text.setText(value)
1598 main_layout.addWidget(mpk_text, i + 1, 0)
1601 vbox = QVBoxLayout()
1602 vbox.addLayout(main_layout)
1603 vbox.addLayout(close_button(dialog))
1605 dialog.setLayout(vbox)
1610 def show_seed_dialog(self, password):
1611 if not self.wallet.has_seed():
1612 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1616 mnemonic = self.wallet.get_mnemonic(password)
1618 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1620 from seed_dialog import SeedDialog
1621 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1626 def show_qrcode(self, data, title = _("QR code")):
1630 d.setWindowTitle(title)
1631 d.setMinimumSize(270, 300)
1632 vbox = QVBoxLayout()
1633 qrw = QRCodeWidget(data)
1634 vbox.addWidget(qrw, 1)
1635 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1636 hbox = QHBoxLayout()
1639 filename = os.path.join(self.config.path, "qrcode.bmp")
1642 bmp.save_qrcode(qrw.qr, filename)
1643 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1645 def copy_to_clipboard():
1646 bmp.save_qrcode(qrw.qr, filename)
1647 self.app.clipboard().setImage(QImage(filename))
1648 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1650 b = QPushButton(_("Copy"))
1652 b.clicked.connect(copy_to_clipboard)
1654 b = QPushButton(_("Save"))
1656 b.clicked.connect(print_qr)
1658 b = QPushButton(_("Close"))
1660 b.clicked.connect(d.accept)
1663 vbox.addLayout(hbox)
1668 def do_protect(self, func, args):
1669 if self.wallet.use_encryption:
1670 password = self.password_dialog()
1676 if args != (False,):
1677 args = (self,) + args + (password,)
1679 args = (self,password)
1683 def show_public_keys(self, address):
1684 if not address: return
1686 pubkey_list = self.wallet.get_public_keys(address)
1687 except Exception as e:
1688 traceback.print_exc(file=sys.stdout)
1689 self.show_message(str(e))
1693 d.setMinimumSize(600, 200)
1695 vbox = QVBoxLayout()
1696 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1697 vbox.addWidget( QLabel(_("Public key") + ':'))
1699 keys.setReadOnly(True)
1700 keys.setText('\n'.join(pubkey_list))
1701 vbox.addWidget(keys)
1702 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1703 vbox.addLayout(close_button(d))
1708 def show_private_key(self, address, password):
1709 if not address: return
1711 pk_list = self.wallet.get_private_key(address, password)
1712 except Exception as e:
1713 traceback.print_exc(file=sys.stdout)
1714 self.show_message(str(e))
1718 d.setMinimumSize(600, 200)
1720 vbox = QVBoxLayout()
1721 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1722 vbox.addWidget( QLabel(_("Private key") + ':'))
1724 keys.setReadOnly(True)
1725 keys.setText('\n'.join(pk_list))
1726 vbox.addWidget(keys)
1727 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1728 vbox.addLayout(close_button(d))
1734 def do_sign(self, address, message, signature, password):
1735 message = unicode(message.toPlainText())
1736 message = message.encode('utf-8')
1738 sig = self.wallet.sign_message(str(address.text()), message, password)
1739 signature.setText(sig)
1740 except Exception as e:
1741 self.show_message(str(e))
1743 def do_verify(self, address, message, signature):
1744 message = unicode(message.toPlainText())
1745 message = message.encode('utf-8')
1746 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1747 self.show_message(_("Signature verified"))
1749 self.show_message(_("Error: wrong signature"))
1752 def sign_verify_message(self, address=''):
1755 d.setWindowTitle(_('Sign/verify Message'))
1756 d.setMinimumSize(410, 290)
1758 layout = QGridLayout(d)
1760 message_e = QTextEdit()
1761 layout.addWidget(QLabel(_('Message')), 1, 0)
1762 layout.addWidget(message_e, 1, 1)
1763 layout.setRowStretch(2,3)
1765 address_e = QLineEdit()
1766 address_e.setText(address)
1767 layout.addWidget(QLabel(_('Address')), 2, 0)
1768 layout.addWidget(address_e, 2, 1)
1770 signature_e = QTextEdit()
1771 layout.addWidget(QLabel(_('Signature')), 3, 0)
1772 layout.addWidget(signature_e, 3, 1)
1773 layout.setRowStretch(3,1)
1775 hbox = QHBoxLayout()
1777 b = QPushButton(_("Sign"))
1778 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1781 b = QPushButton(_("Verify"))
1782 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1785 b = QPushButton(_("Close"))
1786 b.clicked.connect(d.accept)
1788 layout.addLayout(hbox, 4, 1)
1793 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1795 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1796 message_e.setText(decrypted)
1797 except Exception as e:
1798 self.show_message(str(e))
1801 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1802 message = unicode(message_e.toPlainText())
1803 message = message.encode('utf-8')
1805 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1806 encrypted_e.setText(encrypted)
1807 except Exception as e:
1808 self.show_message(str(e))
1812 def encrypt_message(self, address = ''):
1815 d.setWindowTitle(_('Encrypt/decrypt Message'))
1816 d.setMinimumSize(610, 490)
1818 layout = QGridLayout(d)
1820 message_e = QTextEdit()
1821 layout.addWidget(QLabel(_('Message')), 1, 0)
1822 layout.addWidget(message_e, 1, 1)
1823 layout.setRowStretch(2,3)
1825 pubkey_e = QLineEdit()
1827 pubkey = self.wallet.getpubkeys(address)[0]
1828 pubkey_e.setText(pubkey)
1829 layout.addWidget(QLabel(_('Public key')), 2, 0)
1830 layout.addWidget(pubkey_e, 2, 1)
1832 encrypted_e = QTextEdit()
1833 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1834 layout.addWidget(encrypted_e, 3, 1)
1835 layout.setRowStretch(3,1)
1837 hbox = QHBoxLayout()
1838 b = QPushButton(_("Encrypt"))
1839 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1842 b = QPushButton(_("Decrypt"))
1843 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1846 b = QPushButton(_("Close"))
1847 b.clicked.connect(d.accept)
1850 layout.addLayout(hbox, 4, 1)
1854 def question(self, msg):
1855 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1857 def show_message(self, msg):
1858 QMessageBox.information(self, _('Message'), msg, _('OK'))
1860 def password_dialog(self, msg=None):
1863 d.setWindowTitle(_("Enter Password"))
1868 vbox = QVBoxLayout()
1870 msg = _('Please enter your password')
1871 vbox.addWidget(QLabel(msg))
1873 grid = QGridLayout()
1875 grid.addWidget(QLabel(_('Password')), 1, 0)
1876 grid.addWidget(pw, 1, 1)
1877 vbox.addLayout(grid)
1879 vbox.addLayout(ok_cancel_buttons(d))
1882 run_hook('password_dialog', pw, grid, 1)
1883 if not d.exec_(): return
1884 return unicode(pw.text())
1893 def tx_from_text(self, txt):
1894 "json or raw hexadecimal"
1897 tx = Transaction(txt)
1903 tx_dict = json.loads(str(txt))
1904 assert "hex" in tx_dict.keys()
1905 tx = Transaction(tx_dict["hex"])
1906 if tx_dict.has_key("input_info"):
1907 input_info = json.loads(tx_dict['input_info'])
1908 tx.add_input_info(input_info)
1911 traceback.print_exc(file=sys.stdout)
1914 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1918 def read_tx_from_file(self):
1919 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1923 with open(fileName, "r") as f:
1924 file_content = f.read()
1925 except (ValueError, IOError, os.error), reason:
1926 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1928 return self.tx_from_text(file_content)
1932 def sign_raw_transaction(self, tx, input_info, password):
1933 self.wallet.signrawtransaction(tx, input_info, [], password)
1935 def do_process_from_text(self):
1936 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1939 tx = self.tx_from_text(text)
1941 self.show_transaction(tx)
1943 def do_process_from_file(self):
1944 tx = self.read_tx_from_file()
1946 self.show_transaction(tx)
1948 def do_process_from_txid(self):
1949 from electrum import transaction
1950 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1952 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1954 tx = transaction.Transaction(r)
1956 self.show_transaction(tx)
1958 self.show_message("unknown transaction")
1960 def do_process_from_csvReader(self, csvReader):
1965 for position, row in enumerate(csvReader):
1967 if not is_valid(address):
1968 errors.append((position, address))
1970 amount = Decimal(row[1])
1971 amount = int(100000000*amount)
1972 outputs.append((address, amount))
1973 except (ValueError, IOError, os.error), reason:
1974 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1978 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1979 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1983 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1984 except Exception as e:
1985 self.show_message(str(e))
1988 self.show_transaction(tx)
1990 def do_process_from_csv_file(self):
1991 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1995 with open(fileName, "r") as f:
1996 csvReader = csv.reader(f)
1997 self.do_process_from_csvReader(csvReader)
1998 except (ValueError, IOError, os.error), reason:
1999 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2002 def do_process_from_csv_text(self):
2003 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2004 + _("Format: address, amount. One output per line"), _("Load CSV"))
2007 f = StringIO.StringIO(text)
2008 csvReader = csv.reader(f)
2009 self.do_process_from_csvReader(csvReader)
2014 def export_privkeys_dialog(self, password):
2015 if self.wallet.is_watching_only():
2016 self.show_message(_("This is a watching-only wallet"))
2020 d.setWindowTitle(_('Private keys'))
2021 d.setMinimumSize(850, 300)
2022 vbox = QVBoxLayout(d)
2024 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2025 _("Exposing a single private key can compromise your entire wallet!"),
2026 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2027 vbox.addWidget(QLabel(msg))
2033 defaultname = 'electrum-private-keys.csv'
2034 select_msg = _('Select file to export your private keys to')
2035 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2036 vbox.addLayout(hbox)
2038 h, b = ok_cancel_buttons2(d, _('Export'))
2043 addresses = self.wallet.addresses(True)
2045 def privkeys_thread():
2046 for addr in addresses:
2050 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2051 d.emit(SIGNAL('computing_privkeys'))
2052 d.emit(SIGNAL('show_privkeys'))
2054 def show_privkeys():
2055 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2059 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2060 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2061 threading.Thread(target=privkeys_thread).start()
2067 filename = filename_e.text()
2072 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2073 except (IOError, os.error), reason:
2074 export_error_label = _("Electrum was unable to produce a private key-export.")
2075 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2077 except Exception as e:
2078 self.show_message(str(e))
2081 self.show_message(_("Private keys exported."))
2084 def do_export_privkeys(self, fileName, pklist, is_csv):
2085 with open(fileName, "w+") as f:
2087 transaction = csv.writer(f)
2088 transaction.writerow(["address", "private_key"])
2089 for addr, pk in pklist.items():
2090 transaction.writerow(["%34s"%addr,pk])
2093 f.write(json.dumps(pklist, indent = 4))
2096 def do_import_labels(self):
2097 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2098 if not labelsFile: return
2100 f = open(labelsFile, 'r')
2103 for key, value in json.loads(data).items():
2104 self.wallet.set_label(key, value)
2105 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2106 except (IOError, os.error), reason:
2107 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2110 def do_export_labels(self):
2111 labels = self.wallet.labels
2113 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2115 with open(fileName, 'w+') as f:
2116 json.dump(labels, f)
2117 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2118 except (IOError, os.error), reason:
2119 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2122 def export_history_dialog(self):
2125 d.setWindowTitle(_('Export History'))
2126 d.setMinimumSize(400, 200)
2127 vbox = QVBoxLayout(d)
2129 defaultname = os.path.expanduser('~/electrum-history.csv')
2130 select_msg = _('Select file to export your wallet transactions to')
2132 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2133 vbox.addLayout(hbox)
2137 h, b = ok_cancel_buttons2(d, _('Export'))
2142 filename = filename_e.text()
2147 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2148 except (IOError, os.error), reason:
2149 export_error_label = _("Electrum was unable to produce a transaction export.")
2150 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2153 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2156 def do_export_history(self, wallet, fileName, is_csv):
2157 history = wallet.get_tx_history()
2159 for item in history:
2160 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2162 if timestamp is not None:
2164 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2165 except [RuntimeError, TypeError, NameError] as reason:
2166 time_string = "unknown"
2169 time_string = "unknown"
2171 time_string = "pending"
2173 if value is not None:
2174 value_string = format_satoshis(value, True)
2179 fee_string = format_satoshis(fee, True)
2184 label, is_default_label = wallet.get_label(tx_hash)
2185 label = label.encode('utf-8')
2189 balance_string = format_satoshis(balance, False)
2191 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2193 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2195 with open(fileName, "w+") as f:
2197 transaction = csv.writer(f)
2198 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2200 transaction.writerow(line)
2203 f.write(json.dumps(lines, indent = 4))
2206 def sweep_key_dialog(self):
2208 d.setWindowTitle(_('Sweep private keys'))
2209 d.setMinimumSize(600, 300)
2211 vbox = QVBoxLayout(d)
2212 vbox.addWidget(QLabel(_("Enter private keys")))
2214 keys_e = QTextEdit()
2215 keys_e.setTabChangesFocus(True)
2216 vbox.addWidget(keys_e)
2218 h, address_e = address_field(self.wallet.addresses())
2222 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2223 vbox.addLayout(hbox)
2224 button.setEnabled(False)
2227 addr = str(address_e.text())
2228 if bitcoin.is_address(addr):
2232 pk = str(keys_e.toPlainText()).strip()
2233 if Wallet.is_private_key(pk):
2236 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2237 keys_e.textChanged.connect(f)
2238 address_e.textChanged.connect(f)
2242 fee = self.wallet.fee
2243 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2244 self.show_transaction(tx)
2248 def do_import_privkey(self, password):
2249 if not self.wallet.has_imported_keys():
2250 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2251 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2252 + _('Are you sure you understand what you are doing?'), 3, 4)
2255 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2258 text = str(text).split()
2263 addr = self.wallet.import_key(key, password)
2264 except Exception as e:
2270 addrlist.append(addr)
2272 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2274 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2275 self.update_receive_tab()
2276 self.update_history_tab()
2279 def settings_dialog(self):
2281 d.setWindowTitle(_('Electrum Settings'))
2283 vbox = QVBoxLayout()
2284 grid = QGridLayout()
2285 grid.setColumnStretch(0,1)
2287 nz_label = QLabel(_('Display zeros') + ':')
2288 grid.addWidget(nz_label, 0, 0)
2289 nz_e = AmountEdit(None,True)
2290 nz_e.setText("%d"% self.num_zeros)
2291 grid.addWidget(nz_e, 0, 1)
2292 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2293 grid.addWidget(HelpButton(msg), 0, 2)
2294 if not self.config.is_modifiable('num_zeros'):
2295 for w in [nz_e, nz_label]: w.setEnabled(False)
2297 lang_label=QLabel(_('Language') + ':')
2298 grid.addWidget(lang_label, 1, 0)
2299 lang_combo = QComboBox()
2300 from electrum.i18n import languages
2301 lang_combo.addItems(languages.values())
2303 index = languages.keys().index(self.config.get("language",''))
2306 lang_combo.setCurrentIndex(index)
2307 grid.addWidget(lang_combo, 1, 1)
2308 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2309 if not self.config.is_modifiable('language'):
2310 for w in [lang_combo, lang_label]: w.setEnabled(False)
2313 fee_label = QLabel(_('Transaction fee') + ':')
2314 grid.addWidget(fee_label, 2, 0)
2315 fee_e = BTCAmountEdit(self.get_decimal_point)
2316 fee_e.setAmount(self.wallet.fee)
2317 grid.addWidget(fee_e, 2, 1)
2318 msg = _('Fee per kilobyte of transaction.') + '\n' \
2319 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2320 grid.addWidget(HelpButton(msg), 2, 2)
2321 if not self.config.is_modifiable('fee_per_kb'):
2322 for w in [fee_e, fee_label]: w.setEnabled(False)
2324 units = ['BTC', 'mBTC']
2325 unit_label = QLabel(_('Base unit') + ':')
2326 grid.addWidget(unit_label, 3, 0)
2327 unit_combo = QComboBox()
2328 unit_combo.addItems(units)
2329 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2330 grid.addWidget(unit_combo, 3, 1)
2331 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2332 + '\n1BTC=1000mBTC.\n' \
2333 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2335 usechange_cb = QCheckBox(_('Use change addresses'))
2336 usechange_cb.setChecked(self.wallet.use_change)
2337 grid.addWidget(usechange_cb, 4, 0)
2338 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2339 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2341 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2342 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2343 grid.addWidget(block_ex_label, 5, 0)
2344 block_ex_combo = QComboBox()
2345 block_ex_combo.addItems(block_explorers)
2346 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2347 grid.addWidget(block_ex_combo, 5, 1)
2348 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2350 show_tx = self.config.get('show_before_broadcast', False)
2351 showtx_cb = QCheckBox(_('Show before broadcast'))
2352 showtx_cb.setChecked(show_tx)
2353 grid.addWidget(showtx_cb, 6, 0)
2354 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2356 vbox.addLayout(grid)
2358 vbox.addLayout(ok_cancel_buttons(d))
2362 if not d.exec_(): return
2364 fee = fee_e.get_amount()
2366 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2369 self.wallet.set_fee(fee)
2371 nz = unicode(nz_e.text())
2376 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2379 if self.num_zeros != nz:
2381 self.config.set_key('num_zeros', nz, True)
2382 self.update_history_tab()
2383 self.update_receive_tab()
2385 usechange_result = usechange_cb.isChecked()
2386 if self.wallet.use_change != usechange_result:
2387 self.wallet.use_change = usechange_result
2388 self.wallet.storage.put('use_change', self.wallet.use_change)
2390 if showtx_cb.isChecked() != show_tx:
2391 self.config.set_key('show_before_broadcast', not show_tx)
2393 unit_result = units[unit_combo.currentIndex()]
2394 if self.base_unit() != unit_result:
2395 self.decimal_point = 8 if unit_result == 'BTC' else 5
2396 self.config.set_key('decimal_point', self.decimal_point, True)
2397 self.update_history_tab()
2398 self.update_status()
2400 need_restart = False
2402 lang_request = languages.keys()[lang_combo.currentIndex()]
2403 if lang_request != self.config.get('language'):
2404 self.config.set_key("language", lang_request, True)
2407 be_result = block_explorers[block_ex_combo.currentIndex()]
2408 self.config.set_key('block_explorer', be_result, True)
2410 run_hook('close_settings_dialog')
2413 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2416 def run_network_dialog(self):
2417 if not self.network:
2419 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2421 def closeEvent(self, event):
2423 self.config.set_key("is_maximized", self.isMaximized())
2424 if not self.isMaximized():
2426 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2427 self.save_column_widths()
2428 self.config.set_key("console-history", self.console.history[-50:], True)
2429 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2433 def plugins_dialog(self):
2434 from electrum.plugins import plugins
2437 d.setWindowTitle(_('Electrum Plugins'))
2440 vbox = QVBoxLayout(d)
2443 scroll = QScrollArea()
2444 scroll.setEnabled(True)
2445 scroll.setWidgetResizable(True)
2446 scroll.setMinimumSize(400,250)
2447 vbox.addWidget(scroll)
2451 w.setMinimumHeight(len(plugins)*35)
2453 grid = QGridLayout()
2454 grid.setColumnStretch(0,1)
2457 def do_toggle(cb, p, w):
2460 if w: w.setEnabled(r)
2462 def mk_toggle(cb, p, w):
2463 return lambda: do_toggle(cb,p,w)
2465 for i, p in enumerate(plugins):
2467 cb = QCheckBox(p.fullname())
2468 cb.setDisabled(not p.is_available())
2469 cb.setChecked(p.is_enabled())
2470 grid.addWidget(cb, i, 0)
2471 if p.requires_settings():
2472 w = p.settings_widget(self)
2473 w.setEnabled( p.is_enabled() )
2474 grid.addWidget(w, i, 1)
2477 cb.clicked.connect(mk_toggle(cb,p,w))
2478 grid.addWidget(HelpButton(p.description()), i, 2)
2480 print_msg(_("Error: cannot display plugin"), p)
2481 traceback.print_exc(file=sys.stdout)
2482 grid.setRowStretch(i+1,1)
2484 vbox.addLayout(close_button(d))
2489 def show_account_details(self, k):
2490 account = self.wallet.accounts[k]
2493 d.setWindowTitle(_('Account Details'))
2496 vbox = QVBoxLayout(d)
2497 name = self.wallet.get_account_name(k)
2498 label = QLabel('Name: ' + name)
2499 vbox.addWidget(label)
2501 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2503 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2505 vbox.addWidget(QLabel(_('Master Public Key:')))
2508 text.setReadOnly(True)
2509 text.setMaximumHeight(170)
2510 vbox.addWidget(text)
2512 mpk_text = '\n'.join( account.get_master_pubkeys() )
2513 text.setText(mpk_text)
2515 vbox.addLayout(close_button(d))