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)
344 self.raw_transaction_menu = raw_transaction_menu
346 help_menu = menubar.addMenu(_("&Help"))
347 help_menu.addAction(_("&About"), self.show_about)
348 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
349 help_menu.addSeparator()
350 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
351 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
353 self.setMenuBar(menubar)
355 def show_about(self):
356 QMessageBox.about(self, "Electrum",
357 _("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."))
359 def show_report_bug(self):
360 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
361 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
364 def notify_transactions(self):
365 if not self.network or not self.network.is_connected():
368 print_error("Notifying GUI")
369 if len(self.network.pending_transactions_for_notifications) > 0:
370 # Combine the transactions if there are more then three
371 tx_amount = len(self.network.pending_transactions_for_notifications)
374 for tx in self.network.pending_transactions_for_notifications:
375 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
379 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
380 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
382 self.network.pending_transactions_for_notifications = []
384 for tx in self.network.pending_transactions_for_notifications:
386 self.network.pending_transactions_for_notifications.remove(tx)
387 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
389 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
391 def notify(self, message):
392 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
396 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
397 def getOpenFileName(self, title, filter = ""):
398 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
399 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
400 if fileName and directory != os.path.dirname(fileName):
401 self.config.set_key('io_dir', os.path.dirname(fileName), True)
404 def getSaveFileName(self, title, filename, filter = ""):
405 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
406 path = os.path.join( directory, filename )
407 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
408 if fileName and directory != os.path.dirname(fileName):
409 self.config.set_key('io_dir', os.path.dirname(fileName), True)
413 QMainWindow.close(self)
414 run_hook('close_main_window')
416 def connect_slots(self, sender):
417 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
418 self.previous_payto_e=''
420 def timer_actions(self):
421 if self.need_update.is_set():
423 self.need_update.clear()
424 run_hook('timer_actions')
426 def format_amount(self, x, is_diff=False, whitespaces=False):
427 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
430 def get_decimal_point(self):
431 return self.decimal_point
435 assert self.decimal_point in [5,8]
436 return "BTC" if self.decimal_point == 8 else "mBTC"
439 def update_status(self):
440 if self.network is None or not self.network.is_running():
442 icon = QIcon(":icons/status_disconnected.png")
444 elif self.network.is_connected():
445 if not self.wallet.up_to_date:
446 text = _("Synchronizing...")
447 icon = QIcon(":icons/status_waiting.png")
448 elif self.network.server_lag > 1:
449 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
450 icon = QIcon(":icons/status_lagging.png")
452 c, u = self.wallet.get_account_balance(self.current_account)
453 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
454 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
456 # append fiat balance and price from exchange rate plugin
458 run_hook('get_fiat_status_text', c+u, r)
463 self.tray.setToolTip(text)
464 icon = QIcon(":icons/status_connected.png")
466 text = _("Not connected")
467 icon = QIcon(":icons/status_disconnected.png")
469 self.balance_label.setText(text)
470 self.status_button.setIcon( icon )
473 def update_wallet(self):
475 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
476 self.update_history_tab()
477 self.update_receive_tab()
478 self.update_contacts_tab()
479 self.update_completions()
480 self.update_invoices_tab()
483 def create_history_tab(self):
484 self.history_list = l = MyTreeWidget(self)
486 for i,width in enumerate(self.column_widths['history']):
487 l.setColumnWidth(i, width)
488 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
489 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
490 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
492 l.customContextMenuRequested.connect(self.create_history_menu)
496 def create_history_menu(self, position):
497 self.history_list.selectedIndexes()
498 item = self.history_list.currentItem()
499 be = self.config.get('block_explorer', 'Blockchain.info')
500 if be == 'Blockchain.info':
501 block_explorer = 'https://blockchain.info/tx/'
502 elif be == 'Blockr.io':
503 block_explorer = 'https://blockr.io/tx/info/'
504 elif be == 'Insight.is':
505 block_explorer = 'http://live.insight.is/tx/'
507 tx_hash = str(item.data(0, Qt.UserRole).toString())
508 if not tx_hash: return
510 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
511 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
512 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
513 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
514 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
517 def show_transaction(self, tx):
518 import transaction_dialog
519 d = transaction_dialog.TxDialog(tx, self)
522 def tx_label_clicked(self, item, column):
523 if column==2 and item.isSelected():
525 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
526 self.history_list.editItem( item, column )
527 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
530 def tx_label_changed(self, item, column):
534 tx_hash = str(item.data(0, Qt.UserRole).toString())
535 tx = self.wallet.transactions.get(tx_hash)
536 text = unicode( item.text(2) )
537 self.wallet.set_label(tx_hash, text)
539 item.setForeground(2, QBrush(QColor('black')))
541 text = self.wallet.get_default_label(tx_hash)
542 item.setText(2, text)
543 item.setForeground(2, QBrush(QColor('gray')))
547 def edit_label(self, is_recv):
548 l = self.receive_list if is_recv else self.contacts_list
549 item = l.currentItem()
550 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
551 l.editItem( item, 1 )
552 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
556 def address_label_clicked(self, item, column, l, column_addr, column_label):
557 if column == column_label and item.isSelected():
558 is_editable = item.data(0, 32).toBool()
561 addr = unicode( item.text(column_addr) )
562 label = unicode( item.text(column_label) )
563 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
564 l.editItem( item, column )
565 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568 def address_label_changed(self, item, column, l, column_addr, column_label):
569 if column == column_label:
570 addr = unicode( item.text(column_addr) )
571 text = unicode( item.text(column_label) )
572 is_editable = item.data(0, 32).toBool()
576 changed = self.wallet.set_label(addr, text)
578 self.update_history_tab()
579 self.update_completions()
581 self.current_item_changed(item)
583 run_hook('item_changed', item, column)
586 def current_item_changed(self, a):
587 run_hook('current_item_changed', a)
591 def update_history_tab(self):
593 self.history_list.clear()
594 for item in self.wallet.get_tx_history(self.current_account):
595 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
596 time_str = _("unknown")
599 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
601 time_str = _("error")
604 time_str = 'unverified'
605 icon = QIcon(":icons/unconfirmed.png")
608 icon = QIcon(":icons/unconfirmed.png")
610 icon = QIcon(":icons/clock%d.png"%conf)
612 icon = QIcon(":icons/confirmed.png")
614 if value is not None:
615 v_str = self.format_amount(value, True, whitespaces=True)
619 balance_str = self.format_amount(balance, whitespaces=True)
622 label, is_default_label = self.wallet.get_label(tx_hash)
624 label = _('Pruned transaction outputs')
625 is_default_label = False
627 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
628 item.setFont(2, QFont(MONOSPACE_FONT))
629 item.setFont(3, QFont(MONOSPACE_FONT))
630 item.setFont(4, QFont(MONOSPACE_FONT))
632 item.setForeground(3, QBrush(QColor("#BC1E1E")))
634 item.setData(0, Qt.UserRole, tx_hash)
635 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
637 item.setForeground(2, QBrush(QColor('grey')))
639 item.setIcon(0, icon)
640 self.history_list.insertTopLevelItem(0,item)
643 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
644 run_hook('history_tab_update')
647 def create_send_tab(self):
650 self.send_grid = grid = QGridLayout(w)
652 grid.setColumnMinimumWidth(3,300)
653 grid.setColumnStretch(5,1)
654 grid.setRowStretch(8, 1)
656 from paytoedit import PayToEdit
657 self.amount_e = BTCAmountEdit(self.get_decimal_point)
658 self.payto_e = PayToEdit(self.amount_e)
659 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)'))
660 grid.addWidget(QLabel(_('Pay to')), 1, 0)
661 grid.addWidget(self.payto_e, 1, 1, 1, 3)
662 grid.addWidget(self.payto_help, 1, 4)
664 completer = QCompleter()
665 completer.setCaseSensitivity(False)
666 self.payto_e.setCompleter(completer)
667 completer.setModel(self.completions)
669 self.message_e = MyLineEdit()
670 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.'))
671 grid.addWidget(QLabel(_('Description')), 2, 0)
672 grid.addWidget(self.message_e, 2, 1, 1, 3)
673 grid.addWidget(self.message_help, 2, 4)
675 self.from_label = QLabel(_('From'))
676 grid.addWidget(self.from_label, 3, 0)
677 self.from_list = MyTreeWidget(self)
678 self.from_list.setColumnCount(2)
679 self.from_list.setColumnWidth(0, 350)
680 self.from_list.setColumnWidth(1, 50)
681 self.from_list.setHeaderHidden(True)
682 self.from_list.setMaximumHeight(80)
683 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
684 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
685 grid.addWidget(self.from_list, 3, 1, 1, 3)
686 self.set_pay_from([])
688 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
689 + _('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.') \
690 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
691 grid.addWidget(QLabel(_('Amount')), 4, 0)
692 grid.addWidget(self.amount_e, 4, 1, 1, 2)
693 grid.addWidget(self.amount_help, 4, 3)
695 self.fee_e = BTCAmountEdit(self.get_decimal_point)
696 grid.addWidget(QLabel(_('Fee')), 5, 0)
697 grid.addWidget(self.fee_e, 5, 1, 1, 2)
698 grid.addWidget(HelpButton(
699 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
700 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
701 + _('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)
703 self.send_button = EnterButton(_("Send"), self.do_send)
704 grid.addWidget(self.send_button, 6, 1)
706 b = EnterButton(_("Clear"), self.do_clear)
707 grid.addWidget(b, 6, 2)
709 self.payto_sig = QLabel('')
710 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
712 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
713 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
716 def entry_changed( is_fee ):
717 self.funds_error = False
719 if self.amount_e.is_shortcut:
720 self.amount_e.is_shortcut = False
721 sendable = self.get_sendable_balance()
722 # there is only one output because we are completely spending inputs
723 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
724 fee = self.wallet.estimated_fee(inputs, 1)
726 self.amount_e.setAmount(amount)
727 self.fee_e.setAmount(fee)
730 amount = self.amount_e.get_amount()
731 fee = self.fee_e.get_amount()
733 if not is_fee: fee = None
736 # assume that there will be 2 outputs (one for change)
737 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
739 self.fee_e.setAmount(fee)
742 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
746 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
747 self.funds_error = True
748 text = _( "Not enough funds" )
749 c, u = self.wallet.get_frozen_balance()
750 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
752 self.statusBar().showMessage(text)
753 self.amount_e.setPalette(palette)
754 self.fee_e.setPalette(palette)
756 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
757 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
759 run_hook('create_send_tab', grid)
762 def from_list_delete(self, item):
763 i = self.from_list.indexOfTopLevelItem(item)
765 self.redraw_from_list()
767 def from_list_menu(self, position):
768 item = self.from_list.itemAt(position)
770 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
771 menu.exec_(self.from_list.viewport().mapToGlobal(position))
773 def set_pay_from(self, domain = None):
774 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
775 self.redraw_from_list()
777 def redraw_from_list(self):
778 self.from_list.clear()
779 self.from_label.setHidden(len(self.pay_from) == 0)
780 self.from_list.setHidden(len(self.pay_from) == 0)
783 h = x.get('prevout_hash')
784 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
786 for item in self.pay_from:
787 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
789 def update_completions(self):
791 for addr,label in self.wallet.labels.items():
792 if addr in self.wallet.addressbook:
793 l.append( label + ' <' + addr + '>')
795 run_hook('update_completions', l)
796 self.completions.setStringList(l)
800 return lambda s, *args: s.do_protect(func, args)
803 def read_send_tab(self):
804 label = unicode( self.message_e.text() )
806 if self.gui_object.payment_request:
807 outputs = self.gui_object.payment_request.get_outputs()
809 outputs = self.payto_e.get_outputs()
812 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
815 for addr, x in outputs:
816 if addr is None or not bitcoin.is_address(addr):
817 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
820 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
823 amount = sum(map(lambda x:x[1], outputs))
825 fee = self.fee_e.get_amount()
827 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
830 confirm_amount = self.config.get('confirm_amount', 100000000)
831 if amount >= confirm_amount:
832 o = '\n'.join(map(lambda x:x[0], outputs))
833 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
836 confirm_fee = self.config.get('confirm_fee', 100000)
837 if fee >= confirm_fee:
838 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()}):
841 coins = self.get_coins()
842 return outputs, fee, label, coins
846 r = self.read_send_tab()
849 outputs, fee, label, coins = r
850 self.send_tx(outputs, fee, label, coins)
854 def send_tx(self, outputs, fee, label, coins, password):
855 self.send_button.setDisabled(True)
857 # first, create an unsigned tx
859 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
861 except Exception as e:
862 traceback.print_exc(file=sys.stdout)
863 self.show_message(str(e))
864 self.send_button.setDisabled(False)
867 # call hook to see if plugin needs gui interaction
868 run_hook('send_tx', tx)
874 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
875 self.wallet.sign_transaction(tx, keypairs, password)
876 return tx, fee, label
878 def sign_done(tx, fee, label):
880 self.show_message(tx.error)
881 self.send_button.setDisabled(False)
883 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
884 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
885 self.send_button.setDisabled(False)
888 self.wallet.set_label(tx.hash(), label)
890 if not tx.is_complete() or self.config.get('show_before_broadcast'):
891 self.show_transaction(tx)
893 self.send_button.setDisabled(False)
896 self.broadcast_transaction(tx)
898 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
899 self.waiting_dialog.start()
903 def broadcast_transaction(self, tx):
905 def broadcast_thread():
906 pr = self.gui_object.payment_request
908 return self.wallet.sendtx(tx)
911 self.gui_object.payment_request = None
912 return False, _("Payment request has expired")
914 status, msg = self.wallet.sendtx(tx)
918 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_PAID, tx.hash())
919 self.wallet.storage.put('invoices', self.invoices)
920 self.update_invoices_tab()
921 self.gui_object.payment_request = None
922 refund_address = self.wallet.addresses()[0]
923 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
929 def broadcast_done(status, msg):
931 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
934 QMessageBox.warning(self, _('Error'), msg, _('OK'))
935 self.send_button.setDisabled(False)
937 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
938 self.waiting_dialog.start()
942 def prepare_for_payment_request(self):
943 self.tabs.setCurrentIndex(1)
944 self.payto_e.is_pr = True
945 for e in [self.payto_e, self.amount_e, self.message_e]:
947 for h in [self.payto_help, self.amount_help, self.message_help]:
949 self.payto_e.setText(_("please wait..."))
952 def payment_request_ok(self):
953 pr = self.gui_object.payment_request
955 if pr_id not in self.invoices:
956 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_UNPAID, None)
957 self.wallet.storage.put('invoices', self.invoices)
958 self.update_invoices_tab()
960 print_error('invoice already in list')
962 status = self.invoices[pr_id][3]
963 if status == PR_PAID:
965 self.show_message("invoice already paid")
966 self.gui_object.payment_request = None
969 self.payto_help.show()
970 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
972 self.payto_e.setGreen()
973 self.payto_e.setText(pr.domain)
974 self.amount_e.setText(self.format_amount(pr.get_amount()))
975 self.message_e.setText(pr.get_memo())
977 def payment_request_error(self):
979 self.show_message(self.gui_object.payment_request.error)
980 self.gui_object.payment_request = None
982 def set_send(self, address, amount, label, message):
984 if label and self.wallet.labels.get(address) != label:
985 if self.question('Give label "%s" to address %s ?'%(label,address)):
986 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
987 self.wallet.addressbook.append(address)
988 self.wallet.set_label(address, label)
990 self.tabs.setCurrentIndex(1)
991 label = self.wallet.labels.get(address)
992 m_addr = label + ' <'+ address +'>' if label else address
993 self.payto_e.setText(m_addr)
995 self.message_e.setText(message)
997 self.amount_e.setText(amount)
1001 self.payto_e.is_pr = False
1002 self.payto_sig.setVisible(False)
1003 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1007 for h in [self.payto_help, self.amount_help, self.message_help]:
1010 self.payto_help.set_alt(None)
1011 self.set_pay_from([])
1012 self.update_status()
1016 def set_addrs_frozen(self,addrs,freeze):
1018 if not addr: continue
1019 if addr in self.wallet.frozen_addresses and not freeze:
1020 self.wallet.unfreeze(addr)
1021 elif addr not in self.wallet.frozen_addresses and freeze:
1022 self.wallet.freeze(addr)
1023 self.update_receive_tab()
1027 def create_list_tab(self, headers):
1028 "generic tab creation method"
1029 l = MyTreeWidget(self)
1030 l.setColumnCount( len(headers) )
1031 l.setHeaderLabels( headers )
1034 vbox = QVBoxLayout()
1041 vbox.addWidget(buttons)
1046 def create_receive_tab(self):
1047 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1048 l.setContextMenuPolicy(Qt.CustomContextMenu)
1049 l.customContextMenuRequested.connect(self.create_receive_menu)
1050 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1051 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1052 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1053 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1054 self.receive_list = l
1060 def save_column_widths(self):
1061 self.column_widths["receive"] = []
1062 for i in range(self.receive_list.columnCount() -1):
1063 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1065 self.column_widths["history"] = []
1066 for i in range(self.history_list.columnCount() - 1):
1067 self.column_widths["history"].append(self.history_list.columnWidth(i))
1069 self.column_widths["contacts"] = []
1070 for i in range(self.contacts_list.columnCount() - 1):
1071 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1073 self.config.set_key("column_widths_2", self.column_widths, True)
1076 def create_contacts_tab(self):
1077 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1078 l.setContextMenuPolicy(Qt.CustomContextMenu)
1079 l.customContextMenuRequested.connect(self.create_contact_menu)
1080 for i,width in enumerate(self.column_widths['contacts']):
1081 l.setColumnWidth(i, width)
1083 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1084 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1085 self.contacts_list = l
1089 def create_invoices_tab(self):
1090 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1092 h.setStretchLastSection(False)
1093 h.setResizeMode(1, QHeaderView.Stretch)
1094 l.setContextMenuPolicy(Qt.CustomContextMenu)
1095 l.customContextMenuRequested.connect(self.create_invoice_menu)
1096 self.invoices_list = l
1099 def update_invoices_tab(self):
1100 invoices = self.wallet.storage.get('invoices', {})
1101 l = self.invoices_list
1103 for key, value in invoices.items():
1105 domain, memo, amount, status, tx_hash = value
1109 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1110 l.addTopLevelItem(item)
1112 l.setCurrentItem(l.topLevelItem(0))
1116 def delete_imported_key(self, addr):
1117 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1118 self.wallet.delete_imported_key(addr)
1119 self.update_receive_tab()
1120 self.update_history_tab()
1122 def edit_account_label(self, k):
1123 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1125 label = unicode(text)
1126 self.wallet.set_label(k,label)
1127 self.update_receive_tab()
1129 def account_set_expanded(self, item, k, b):
1131 self.accounts_expanded[k] = b
1133 def create_account_menu(self, position, k, item):
1135 if item.isExpanded():
1136 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1138 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1139 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1140 if self.wallet.seed_version > 4:
1141 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1142 if self.wallet.account_is_pending(k):
1143 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1144 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1146 def delete_pending_account(self, k):
1147 self.wallet.delete_pending_account(k)
1148 self.update_receive_tab()
1150 def create_receive_menu(self, position):
1151 # fixme: this function apparently has a side effect.
1152 # if it is not called the menu pops up several times
1153 #self.receive_list.selectedIndexes()
1155 selected = self.receive_list.selectedItems()
1156 multi_select = len(selected) > 1
1157 addrs = [unicode(item.text(0)) for item in selected]
1158 if not multi_select:
1159 item = self.receive_list.itemAt(position)
1163 if not is_valid(addr):
1164 k = str(item.data(0,32).toString())
1166 self.create_account_menu(position, k, item)
1168 item.setExpanded(not item.isExpanded())
1172 if not multi_select:
1173 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1174 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1175 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1176 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1177 if not self.wallet.is_watching_only():
1178 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1179 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1180 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1181 if self.wallet.is_imported(addr):
1182 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1184 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1185 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1186 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1187 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1189 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1190 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1192 run_hook('receive_menu', menu, addrs)
1193 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1196 def get_sendable_balance(self):
1197 return sum(map(lambda x:x['value'], self.get_coins()))
1200 def get_coins(self):
1202 return self.pay_from
1204 domain = self.wallet.get_account_addresses(self.current_account)
1205 for i in self.wallet.frozen_addresses:
1206 if i in domain: domain.remove(i)
1207 return self.wallet.get_unspent_coins(domain)
1210 def send_from_addresses(self, addrs):
1211 self.set_pay_from( addrs )
1212 self.tabs.setCurrentIndex(1)
1215 def payto(self, addr):
1217 label = self.wallet.labels.get(addr)
1218 m_addr = label + ' <' + addr + '>' if label else addr
1219 self.tabs.setCurrentIndex(1)
1220 self.payto_e.setText(m_addr)
1221 self.amount_e.setFocus()
1224 def delete_contact(self, x):
1225 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1226 self.wallet.delete_contact(x)
1227 self.wallet.set_label(x, None)
1228 self.update_history_tab()
1229 self.update_contacts_tab()
1230 self.update_completions()
1233 def create_contact_menu(self, position):
1234 item = self.contacts_list.itemAt(position)
1237 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1239 addr = unicode(item.text(0))
1240 label = unicode(item.text(1))
1241 is_editable = item.data(0,32).toBool()
1242 payto_addr = item.data(0,33).toString()
1243 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1244 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1245 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1247 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1248 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1250 run_hook('create_contact_menu', menu, item)
1251 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1253 def delete_invoice(self, key):
1254 self.invoices.pop(key)
1255 self.wallet.storage.put('invoices', self.invoices)
1256 self.update_invoices_tab()
1258 def show_invoice(self, key):
1259 from electrum.paymentrequest import PaymentRequest
1260 domain, memo, value, status, tx_hash = self.invoices[key]
1261 pr = PaymentRequest(self.config)
1265 self.show_pr_details(pr)
1267 def show_pr_details(self, pr):
1268 msg = 'Domain: ' + pr.domain
1269 msg += '\nStatus: ' + pr.get_status()
1270 msg += '\nMemo: ' + pr.get_memo()
1271 msg += '\nPayment URL: ' + pr.payment_url
1272 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1273 QMessageBox.information(self, 'Invoice', msg , 'OK')
1275 def do_pay_invoice(self, key):
1276 from electrum.paymentrequest import PaymentRequest
1277 domain, memo, value, status, tx_hash = self.invoices[key]
1278 pr = PaymentRequest(self.config)
1281 self.gui_object.payment_request = pr
1282 self.prepare_for_payment_request()
1284 self.payment_request_ok()
1286 self.payment_request_error()
1289 def create_invoice_menu(self, position):
1290 item = self.invoices_list.itemAt(position)
1293 k = self.invoices_list.indexOfTopLevelItem(item)
1294 key = self.invoices.keys()[k]
1295 domain, memo, value, status, tx_hash = self.invoices[key]
1297 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1298 if status == PR_UNPAID:
1299 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1300 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1301 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1304 def update_receive_item(self, item):
1305 item.setFont(0, QFont(MONOSPACE_FONT))
1306 address = str(item.data(0,0).toString())
1307 label = self.wallet.labels.get(address,'')
1308 item.setData(1,0,label)
1309 item.setData(0,32, True) # is editable
1311 run_hook('update_receive_item', address, item)
1313 if not self.wallet.is_mine(address): return
1315 c, u = self.wallet.get_addr_balance(address)
1316 balance = self.format_amount(c + u)
1317 item.setData(2,0,balance)
1319 if address in self.wallet.frozen_addresses:
1320 item.setBackgroundColor(0, QColor('lightblue'))
1323 def update_receive_tab(self):
1324 l = self.receive_list
1325 # extend the syntax for consistency
1326 l.addChild = l.addTopLevelItem
1327 l.insertChild = l.insertTopLevelItem
1330 for i,width in enumerate(self.column_widths['receive']):
1331 l.setColumnWidth(i, width)
1333 accounts = self.wallet.get_accounts()
1334 if self.current_account is None:
1335 account_items = sorted(accounts.items())
1337 account_items = [(self.current_account, accounts.get(self.current_account))]
1340 for k, account in account_items:
1342 if len(accounts) > 1:
1343 name = self.wallet.get_account_name(k)
1344 c,u = self.wallet.get_account_balance(k)
1345 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1346 l.addTopLevelItem(account_item)
1347 account_item.setExpanded(self.accounts_expanded.get(k, True))
1348 account_item.setData(0, 32, k)
1352 sequences = [0,1] if account.has_change() else [0]
1353 for is_change in sequences:
1354 if len(sequences) > 1:
1355 name = _("Receiving") if not is_change else _("Change")
1356 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1357 account_item.addChild(seq_item)
1359 seq_item.setExpanded(True)
1361 seq_item = account_item
1363 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1369 for address in account.get_addresses(is_change):
1371 num, is_used = self.wallet.is_used(address)
1374 if gap > self.wallet.gap_limit:
1379 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1380 self.update_receive_item(item)
1382 item.setBackgroundColor(1, QColor('red'))
1386 seq_item.insertChild(0,used_item)
1388 used_item.addChild(item)
1390 seq_item.addChild(item)
1392 # we use column 1 because column 0 may be hidden
1393 l.setCurrentItem(l.topLevelItem(0),1)
1396 def update_contacts_tab(self):
1397 l = self.contacts_list
1400 for address in self.wallet.addressbook:
1401 label = self.wallet.labels.get(address,'')
1402 n = self.wallet.get_num_tx(address)
1403 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1404 item.setFont(0, QFont(MONOSPACE_FONT))
1405 # 32 = label can be edited (bool)
1406 item.setData(0,32, True)
1408 item.setData(0,33, address)
1409 l.addTopLevelItem(item)
1411 run_hook('update_contacts_tab', l)
1412 l.setCurrentItem(l.topLevelItem(0))
1416 def create_console_tab(self):
1417 from console import Console
1418 self.console = console = Console()
1422 def update_console(self):
1423 console = self.console
1424 console.history = self.config.get("console-history",[])
1425 console.history_index = len(console.history)
1427 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1428 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1430 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1432 def mkfunc(f, method):
1433 return lambda *args: apply( f, (method, args, self.password_dialog ))
1435 if m[0]=='_' or m in ['network','wallet']: continue
1436 methods[m] = mkfunc(c._run, m)
1438 console.updateNamespace(methods)
1441 def change_account(self,s):
1442 if s == _("All accounts"):
1443 self.current_account = None
1445 accounts = self.wallet.get_account_names()
1446 for k, v in accounts.items():
1448 self.current_account = k
1449 self.update_history_tab()
1450 self.update_status()
1451 self.update_receive_tab()
1453 def create_status_bar(self):
1456 sb.setFixedHeight(35)
1457 qtVersion = qVersion()
1459 self.balance_label = QLabel("")
1460 sb.addWidget(self.balance_label)
1462 from version_getter import UpdateLabel
1463 self.updatelabel = UpdateLabel(self.config, sb)
1465 self.account_selector = QComboBox()
1466 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1467 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1468 sb.addPermanentWidget(self.account_selector)
1470 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1471 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1473 self.lock_icon = QIcon()
1474 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1475 sb.addPermanentWidget( self.password_button )
1477 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1478 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1479 sb.addPermanentWidget( self.seed_button )
1480 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1481 sb.addPermanentWidget( self.status_button )
1483 run_hook('create_status_bar', (sb,))
1485 self.setStatusBar(sb)
1488 def update_lock_icon(self):
1489 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1490 self.password_button.setIcon( icon )
1493 def update_buttons_on_seed(self):
1494 if self.wallet.has_seed():
1495 self.seed_button.show()
1497 self.seed_button.hide()
1499 if not self.wallet.is_watching_only():
1500 self.password_button.show()
1501 self.send_button.setText(_("Send"))
1503 self.password_button.hide()
1504 self.send_button.setText(_("Create unsigned transaction"))
1507 def change_password_dialog(self):
1508 from password_dialog import PasswordDialog
1509 d = PasswordDialog(self.wallet, self)
1511 self.update_lock_icon()
1514 def new_contact_dialog(self):
1517 d.setWindowTitle(_("New Contact"))
1518 vbox = QVBoxLayout(d)
1519 vbox.addWidget(QLabel(_('New Contact')+':'))
1521 grid = QGridLayout()
1524 grid.addWidget(QLabel(_("Address")), 1, 0)
1525 grid.addWidget(line1, 1, 1)
1526 grid.addWidget(QLabel(_("Name")), 2, 0)
1527 grid.addWidget(line2, 2, 1)
1529 vbox.addLayout(grid)
1530 vbox.addLayout(ok_cancel_buttons(d))
1535 address = str(line1.text())
1536 label = unicode(line2.text())
1538 if not is_valid(address):
1539 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1542 self.wallet.add_contact(address)
1544 self.wallet.set_label(address, label)
1546 self.update_contacts_tab()
1547 self.update_history_tab()
1548 self.update_completions()
1549 self.tabs.setCurrentIndex(3)
1553 def new_account_dialog(self, password):
1555 dialog = QDialog(self)
1557 dialog.setWindowTitle(_("New Account"))
1559 vbox = QVBoxLayout()
1560 vbox.addWidget(QLabel(_('Account name')+':'))
1563 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1564 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1569 vbox.addLayout(ok_cancel_buttons(dialog))
1570 dialog.setLayout(vbox)
1574 name = str(e.text())
1577 self.wallet.create_pending_account(name, password)
1578 self.update_receive_tab()
1579 self.tabs.setCurrentIndex(2)
1584 def show_master_public_keys(self):
1586 dialog = QDialog(self)
1588 dialog.setWindowTitle(_("Master Public Keys"))
1590 main_layout = QGridLayout()
1591 mpk_dict = self.wallet.get_master_public_keys()
1593 for key, value in mpk_dict.items():
1594 main_layout.addWidget(QLabel(key), i, 0)
1595 mpk_text = QTextEdit()
1596 mpk_text.setReadOnly(True)
1597 mpk_text.setMaximumHeight(170)
1598 mpk_text.setText(value)
1599 main_layout.addWidget(mpk_text, i + 1, 0)
1602 vbox = QVBoxLayout()
1603 vbox.addLayout(main_layout)
1604 vbox.addLayout(close_button(dialog))
1606 dialog.setLayout(vbox)
1611 def show_seed_dialog(self, password):
1612 if not self.wallet.has_seed():
1613 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1617 mnemonic = self.wallet.get_mnemonic(password)
1619 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1621 from seed_dialog import SeedDialog
1622 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1627 def show_qrcode(self, data, title = _("QR code")):
1631 d.setWindowTitle(title)
1632 d.setMinimumSize(270, 300)
1633 vbox = QVBoxLayout()
1634 qrw = QRCodeWidget(data)
1635 vbox.addWidget(qrw, 1)
1636 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1637 hbox = QHBoxLayout()
1640 filename = os.path.join(self.config.path, "qrcode.bmp")
1643 bmp.save_qrcode(qrw.qr, filename)
1644 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1646 def copy_to_clipboard():
1647 bmp.save_qrcode(qrw.qr, filename)
1648 self.app.clipboard().setImage(QImage(filename))
1649 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1651 b = QPushButton(_("Copy"))
1653 b.clicked.connect(copy_to_clipboard)
1655 b = QPushButton(_("Save"))
1657 b.clicked.connect(print_qr)
1659 b = QPushButton(_("Close"))
1661 b.clicked.connect(d.accept)
1664 vbox.addLayout(hbox)
1669 def do_protect(self, func, args):
1670 if self.wallet.use_encryption:
1671 password = self.password_dialog()
1677 if args != (False,):
1678 args = (self,) + args + (password,)
1680 args = (self,password)
1684 def show_public_keys(self, address):
1685 if not address: return
1687 pubkey_list = self.wallet.get_public_keys(address)
1688 except Exception as e:
1689 traceback.print_exc(file=sys.stdout)
1690 self.show_message(str(e))
1694 d.setMinimumSize(600, 200)
1696 vbox = QVBoxLayout()
1697 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1698 vbox.addWidget( QLabel(_("Public key") + ':'))
1700 keys.setReadOnly(True)
1701 keys.setText('\n'.join(pubkey_list))
1702 vbox.addWidget(keys)
1703 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1704 vbox.addLayout(close_button(d))
1709 def show_private_key(self, address, password):
1710 if not address: return
1712 pk_list = self.wallet.get_private_key(address, password)
1713 except Exception as e:
1714 traceback.print_exc(file=sys.stdout)
1715 self.show_message(str(e))
1719 d.setMinimumSize(600, 200)
1721 vbox = QVBoxLayout()
1722 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1723 vbox.addWidget( QLabel(_("Private key") + ':'))
1725 keys.setReadOnly(True)
1726 keys.setText('\n'.join(pk_list))
1727 vbox.addWidget(keys)
1728 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1729 vbox.addLayout(close_button(d))
1735 def do_sign(self, address, message, signature, password):
1736 message = unicode(message.toPlainText())
1737 message = message.encode('utf-8')
1739 sig = self.wallet.sign_message(str(address.text()), message, password)
1740 signature.setText(sig)
1741 except Exception as e:
1742 self.show_message(str(e))
1744 def do_verify(self, address, message, signature):
1745 message = unicode(message.toPlainText())
1746 message = message.encode('utf-8')
1747 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1748 self.show_message(_("Signature verified"))
1750 self.show_message(_("Error: wrong signature"))
1753 def sign_verify_message(self, address=''):
1756 d.setWindowTitle(_('Sign/verify Message'))
1757 d.setMinimumSize(410, 290)
1759 layout = QGridLayout(d)
1761 message_e = QTextEdit()
1762 layout.addWidget(QLabel(_('Message')), 1, 0)
1763 layout.addWidget(message_e, 1, 1)
1764 layout.setRowStretch(2,3)
1766 address_e = QLineEdit()
1767 address_e.setText(address)
1768 layout.addWidget(QLabel(_('Address')), 2, 0)
1769 layout.addWidget(address_e, 2, 1)
1771 signature_e = QTextEdit()
1772 layout.addWidget(QLabel(_('Signature')), 3, 0)
1773 layout.addWidget(signature_e, 3, 1)
1774 layout.setRowStretch(3,1)
1776 hbox = QHBoxLayout()
1778 b = QPushButton(_("Sign"))
1779 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1782 b = QPushButton(_("Verify"))
1783 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1786 b = QPushButton(_("Close"))
1787 b.clicked.connect(d.accept)
1789 layout.addLayout(hbox, 4, 1)
1794 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1796 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1797 message_e.setText(decrypted)
1798 except Exception as e:
1799 self.show_message(str(e))
1802 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1803 message = unicode(message_e.toPlainText())
1804 message = message.encode('utf-8')
1806 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1807 encrypted_e.setText(encrypted)
1808 except Exception as e:
1809 self.show_message(str(e))
1813 def encrypt_message(self, address = ''):
1816 d.setWindowTitle(_('Encrypt/decrypt Message'))
1817 d.setMinimumSize(610, 490)
1819 layout = QGridLayout(d)
1821 message_e = QTextEdit()
1822 layout.addWidget(QLabel(_('Message')), 1, 0)
1823 layout.addWidget(message_e, 1, 1)
1824 layout.setRowStretch(2,3)
1826 pubkey_e = QLineEdit()
1828 pubkey = self.wallet.getpubkeys(address)[0]
1829 pubkey_e.setText(pubkey)
1830 layout.addWidget(QLabel(_('Public key')), 2, 0)
1831 layout.addWidget(pubkey_e, 2, 1)
1833 encrypted_e = QTextEdit()
1834 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1835 layout.addWidget(encrypted_e, 3, 1)
1836 layout.setRowStretch(3,1)
1838 hbox = QHBoxLayout()
1839 b = QPushButton(_("Encrypt"))
1840 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1843 b = QPushButton(_("Decrypt"))
1844 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1847 b = QPushButton(_("Close"))
1848 b.clicked.connect(d.accept)
1851 layout.addLayout(hbox, 4, 1)
1855 def question(self, msg):
1856 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1858 def show_message(self, msg):
1859 QMessageBox.information(self, _('Message'), msg, _('OK'))
1861 def password_dialog(self, msg=None):
1864 d.setWindowTitle(_("Enter Password"))
1869 vbox = QVBoxLayout()
1871 msg = _('Please enter your password')
1872 vbox.addWidget(QLabel(msg))
1874 grid = QGridLayout()
1876 grid.addWidget(QLabel(_('Password')), 1, 0)
1877 grid.addWidget(pw, 1, 1)
1878 vbox.addLayout(grid)
1880 vbox.addLayout(ok_cancel_buttons(d))
1883 run_hook('password_dialog', pw, grid, 1)
1884 if not d.exec_(): return
1885 return unicode(pw.text())
1894 def tx_from_text(self, txt):
1895 "json or raw hexadecimal"
1898 tx = Transaction(txt)
1904 tx_dict = json.loads(str(txt))
1905 assert "hex" in tx_dict.keys()
1906 tx = Transaction(tx_dict["hex"])
1907 if tx_dict.has_key("input_info"):
1908 input_info = json.loads(tx_dict['input_info'])
1909 tx.add_input_info(input_info)
1912 traceback.print_exc(file=sys.stdout)
1915 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1919 def read_tx_from_file(self):
1920 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1924 with open(fileName, "r") as f:
1925 file_content = f.read()
1926 except (ValueError, IOError, os.error), reason:
1927 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1929 return self.tx_from_text(file_content)
1933 def sign_raw_transaction(self, tx, input_info, password):
1934 self.wallet.signrawtransaction(tx, input_info, [], password)
1936 def do_process_from_text(self):
1937 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1940 tx = self.tx_from_text(text)
1942 self.show_transaction(tx)
1944 def do_process_from_file(self):
1945 tx = self.read_tx_from_file()
1947 self.show_transaction(tx)
1949 def do_process_from_txid(self):
1950 from electrum import transaction
1951 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1953 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1955 tx = transaction.Transaction(r)
1957 self.show_transaction(tx)
1959 self.show_message("unknown transaction")
1961 def do_process_from_csvReader(self, csvReader):
1966 for position, row in enumerate(csvReader):
1968 if not is_valid(address):
1969 errors.append((position, address))
1971 amount = Decimal(row[1])
1972 amount = int(100000000*amount)
1973 outputs.append((address, amount))
1974 except (ValueError, IOError, os.error), reason:
1975 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1979 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1980 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1984 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1985 except Exception as e:
1986 self.show_message(str(e))
1989 self.show_transaction(tx)
1991 def do_process_from_csv_file(self):
1992 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1996 with open(fileName, "r") as f:
1997 csvReader = csv.reader(f)
1998 self.do_process_from_csvReader(csvReader)
1999 except (ValueError, IOError, os.error), reason:
2000 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2003 def do_process_from_csv_text(self):
2004 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2005 + _("Format: address, amount. One output per line"), _("Load CSV"))
2008 f = StringIO.StringIO(text)
2009 csvReader = csv.reader(f)
2010 self.do_process_from_csvReader(csvReader)
2015 def export_privkeys_dialog(self, password):
2016 if self.wallet.is_watching_only():
2017 self.show_message(_("This is a watching-only wallet"))
2021 d.setWindowTitle(_('Private keys'))
2022 d.setMinimumSize(850, 300)
2023 vbox = QVBoxLayout(d)
2025 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2026 _("Exposing a single private key can compromise your entire wallet!"),
2027 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2028 vbox.addWidget(QLabel(msg))
2034 defaultname = 'electrum-private-keys.csv'
2035 select_msg = _('Select file to export your private keys to')
2036 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2037 vbox.addLayout(hbox)
2039 h, b = ok_cancel_buttons2(d, _('Export'))
2044 addresses = self.wallet.addresses(True)
2046 def privkeys_thread():
2047 for addr in addresses:
2051 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2052 d.emit(SIGNAL('computing_privkeys'))
2053 d.emit(SIGNAL('show_privkeys'))
2055 def show_privkeys():
2056 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2060 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2061 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2062 threading.Thread(target=privkeys_thread).start()
2068 filename = filename_e.text()
2073 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2074 except (IOError, os.error), reason:
2075 export_error_label = _("Electrum was unable to produce a private key-export.")
2076 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2078 except Exception as e:
2079 self.show_message(str(e))
2082 self.show_message(_("Private keys exported."))
2085 def do_export_privkeys(self, fileName, pklist, is_csv):
2086 with open(fileName, "w+") as f:
2088 transaction = csv.writer(f)
2089 transaction.writerow(["address", "private_key"])
2090 for addr, pk in pklist.items():
2091 transaction.writerow(["%34s"%addr,pk])
2094 f.write(json.dumps(pklist, indent = 4))
2097 def do_import_labels(self):
2098 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2099 if not labelsFile: return
2101 f = open(labelsFile, 'r')
2104 for key, value in json.loads(data).items():
2105 self.wallet.set_label(key, value)
2106 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2107 except (IOError, os.error), reason:
2108 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2111 def do_export_labels(self):
2112 labels = self.wallet.labels
2114 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2116 with open(fileName, 'w+') as f:
2117 json.dump(labels, f)
2118 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2119 except (IOError, os.error), reason:
2120 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2123 def export_history_dialog(self):
2126 d.setWindowTitle(_('Export History'))
2127 d.setMinimumSize(400, 200)
2128 vbox = QVBoxLayout(d)
2130 defaultname = os.path.expanduser('~/electrum-history.csv')
2131 select_msg = _('Select file to export your wallet transactions to')
2133 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2134 vbox.addLayout(hbox)
2138 h, b = ok_cancel_buttons2(d, _('Export'))
2143 filename = filename_e.text()
2148 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2149 except (IOError, os.error), reason:
2150 export_error_label = _("Electrum was unable to produce a transaction export.")
2151 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2154 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2157 def do_export_history(self, wallet, fileName, is_csv):
2158 history = wallet.get_tx_history()
2160 for item in history:
2161 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2163 if timestamp is not None:
2165 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2166 except [RuntimeError, TypeError, NameError] as reason:
2167 time_string = "unknown"
2170 time_string = "unknown"
2172 time_string = "pending"
2174 if value is not None:
2175 value_string = format_satoshis(value, True)
2180 fee_string = format_satoshis(fee, True)
2185 label, is_default_label = wallet.get_label(tx_hash)
2186 label = label.encode('utf-8')
2190 balance_string = format_satoshis(balance, False)
2192 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2194 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2196 with open(fileName, "w+") as f:
2198 transaction = csv.writer(f)
2199 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2201 transaction.writerow(line)
2204 f.write(json.dumps(lines, indent = 4))
2207 def sweep_key_dialog(self):
2209 d.setWindowTitle(_('Sweep private keys'))
2210 d.setMinimumSize(600, 300)
2212 vbox = QVBoxLayout(d)
2213 vbox.addWidget(QLabel(_("Enter private keys")))
2215 keys_e = QTextEdit()
2216 keys_e.setTabChangesFocus(True)
2217 vbox.addWidget(keys_e)
2219 h, address_e = address_field(self.wallet.addresses())
2223 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2224 vbox.addLayout(hbox)
2225 button.setEnabled(False)
2228 addr = str(address_e.text())
2229 if bitcoin.is_address(addr):
2233 pk = str(keys_e.toPlainText()).strip()
2234 if Wallet.is_private_key(pk):
2237 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2238 keys_e.textChanged.connect(f)
2239 address_e.textChanged.connect(f)
2243 fee = self.wallet.fee
2244 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2245 self.show_transaction(tx)
2249 def do_import_privkey(self, password):
2250 if not self.wallet.has_imported_keys():
2251 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2252 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2253 + _('Are you sure you understand what you are doing?'), 3, 4)
2256 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2259 text = str(text).split()
2264 addr = self.wallet.import_key(key, password)
2265 except Exception as e:
2271 addrlist.append(addr)
2273 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2275 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2276 self.update_receive_tab()
2277 self.update_history_tab()
2280 def settings_dialog(self):
2282 d.setWindowTitle(_('Electrum Settings'))
2284 vbox = QVBoxLayout()
2285 grid = QGridLayout()
2286 grid.setColumnStretch(0,1)
2288 nz_label = QLabel(_('Display zeros') + ':')
2289 grid.addWidget(nz_label, 0, 0)
2290 nz_e = AmountEdit(None,True)
2291 nz_e.setText("%d"% self.num_zeros)
2292 grid.addWidget(nz_e, 0, 1)
2293 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2294 grid.addWidget(HelpButton(msg), 0, 2)
2295 if not self.config.is_modifiable('num_zeros'):
2296 for w in [nz_e, nz_label]: w.setEnabled(False)
2298 lang_label=QLabel(_('Language') + ':')
2299 grid.addWidget(lang_label, 1, 0)
2300 lang_combo = QComboBox()
2301 from electrum.i18n import languages
2302 lang_combo.addItems(languages.values())
2304 index = languages.keys().index(self.config.get("language",''))
2307 lang_combo.setCurrentIndex(index)
2308 grid.addWidget(lang_combo, 1, 1)
2309 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2310 if not self.config.is_modifiable('language'):
2311 for w in [lang_combo, lang_label]: w.setEnabled(False)
2314 fee_label = QLabel(_('Transaction fee') + ':')
2315 grid.addWidget(fee_label, 2, 0)
2316 fee_e = BTCAmountEdit(self.get_decimal_point)
2317 fee_e.setAmount(self.wallet.fee)
2318 grid.addWidget(fee_e, 2, 1)
2319 msg = _('Fee per kilobyte of transaction.') + '\n' \
2320 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2321 grid.addWidget(HelpButton(msg), 2, 2)
2322 if not self.config.is_modifiable('fee_per_kb'):
2323 for w in [fee_e, fee_label]: w.setEnabled(False)
2325 units = ['BTC', 'mBTC']
2326 unit_label = QLabel(_('Base unit') + ':')
2327 grid.addWidget(unit_label, 3, 0)
2328 unit_combo = QComboBox()
2329 unit_combo.addItems(units)
2330 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2331 grid.addWidget(unit_combo, 3, 1)
2332 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2333 + '\n1BTC=1000mBTC.\n' \
2334 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2336 usechange_cb = QCheckBox(_('Use change addresses'))
2337 usechange_cb.setChecked(self.wallet.use_change)
2338 grid.addWidget(usechange_cb, 4, 0)
2339 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2340 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2342 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2343 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2344 grid.addWidget(block_ex_label, 5, 0)
2345 block_ex_combo = QComboBox()
2346 block_ex_combo.addItems(block_explorers)
2347 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2348 grid.addWidget(block_ex_combo, 5, 1)
2349 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2351 show_tx = self.config.get('show_before_broadcast', False)
2352 showtx_cb = QCheckBox(_('Show before broadcast'))
2353 showtx_cb.setChecked(show_tx)
2354 grid.addWidget(showtx_cb, 6, 0)
2355 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2357 vbox.addLayout(grid)
2359 vbox.addLayout(ok_cancel_buttons(d))
2363 if not d.exec_(): return
2365 fee = fee_e.get_amount()
2367 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2370 self.wallet.set_fee(fee)
2372 nz = unicode(nz_e.text())
2377 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2380 if self.num_zeros != nz:
2382 self.config.set_key('num_zeros', nz, True)
2383 self.update_history_tab()
2384 self.update_receive_tab()
2386 usechange_result = usechange_cb.isChecked()
2387 if self.wallet.use_change != usechange_result:
2388 self.wallet.use_change = usechange_result
2389 self.wallet.storage.put('use_change', self.wallet.use_change)
2391 if showtx_cb.isChecked() != show_tx:
2392 self.config.set_key('show_before_broadcast', not show_tx)
2394 unit_result = units[unit_combo.currentIndex()]
2395 if self.base_unit() != unit_result:
2396 self.decimal_point = 8 if unit_result == 'BTC' else 5
2397 self.config.set_key('decimal_point', self.decimal_point, True)
2398 self.update_history_tab()
2399 self.update_status()
2401 need_restart = False
2403 lang_request = languages.keys()[lang_combo.currentIndex()]
2404 if lang_request != self.config.get('language'):
2405 self.config.set_key("language", lang_request, True)
2408 be_result = block_explorers[block_ex_combo.currentIndex()]
2409 self.config.set_key('block_explorer', be_result, True)
2411 run_hook('close_settings_dialog')
2414 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2417 def run_network_dialog(self):
2418 if not self.network:
2420 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2422 def closeEvent(self, event):
2424 self.config.set_key("is_maximized", self.isMaximized())
2425 if not self.isMaximized():
2427 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2428 self.save_column_widths()
2429 self.config.set_key("console-history", self.console.history[-50:], True)
2430 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2434 def plugins_dialog(self):
2435 from electrum.plugins import plugins
2438 d.setWindowTitle(_('Electrum Plugins'))
2441 vbox = QVBoxLayout(d)
2444 scroll = QScrollArea()
2445 scroll.setEnabled(True)
2446 scroll.setWidgetResizable(True)
2447 scroll.setMinimumSize(400,250)
2448 vbox.addWidget(scroll)
2452 w.setMinimumHeight(len(plugins)*35)
2454 grid = QGridLayout()
2455 grid.setColumnStretch(0,1)
2458 def do_toggle(cb, p, w):
2461 if w: w.setEnabled(r)
2463 def mk_toggle(cb, p, w):
2464 return lambda: do_toggle(cb,p,w)
2466 for i, p in enumerate(plugins):
2468 cb = QCheckBox(p.fullname())
2469 cb.setDisabled(not p.is_available())
2470 cb.setChecked(p.is_enabled())
2471 grid.addWidget(cb, i, 0)
2472 if p.requires_settings():
2473 w = p.settings_widget(self)
2474 w.setEnabled( p.is_enabled() )
2475 grid.addWidget(w, i, 1)
2478 cb.clicked.connect(mk_toggle(cb,p,w))
2479 grid.addWidget(HelpButton(p.description()), i, 2)
2481 print_msg(_("Error: cannot display plugin"), p)
2482 traceback.print_exc(file=sys.stdout)
2483 grid.setRowStretch(i+1,1)
2485 vbox.addLayout(close_button(d))
2490 def show_account_details(self, k):
2491 account = self.wallet.accounts[k]
2494 d.setWindowTitle(_('Account Details'))
2497 vbox = QVBoxLayout(d)
2498 name = self.wallet.get_account_name(k)
2499 label = QLabel('Name: ' + name)
2500 vbox.addWidget(label)
2502 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2504 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2506 vbox.addWidget(QLabel(_('Master Public Key:')))
2509 text.setReadOnly(True)
2510 text.setMaximumHeight(170)
2511 vbox.addWidget(text)
2513 mpk_text = '\n'.join( account.get_master_pubkeys() )
2514 text.setText(mpk_text)
2516 vbox.addLayout(close_button(d))