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
45 from electrum import bmp, pyqrnative
47 from amountedit import AmountEdit, MyLineEdit
48 from network_dialog import NetworkDialog
49 from qrcodewidget import QRCodeWidget
51 from decimal import Decimal
59 if platform.system() == 'Windows':
60 MONOSPACE_FONT = 'Lucida Console'
61 elif platform.system() == 'Darwin':
62 MONOSPACE_FONT = 'Monaco'
64 MONOSPACE_FONT = 'monospace'
66 from electrum import ELECTRUM_VERSION
76 class StatusBarButton(QPushButton):
77 def __init__(self, icon, tooltip, func):
78 QPushButton.__init__(self, icon, '')
79 self.setToolTip(tooltip)
81 self.setMaximumWidth(25)
82 self.clicked.connect(func)
84 self.setIconSize(QSize(25,25))
86 def keyPressEvent(self, e):
87 if e.key() == QtCore.Qt.Key_Return:
99 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
101 class ElectrumWindow(QMainWindow):
105 def __init__(self, config, network, gui_object):
106 QMainWindow.__init__(self)
109 self.network = network
110 self.gui_object = gui_object
111 self.tray = gui_object.tray
112 self.go_lite = gui_object.go_lite
115 self.create_status_bar()
116 self.need_update = threading.Event()
118 self.decimal_point = config.get('decimal_point', 5)
119 self.num_zeros = int(config.get('num_zeros',0))
121 set_language(config.get('language'))
123 self.funds_error = False
124 self.completions = QStringListModel()
126 self.tabs = tabs = QTabWidget(self)
127 self.column_widths = self.config.get("column_widths_2", default_column_widths )
128 tabs.addTab(self.create_history_tab(), _('History') )
129 tabs.addTab(self.create_send_tab(), _('Send') )
130 tabs.addTab(self.create_receive_tab(), _('Receive') )
131 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
132 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
133 tabs.addTab(self.create_console_tab(), _('Console') )
134 tabs.setMinimumSize(600, 400)
135 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
136 self.setCentralWidget(tabs)
138 g = self.config.get("winpos-qt",[100, 100, 840, 400])
139 self.setGeometry(g[0], g[1], g[2], g[3])
140 if self.config.get("is_maximized"):
143 self.setWindowIcon(QIcon(":icons/electrum.png"))
146 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
147 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
148 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
149 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
150 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
152 for i in range(tabs.count()):
153 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
155 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
156 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
157 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
158 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
159 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
161 self.history_list.setFocus(True)
165 self.network.register_callback('updated', lambda: self.need_update.set())
166 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
167 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
168 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
169 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
171 # set initial message
172 self.console.showMessage(self.network.banner)
177 def update_account_selector(self):
179 accounts = self.wallet.get_account_names()
180 self.account_selector.clear()
181 if len(accounts) > 1:
182 self.account_selector.addItems([_("All accounts")] + accounts.values())
183 self.account_selector.setCurrentIndex(0)
184 self.account_selector.show()
186 self.account_selector.hide()
189 def load_wallet(self, wallet):
193 self.update_wallet_format()
195 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
196 self.current_account = self.wallet.storage.get("current_account", None)
197 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
198 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
199 self.setWindowTitle( title )
201 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
202 self.notify_transactions()
203 self.update_account_selector()
205 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
206 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
207 self.password_menu.setEnabled(not self.wallet.is_watching_only())
208 self.seed_menu.setEnabled(self.wallet.has_seed())
209 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
210 self.import_menu.setEnabled(self.wallet.can_import())
212 self.update_lock_icon()
213 self.update_buttons_on_seed()
214 self.update_console()
216 run_hook('load_wallet', wallet)
219 def update_wallet_format(self):
220 # convert old-format imported keys
221 if self.wallet.imported_keys:
222 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
224 self.wallet.convert_imported_keys(password)
226 self.show_message("error")
229 def open_wallet(self):
230 wallet_folder = self.wallet.storage.path
231 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
235 storage = WalletStorage({'wallet_path': filename})
236 if not storage.file_exists:
237 self.show_message("file not found "+ filename)
240 self.wallet.stop_threads()
243 wallet = Wallet(storage)
244 wallet.start_threads(self.network)
246 self.load_wallet(wallet)
250 def backup_wallet(self):
252 path = self.wallet.storage.path
253 wallet_folder = os.path.dirname(path)
254 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
258 new_path = os.path.join(wallet_folder, filename)
261 shutil.copy2(path, new_path)
262 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
263 except (IOError, os.error), reason:
264 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
267 def new_wallet(self):
270 wallet_folder = os.path.dirname(self.wallet.storage.path)
271 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
274 filename = os.path.join(wallet_folder, filename)
276 storage = WalletStorage({'wallet_path': filename})
277 if storage.file_exists:
278 QMessageBox.critical(None, "Error", _("File exists"))
281 wizard = installwizard.InstallWizard(self.config, self.network, storage)
282 wallet = wizard.run('new')
284 self.load_wallet(wallet)
288 def init_menubar(self):
291 file_menu = menubar.addMenu(_("&File"))
292 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
293 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
294 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
295 file_menu.addAction(_("&Quit"), self.close)
297 wallet_menu = menubar.addMenu(_("&Wallet"))
298 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
299 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
301 wallet_menu.addSeparator()
303 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
304 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
305 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
307 wallet_menu.addSeparator()
308 labels_menu = wallet_menu.addMenu(_("&Labels"))
309 labels_menu.addAction(_("&Import"), self.do_import_labels)
310 labels_menu.addAction(_("&Export"), self.do_export_labels)
312 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
313 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
314 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
315 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
316 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
318 tools_menu = menubar.addMenu(_("&Tools"))
320 # Settings / Preferences are all reserved keywords in OSX using this as work around
321 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
322 tools_menu.addAction(_("&Network"), self.run_network_dialog)
323 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
324 tools_menu.addSeparator()
325 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
326 #tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
327 tools_menu.addSeparator()
329 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
330 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
331 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
333 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
334 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
335 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
336 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
338 help_menu = menubar.addMenu(_("&Help"))
339 help_menu.addAction(_("&About"), self.show_about)
340 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
341 help_menu.addSeparator()
342 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
343 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
345 self.setMenuBar(menubar)
347 def show_about(self):
348 QMessageBox.about(self, "Electrum",
349 _("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."))
351 def show_report_bug(self):
352 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
353 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
356 def notify_transactions(self):
357 if not self.network or not self.network.is_connected():
360 print_error("Notifying GUI")
361 if len(self.network.pending_transactions_for_notifications) > 0:
362 # Combine the transactions if there are more then three
363 tx_amount = len(self.network.pending_transactions_for_notifications)
366 for tx in self.network.pending_transactions_for_notifications:
367 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
371 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
372 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
374 self.network.pending_transactions_for_notifications = []
376 for tx in self.network.pending_transactions_for_notifications:
378 self.network.pending_transactions_for_notifications.remove(tx)
379 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
381 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
383 def notify(self, message):
384 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
388 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
389 def getOpenFileName(self, title, filter = ""):
390 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
391 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
392 if fileName and directory != os.path.dirname(fileName):
393 self.config.set_key('io_dir', os.path.dirname(fileName), True)
396 def getSaveFileName(self, title, filename, filter = ""):
397 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
398 path = os.path.join( directory, filename )
399 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
400 if fileName and directory != os.path.dirname(fileName):
401 self.config.set_key('io_dir', os.path.dirname(fileName), True)
405 QMainWindow.close(self)
406 run_hook('close_main_window')
408 def connect_slots(self, sender):
409 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
410 self.previous_payto_e=''
412 def timer_actions(self):
413 if self.need_update.is_set():
415 self.need_update.clear()
416 run_hook('timer_actions')
418 def format_amount(self, x, is_diff=False, whitespaces=False):
419 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
422 def get_decimal_point(self):
423 return self.decimal_point
427 assert self.decimal_point in [5,8]
428 return "BTC" if self.decimal_point == 8 else "mBTC"
431 def update_status(self):
432 if self.network is None or not self.network.is_running():
434 icon = QIcon(":icons/status_disconnected.png")
436 elif self.network.is_connected():
437 if not self.wallet.up_to_date:
438 text = _("Synchronizing...")
439 icon = QIcon(":icons/status_waiting.png")
440 elif self.network.server_lag > 1:
441 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
442 icon = QIcon(":icons/status_lagging.png")
444 c, u = self.wallet.get_account_balance(self.current_account)
445 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
446 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
448 # append fiat balance and price from exchange rate plugin
450 run_hook('get_fiat_status_text', c+u, r)
455 self.tray.setToolTip(text)
456 icon = QIcon(":icons/status_connected.png")
458 text = _("Not connected")
459 icon = QIcon(":icons/status_disconnected.png")
461 self.balance_label.setText(text)
462 self.status_button.setIcon( icon )
465 def update_wallet(self):
467 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
468 self.update_history_tab()
469 self.update_receive_tab()
470 self.update_contacts_tab()
471 self.update_completions()
472 self.update_invoices_tab()
475 def create_history_tab(self):
476 self.history_list = l = MyTreeWidget(self)
478 for i,width in enumerate(self.column_widths['history']):
479 l.setColumnWidth(i, width)
480 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
481 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
482 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
484 l.customContextMenuRequested.connect(self.create_history_menu)
488 def create_history_menu(self, position):
489 self.history_list.selectedIndexes()
490 item = self.history_list.currentItem()
491 be = self.config.get('block_explorer', 'Blockchain.info')
492 if be == 'Blockchain.info':
493 block_explorer = 'https://blockchain.info/tx/'
494 elif be == 'Blockr.io':
495 block_explorer = 'https://blockr.io/tx/info/'
496 elif be == 'Insight.is':
497 block_explorer = 'http://live.insight.is/tx/'
499 tx_hash = str(item.data(0, Qt.UserRole).toString())
500 if not tx_hash: return
502 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
503 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
504 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
505 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
506 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
509 def show_transaction(self, tx):
510 import transaction_dialog
511 d = transaction_dialog.TxDialog(tx, self)
514 def tx_label_clicked(self, item, column):
515 if column==2 and item.isSelected():
517 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
518 self.history_list.editItem( item, column )
519 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
522 def tx_label_changed(self, item, column):
526 tx_hash = str(item.data(0, Qt.UserRole).toString())
527 tx = self.wallet.transactions.get(tx_hash)
528 text = unicode( item.text(2) )
529 self.wallet.set_label(tx_hash, text)
531 item.setForeground(2, QBrush(QColor('black')))
533 text = self.wallet.get_default_label(tx_hash)
534 item.setText(2, text)
535 item.setForeground(2, QBrush(QColor('gray')))
539 def edit_label(self, is_recv):
540 l = self.receive_list if is_recv else self.contacts_list
541 item = l.currentItem()
542 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
543 l.editItem( item, 1 )
544 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
548 def address_label_clicked(self, item, column, l, column_addr, column_label):
549 if column == column_label and item.isSelected():
550 is_editable = item.data(0, 32).toBool()
553 addr = unicode( item.text(column_addr) )
554 label = unicode( item.text(column_label) )
555 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
556 l.editItem( item, column )
557 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
560 def address_label_changed(self, item, column, l, column_addr, column_label):
561 if column == column_label:
562 addr = unicode( item.text(column_addr) )
563 text = unicode( item.text(column_label) )
564 is_editable = item.data(0, 32).toBool()
568 changed = self.wallet.set_label(addr, text)
570 self.update_history_tab()
571 self.update_completions()
573 self.current_item_changed(item)
575 run_hook('item_changed', item, column)
578 def current_item_changed(self, a):
579 run_hook('current_item_changed', a)
583 def update_history_tab(self):
585 self.history_list.clear()
586 for item in self.wallet.get_tx_history(self.current_account):
587 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
588 time_str = _("unknown")
591 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
593 time_str = _("error")
596 time_str = 'unverified'
597 icon = QIcon(":icons/unconfirmed.png")
600 icon = QIcon(":icons/unconfirmed.png")
602 icon = QIcon(":icons/clock%d.png"%conf)
604 icon = QIcon(":icons/confirmed.png")
606 if value is not None:
607 v_str = self.format_amount(value, True, whitespaces=True)
611 balance_str = self.format_amount(balance, whitespaces=True)
614 label, is_default_label = self.wallet.get_label(tx_hash)
616 label = _('Pruned transaction outputs')
617 is_default_label = False
619 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
620 item.setFont(2, QFont(MONOSPACE_FONT))
621 item.setFont(3, QFont(MONOSPACE_FONT))
622 item.setFont(4, QFont(MONOSPACE_FONT))
624 item.setForeground(3, QBrush(QColor("#BC1E1E")))
626 item.setData(0, Qt.UserRole, tx_hash)
627 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
629 item.setForeground(2, QBrush(QColor('grey')))
631 item.setIcon(0, icon)
632 self.history_list.insertTopLevelItem(0,item)
635 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
636 run_hook('history_tab_update')
639 def create_send_tab(self):
642 grid = QGridLayout(w)
644 grid.setColumnMinimumWidth(3,300)
645 grid.setColumnStretch(5,1)
646 grid.setRowStretch(8, 1)
648 from paytoedit import PayToEdit
649 self.amount_e = AmountEdit(self.get_decimal_point)
650 self.payto_e = PayToEdit(self.amount_e)
651 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)'))
652 grid.addWidget(QLabel(_('Pay to')), 1, 0)
653 grid.addWidget(self.payto_e, 1, 1, 1, 3)
654 grid.addWidget(self.payto_help, 1, 4)
656 completer = QCompleter()
657 completer.setCaseSensitivity(False)
658 self.payto_e.setCompleter(completer)
659 completer.setModel(self.completions)
661 self.message_e = MyLineEdit()
662 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.'))
663 grid.addWidget(QLabel(_('Description')), 2, 0)
664 grid.addWidget(self.message_e, 2, 1, 1, 3)
665 grid.addWidget(self.message_help, 2, 4)
667 self.from_label = QLabel(_('From'))
668 grid.addWidget(self.from_label, 3, 0)
669 self.from_list = MyTreeWidget(self)
670 self.from_list.setColumnCount(2)
671 self.from_list.setColumnWidth(0, 350)
672 self.from_list.setColumnWidth(1, 50)
673 self.from_list.setHeaderHidden(True)
674 self.from_list.setMaximumHeight(80)
675 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
676 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
677 grid.addWidget(self.from_list, 3, 1, 1, 3)
678 self.set_pay_from([])
680 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
681 + _('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.') \
682 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
683 grid.addWidget(QLabel(_('Amount')), 4, 0)
684 grid.addWidget(self.amount_e, 4, 1, 1, 2)
685 grid.addWidget(self.amount_help, 4, 3)
687 self.fee_e = AmountEdit(self.get_decimal_point)
688 grid.addWidget(QLabel(_('Fee')), 5, 0)
689 grid.addWidget(self.fee_e, 5, 1, 1, 2)
690 grid.addWidget(HelpButton(
691 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
692 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
693 + _('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)
695 run_hook('exchange_rate_button', grid)
697 self.send_button = EnterButton(_("Send"), self.do_send)
698 grid.addWidget(self.send_button, 6, 1)
700 b = EnterButton(_("Clear"), self.do_clear)
701 grid.addWidget(b, 6, 2)
703 self.payto_sig = QLabel('')
704 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
706 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
707 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
710 def entry_changed( is_fee ):
711 self.funds_error = False
713 if self.amount_e.is_shortcut:
714 self.amount_e.is_shortcut = False
715 sendable = self.get_sendable_balance()
716 # there is only one output because we are completely spending inputs
717 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
718 fee = self.wallet.estimated_fee(inputs, 1)
720 self.amount_e.setText( self.format_amount(amount) )
721 self.fee_e.setText( self.format_amount( fee ) )
724 amount = self.amount_e.get_amount()
725 fee = self.fee_e.get_amount()
727 if not is_fee: fee = None
730 # assume that there will be 2 outputs (one for change)
731 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
733 self.fee_e.setText( self.format_amount( fee ) )
736 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
740 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
741 self.funds_error = True
742 text = _( "Not enough funds" )
743 c, u = self.wallet.get_frozen_balance()
744 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
746 self.statusBar().showMessage(text)
747 self.amount_e.setPalette(palette)
748 self.fee_e.setPalette(palette)
750 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
751 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
753 run_hook('create_send_tab', grid)
756 def from_list_delete(self, item):
757 i = self.from_list.indexOfTopLevelItem(item)
759 self.redraw_from_list()
761 def from_list_menu(self, position):
762 item = self.from_list.itemAt(position)
764 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
765 menu.exec_(self.from_list.viewport().mapToGlobal(position))
767 def set_pay_from(self, domain = None):
768 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
769 self.redraw_from_list()
771 def redraw_from_list(self):
772 self.from_list.clear()
773 self.from_label.setHidden(len(self.pay_from) == 0)
774 self.from_list.setHidden(len(self.pay_from) == 0)
777 h = x.get('prevout_hash')
778 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
780 for item in self.pay_from:
781 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
783 def update_completions(self):
785 for addr,label in self.wallet.labels.items():
786 if addr in self.wallet.addressbook:
787 l.append( label + ' <' + addr + '>')
789 run_hook('update_completions', l)
790 self.completions.setStringList(l)
794 return lambda s, *args: s.do_protect(func, args)
798 label = unicode( self.message_e.text() )
800 if self.gui_object.payment_request:
801 outputs = self.gui_object.payment_request.outputs
803 outputs = self.payto_e.get_outputs()
806 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
809 for addr, x in outputs:
810 if addr is None or not bitcoin.is_address(addr):
811 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
814 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
817 amount = sum(map(lambda x:x[1], outputs))
820 fee = self.fee_e.get_amount()
822 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
825 confirm_amount = self.config.get('confirm_amount', 100000000)
826 if amount >= confirm_amount:
827 o = '\n'.join(map(lambda x:x[0], outputs))
828 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
831 confirm_fee = self.config.get('confirm_fee', 100000)
832 if fee >= confirm_fee:
833 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()}):
836 self.send_tx(outputs, fee, label)
841 def send_tx(self, outputs, fee, label, password):
842 self.send_button.setDisabled(True)
844 # first, create an unsigned tx
845 coins = self.get_coins()
847 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
849 except Exception as e:
850 traceback.print_exc(file=sys.stdout)
851 self.show_message(str(e))
852 self.send_button.setDisabled(False)
855 # call hook to see if plugin needs gui interaction
856 run_hook('send_tx', tx)
862 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
863 self.wallet.sign_transaction(tx, keypairs, password)
864 return tx, fee, label
866 def sign_done(tx, fee, label):
868 self.show_message(tx.error)
869 self.send_button.setDisabled(False)
871 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
872 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
873 self.send_button.setDisabled(False)
876 self.wallet.set_label(tx.hash(), label)
878 if not tx.is_complete() or self.config.get('show_before_broadcast'):
879 self.show_transaction(tx)
881 self.send_button.setDisabled(False)
884 self.broadcast_transaction(tx)
886 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
887 self.waiting_dialog.start()
891 def broadcast_transaction(self, tx):
893 def broadcast_thread():
894 if self.gui_object.payment_request:
895 refund_address = self.wallet.addresses()[0]
896 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
897 self.gui_object.payment_request = None
899 status, msg = self.wallet.sendtx(tx)
902 def broadcast_done(status, msg):
904 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
907 QMessageBox.warning(self, _('Error'), msg, _('OK'))
908 self.send_button.setDisabled(False)
910 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
911 self.waiting_dialog.start()
915 def prepare_for_payment_request(self):
916 self.tabs.setCurrentIndex(1)
917 self.payto_e.is_pr = True
918 for e in [self.payto_e, self.amount_e, self.message_e]:
920 for h in [self.payto_help, self.amount_help, self.message_help]:
922 self.payto_e.setText(_("please wait..."))
925 def payment_request_ok(self):
926 pr = self.gui_object.payment_request
929 invoices = self.wallet.storage.get('invoices', {})
930 invoices[pr_id] = (pr.get_domain(), pr.get_amount())
931 invoices = self.wallet.storage.put('invoices', invoices)
932 self.update_invoices_tab()
934 self.payto_help.show()
935 self.payto_help.set_alt(pr.status)
936 self.payto_e.setGreen()
937 self.payto_e.setText(pr.domain)
938 self.amount_e.setText(self.format_amount(pr.get_amount()))
939 self.message_e.setText(pr.memo)
941 def payment_request_error(self):
943 self.show_message(self.gui_object.payment_request.error)
944 self.gui_object.payment_request = None
946 def set_send(self, address, amount, label, message):
948 if label and self.wallet.labels.get(address) != label:
949 if self.question('Give label "%s" to address %s ?'%(label,address)):
950 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
951 self.wallet.addressbook.append(address)
952 self.wallet.set_label(address, label)
954 self.tabs.setCurrentIndex(1)
955 label = self.wallet.labels.get(address)
956 m_addr = label + ' <'+ address +'>' if label else address
957 self.payto_e.setText(m_addr)
959 self.message_e.setText(message)
961 self.amount_e.setText(amount)
965 self.payto_e.is_pr = False
966 self.payto_sig.setVisible(False)
967 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
971 for h in [self.payto_help, self.amount_help, self.message_help]:
974 self.payto_help.set_alt(None)
976 self.set_pay_from([])
981 def set_addrs_frozen(self,addrs,freeze):
983 if not addr: continue
984 if addr in self.wallet.frozen_addresses and not freeze:
985 self.wallet.unfreeze(addr)
986 elif addr not in self.wallet.frozen_addresses and freeze:
987 self.wallet.freeze(addr)
988 self.update_receive_tab()
992 def create_list_tab(self, headers):
993 "generic tab creation method"
994 l = MyTreeWidget(self)
995 l.setColumnCount( len(headers) )
996 l.setHeaderLabels( headers )
1006 vbox.addWidget(buttons)
1008 hbox = QHBoxLayout()
1011 buttons.setLayout(hbox)
1016 def create_receive_tab(self):
1017 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1018 l.setContextMenuPolicy(Qt.CustomContextMenu)
1019 l.customContextMenuRequested.connect(self.create_receive_menu)
1020 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1021 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1022 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1023 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1024 self.receive_list = l
1025 self.receive_buttons_hbox = hbox
1032 def save_column_widths(self):
1033 self.column_widths["receive"] = []
1034 for i in range(self.receive_list.columnCount() -1):
1035 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1037 self.column_widths["history"] = []
1038 for i in range(self.history_list.columnCount() - 1):
1039 self.column_widths["history"].append(self.history_list.columnWidth(i))
1041 self.column_widths["contacts"] = []
1042 for i in range(self.contacts_list.columnCount() - 1):
1043 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1045 self.config.set_key("column_widths_2", self.column_widths, True)
1048 def create_contacts_tab(self):
1049 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1050 l.setContextMenuPolicy(Qt.CustomContextMenu)
1051 l.customContextMenuRequested.connect(self.create_contact_menu)
1052 for i,width in enumerate(self.column_widths['contacts']):
1053 l.setColumnWidth(i, width)
1055 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1056 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1057 self.contacts_list = l
1058 self.contacts_buttons_hbox = hbox
1063 def create_invoices_tab(self):
1064 l,w,hbox = self.create_list_tab([_('Recipient'), _('Amount'), _('Status')])
1065 l.setContextMenuPolicy(Qt.CustomContextMenu)
1066 #l.customContextMenuRequested.connect(self.create_contact_menu)
1067 #self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1068 #self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1069 self.invoices_list = l
1073 def update_invoices_tab(self):
1074 invoices = self.wallet.storage.get('invoices', {})
1075 l = self.invoices_list
1078 for item, value in invoices.items():
1079 domain, amount = value
1080 item = QTreeWidgetItem( [ domain, self.format_amount(amount), ""] )
1081 l.addTopLevelItem(item)
1083 l.setCurrentItem(l.topLevelItem(0))
1087 def delete_imported_key(self, addr):
1088 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1089 self.wallet.delete_imported_key(addr)
1090 self.update_receive_tab()
1091 self.update_history_tab()
1093 def edit_account_label(self, k):
1094 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1096 label = unicode(text)
1097 self.wallet.set_label(k,label)
1098 self.update_receive_tab()
1100 def account_set_expanded(self, item, k, b):
1102 self.accounts_expanded[k] = b
1104 def create_account_menu(self, position, k, item):
1106 if item.isExpanded():
1107 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1109 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1110 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1111 if self.wallet.seed_version > 4:
1112 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1113 if self.wallet.account_is_pending(k):
1114 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1115 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1117 def delete_pending_account(self, k):
1118 self.wallet.delete_pending_account(k)
1119 self.update_receive_tab()
1121 def create_receive_menu(self, position):
1122 # fixme: this function apparently has a side effect.
1123 # if it is not called the menu pops up several times
1124 #self.receive_list.selectedIndexes()
1126 selected = self.receive_list.selectedItems()
1127 multi_select = len(selected) > 1
1128 addrs = [unicode(item.text(0)) for item in selected]
1129 if not multi_select:
1130 item = self.receive_list.itemAt(position)
1134 if not is_valid(addr):
1135 k = str(item.data(0,32).toString())
1137 self.create_account_menu(position, k, item)
1139 item.setExpanded(not item.isExpanded())
1143 if not multi_select:
1144 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1145 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1146 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1147 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1148 if not self.wallet.is_watching_only():
1149 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1150 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1151 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1152 if self.wallet.is_imported(addr):
1153 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1155 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1156 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1157 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1158 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1160 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1161 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1163 run_hook('receive_menu', menu, addrs)
1164 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1167 def get_sendable_balance(self):
1168 return sum(map(lambda x:x['value'], self.get_coins()))
1171 def get_coins(self):
1173 return self.pay_from
1175 domain = self.wallet.get_account_addresses(self.current_account)
1176 for i in self.wallet.frozen_addresses:
1177 if i in domain: domain.remove(i)
1178 return self.wallet.get_unspent_coins(domain)
1181 def send_from_addresses(self, addrs):
1182 self.set_pay_from( addrs )
1183 self.tabs.setCurrentIndex(1)
1186 def payto(self, addr):
1188 label = self.wallet.labels.get(addr)
1189 m_addr = label + ' <' + addr + '>' if label else addr
1190 self.tabs.setCurrentIndex(1)
1191 self.payto_e.setText(m_addr)
1192 self.amount_e.setFocus()
1195 def delete_contact(self, x):
1196 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1197 self.wallet.delete_contact(x)
1198 self.wallet.set_label(x, None)
1199 self.update_history_tab()
1200 self.update_contacts_tab()
1201 self.update_completions()
1204 def create_contact_menu(self, position):
1205 item = self.contacts_list.itemAt(position)
1208 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1210 addr = unicode(item.text(0))
1211 label = unicode(item.text(1))
1212 is_editable = item.data(0,32).toBool()
1213 payto_addr = item.data(0,33).toString()
1214 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1215 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1216 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1218 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1219 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1221 run_hook('create_contact_menu', menu, item)
1222 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1225 def update_receive_item(self, item):
1226 item.setFont(0, QFont(MONOSPACE_FONT))
1227 address = str(item.data(0,0).toString())
1228 label = self.wallet.labels.get(address,'')
1229 item.setData(1,0,label)
1230 item.setData(0,32, True) # is editable
1232 run_hook('update_receive_item', address, item)
1234 if not self.wallet.is_mine(address): return
1236 c, u = self.wallet.get_addr_balance(address)
1237 balance = self.format_amount(c + u)
1238 item.setData(2,0,balance)
1240 if address in self.wallet.frozen_addresses:
1241 item.setBackgroundColor(0, QColor('lightblue'))
1244 def update_receive_tab(self):
1245 l = self.receive_list
1246 # extend the syntax for consistency
1247 l.addChild = l.addTopLevelItem
1248 l.insertChild = l.insertTopLevelItem
1251 for i,width in enumerate(self.column_widths['receive']):
1252 l.setColumnWidth(i, width)
1254 accounts = self.wallet.get_accounts()
1255 if self.current_account is None:
1256 account_items = sorted(accounts.items())
1258 account_items = [(self.current_account, accounts.get(self.current_account))]
1261 for k, account in account_items:
1263 if len(accounts) > 1:
1264 name = self.wallet.get_account_name(k)
1265 c,u = self.wallet.get_account_balance(k)
1266 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1267 l.addTopLevelItem(account_item)
1268 account_item.setExpanded(self.accounts_expanded.get(k, True))
1269 account_item.setData(0, 32, k)
1273 sequences = [0,1] if account.has_change() else [0]
1274 for is_change in sequences:
1275 if len(sequences) > 1:
1276 name = _("Receiving") if not is_change else _("Change")
1277 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1278 account_item.addChild(seq_item)
1280 seq_item.setExpanded(True)
1282 seq_item = account_item
1284 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1290 for address in account.get_addresses(is_change):
1292 num, is_used = self.wallet.is_used(address)
1295 if gap > self.wallet.gap_limit:
1300 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1301 self.update_receive_item(item)
1303 item.setBackgroundColor(1, QColor('red'))
1307 seq_item.insertChild(0,used_item)
1309 used_item.addChild(item)
1311 seq_item.addChild(item)
1313 # we use column 1 because column 0 may be hidden
1314 l.setCurrentItem(l.topLevelItem(0),1)
1317 def update_contacts_tab(self):
1318 l = self.contacts_list
1321 for address in self.wallet.addressbook:
1322 label = self.wallet.labels.get(address,'')
1323 n = self.wallet.get_num_tx(address)
1324 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1325 item.setFont(0, QFont(MONOSPACE_FONT))
1326 # 32 = label can be edited (bool)
1327 item.setData(0,32, True)
1329 item.setData(0,33, address)
1330 l.addTopLevelItem(item)
1332 run_hook('update_contacts_tab', l)
1333 l.setCurrentItem(l.topLevelItem(0))
1337 def create_console_tab(self):
1338 from console import Console
1339 self.console = console = Console()
1343 def update_console(self):
1344 console = self.console
1345 console.history = self.config.get("console-history",[])
1346 console.history_index = len(console.history)
1348 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1349 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1351 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1353 def mkfunc(f, method):
1354 return lambda *args: apply( f, (method, args, self.password_dialog ))
1356 if m[0]=='_' or m in ['network','wallet']: continue
1357 methods[m] = mkfunc(c._run, m)
1359 console.updateNamespace(methods)
1362 def change_account(self,s):
1363 if s == _("All accounts"):
1364 self.current_account = None
1366 accounts = self.wallet.get_account_names()
1367 for k, v in accounts.items():
1369 self.current_account = k
1370 self.update_history_tab()
1371 self.update_status()
1372 self.update_receive_tab()
1374 def create_status_bar(self):
1377 sb.setFixedHeight(35)
1378 qtVersion = qVersion()
1380 self.balance_label = QLabel("")
1381 sb.addWidget(self.balance_label)
1383 from version_getter import UpdateLabel
1384 self.updatelabel = UpdateLabel(self.config, sb)
1386 self.account_selector = QComboBox()
1387 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1388 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1389 sb.addPermanentWidget(self.account_selector)
1391 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1392 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1394 self.lock_icon = QIcon()
1395 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1396 sb.addPermanentWidget( self.password_button )
1398 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1399 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1400 sb.addPermanentWidget( self.seed_button )
1401 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1402 sb.addPermanentWidget( self.status_button )
1404 run_hook('create_status_bar', (sb,))
1406 self.setStatusBar(sb)
1409 def update_lock_icon(self):
1410 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1411 self.password_button.setIcon( icon )
1414 def update_buttons_on_seed(self):
1415 if self.wallet.has_seed():
1416 self.seed_button.show()
1418 self.seed_button.hide()
1420 if not self.wallet.is_watching_only():
1421 self.password_button.show()
1422 self.send_button.setText(_("Send"))
1424 self.password_button.hide()
1425 self.send_button.setText(_("Create unsigned transaction"))
1428 def change_password_dialog(self):
1429 from password_dialog import PasswordDialog
1430 d = PasswordDialog(self.wallet, self)
1432 self.update_lock_icon()
1435 def new_contact_dialog(self):
1438 d.setWindowTitle(_("New Contact"))
1439 vbox = QVBoxLayout(d)
1440 vbox.addWidget(QLabel(_('New Contact')+':'))
1442 grid = QGridLayout()
1445 grid.addWidget(QLabel(_("Address")), 1, 0)
1446 grid.addWidget(line1, 1, 1)
1447 grid.addWidget(QLabel(_("Name")), 2, 0)
1448 grid.addWidget(line2, 2, 1)
1450 vbox.addLayout(grid)
1451 vbox.addLayout(ok_cancel_buttons(d))
1456 address = str(line1.text())
1457 label = unicode(line2.text())
1459 if not is_valid(address):
1460 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1463 self.wallet.add_contact(address)
1465 self.wallet.set_label(address, label)
1467 self.update_contacts_tab()
1468 self.update_history_tab()
1469 self.update_completions()
1470 self.tabs.setCurrentIndex(3)
1474 def new_account_dialog(self, password):
1476 dialog = QDialog(self)
1478 dialog.setWindowTitle(_("New Account"))
1480 vbox = QVBoxLayout()
1481 vbox.addWidget(QLabel(_('Account name')+':'))
1484 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1485 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1490 vbox.addLayout(ok_cancel_buttons(dialog))
1491 dialog.setLayout(vbox)
1495 name = str(e.text())
1498 self.wallet.create_pending_account(name, password)
1499 self.update_receive_tab()
1500 self.tabs.setCurrentIndex(2)
1505 def show_master_public_keys(self):
1507 dialog = QDialog(self)
1509 dialog.setWindowTitle(_("Master Public Keys"))
1511 main_layout = QGridLayout()
1512 mpk_dict = self.wallet.get_master_public_keys()
1514 for key, value in mpk_dict.items():
1515 main_layout.addWidget(QLabel(key), i, 0)
1516 mpk_text = QTextEdit()
1517 mpk_text.setReadOnly(True)
1518 mpk_text.setMaximumHeight(170)
1519 mpk_text.setText(value)
1520 main_layout.addWidget(mpk_text, i + 1, 0)
1523 vbox = QVBoxLayout()
1524 vbox.addLayout(main_layout)
1525 vbox.addLayout(close_button(dialog))
1527 dialog.setLayout(vbox)
1532 def show_seed_dialog(self, password):
1533 if not self.wallet.has_seed():
1534 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1538 mnemonic = self.wallet.get_mnemonic(password)
1540 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1542 from seed_dialog import SeedDialog
1543 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1548 def show_qrcode(self, data, title = _("QR code")):
1552 d.setWindowTitle(title)
1553 d.setMinimumSize(270, 300)
1554 vbox = QVBoxLayout()
1555 qrw = QRCodeWidget(data)
1556 vbox.addWidget(qrw, 1)
1557 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1558 hbox = QHBoxLayout()
1561 filename = os.path.join(self.config.path, "qrcode.bmp")
1564 bmp.save_qrcode(qrw.qr, filename)
1565 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1567 def copy_to_clipboard():
1568 bmp.save_qrcode(qrw.qr, filename)
1569 self.app.clipboard().setImage(QImage(filename))
1570 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1572 b = QPushButton(_("Copy"))
1574 b.clicked.connect(copy_to_clipboard)
1576 b = QPushButton(_("Save"))
1578 b.clicked.connect(print_qr)
1580 b = QPushButton(_("Close"))
1582 b.clicked.connect(d.accept)
1585 vbox.addLayout(hbox)
1590 def do_protect(self, func, args):
1591 if self.wallet.use_encryption:
1592 password = self.password_dialog()
1598 if args != (False,):
1599 args = (self,) + args + (password,)
1601 args = (self,password)
1605 def show_public_keys(self, address):
1606 if not address: return
1608 pubkey_list = self.wallet.get_public_keys(address)
1609 except Exception as e:
1610 traceback.print_exc(file=sys.stdout)
1611 self.show_message(str(e))
1615 d.setMinimumSize(600, 200)
1617 vbox = QVBoxLayout()
1618 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1619 vbox.addWidget( QLabel(_("Public key") + ':'))
1621 keys.setReadOnly(True)
1622 keys.setText('\n'.join(pubkey_list))
1623 vbox.addWidget(keys)
1624 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1625 vbox.addLayout(close_button(d))
1630 def show_private_key(self, address, password):
1631 if not address: return
1633 pk_list = self.wallet.get_private_key(address, password)
1634 except Exception as e:
1635 traceback.print_exc(file=sys.stdout)
1636 self.show_message(str(e))
1640 d.setMinimumSize(600, 200)
1642 vbox = QVBoxLayout()
1643 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1644 vbox.addWidget( QLabel(_("Private key") + ':'))
1646 keys.setReadOnly(True)
1647 keys.setText('\n'.join(pk_list))
1648 vbox.addWidget(keys)
1649 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1650 vbox.addLayout(close_button(d))
1656 def do_sign(self, address, message, signature, password):
1657 message = unicode(message.toPlainText())
1658 message = message.encode('utf-8')
1660 sig = self.wallet.sign_message(str(address.text()), message, password)
1661 signature.setText(sig)
1662 except Exception as e:
1663 self.show_message(str(e))
1665 def do_verify(self, address, message, signature):
1666 message = unicode(message.toPlainText())
1667 message = message.encode('utf-8')
1668 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1669 self.show_message(_("Signature verified"))
1671 self.show_message(_("Error: wrong signature"))
1674 def sign_verify_message(self, address=''):
1677 d.setWindowTitle(_('Sign/verify Message'))
1678 d.setMinimumSize(410, 290)
1680 layout = QGridLayout(d)
1682 message_e = QTextEdit()
1683 layout.addWidget(QLabel(_('Message')), 1, 0)
1684 layout.addWidget(message_e, 1, 1)
1685 layout.setRowStretch(2,3)
1687 address_e = QLineEdit()
1688 address_e.setText(address)
1689 layout.addWidget(QLabel(_('Address')), 2, 0)
1690 layout.addWidget(address_e, 2, 1)
1692 signature_e = QTextEdit()
1693 layout.addWidget(QLabel(_('Signature')), 3, 0)
1694 layout.addWidget(signature_e, 3, 1)
1695 layout.setRowStretch(3,1)
1697 hbox = QHBoxLayout()
1699 b = QPushButton(_("Sign"))
1700 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1703 b = QPushButton(_("Verify"))
1704 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1707 b = QPushButton(_("Close"))
1708 b.clicked.connect(d.accept)
1710 layout.addLayout(hbox, 4, 1)
1715 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1717 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1718 message_e.setText(decrypted)
1719 except Exception as e:
1720 self.show_message(str(e))
1723 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1724 message = unicode(message_e.toPlainText())
1725 message = message.encode('utf-8')
1727 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1728 encrypted_e.setText(encrypted)
1729 except Exception as e:
1730 self.show_message(str(e))
1734 def encrypt_message(self, address = ''):
1737 d.setWindowTitle(_('Encrypt/decrypt Message'))
1738 d.setMinimumSize(610, 490)
1740 layout = QGridLayout(d)
1742 message_e = QTextEdit()
1743 layout.addWidget(QLabel(_('Message')), 1, 0)
1744 layout.addWidget(message_e, 1, 1)
1745 layout.setRowStretch(2,3)
1747 pubkey_e = QLineEdit()
1749 pubkey = self.wallet.getpubkeys(address)[0]
1750 pubkey_e.setText(pubkey)
1751 layout.addWidget(QLabel(_('Public key')), 2, 0)
1752 layout.addWidget(pubkey_e, 2, 1)
1754 encrypted_e = QTextEdit()
1755 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1756 layout.addWidget(encrypted_e, 3, 1)
1757 layout.setRowStretch(3,1)
1759 hbox = QHBoxLayout()
1760 b = QPushButton(_("Encrypt"))
1761 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1764 b = QPushButton(_("Decrypt"))
1765 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1768 b = QPushButton(_("Close"))
1769 b.clicked.connect(d.accept)
1772 layout.addLayout(hbox, 4, 1)
1776 def question(self, msg):
1777 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1779 def show_message(self, msg):
1780 QMessageBox.information(self, _('Message'), msg, _('OK'))
1782 def password_dialog(self, msg=None):
1785 d.setWindowTitle(_("Enter Password"))
1790 vbox = QVBoxLayout()
1792 msg = _('Please enter your password')
1793 vbox.addWidget(QLabel(msg))
1795 grid = QGridLayout()
1797 grid.addWidget(QLabel(_('Password')), 1, 0)
1798 grid.addWidget(pw, 1, 1)
1799 vbox.addLayout(grid)
1801 vbox.addLayout(ok_cancel_buttons(d))
1804 run_hook('password_dialog', pw, grid, 1)
1805 if not d.exec_(): return
1806 return unicode(pw.text())
1815 def tx_from_text(self, txt):
1816 "json or raw hexadecimal"
1819 tx = Transaction(txt)
1825 tx_dict = json.loads(str(txt))
1826 assert "hex" in tx_dict.keys()
1827 tx = Transaction(tx_dict["hex"])
1828 if tx_dict.has_key("input_info"):
1829 input_info = json.loads(tx_dict['input_info'])
1830 tx.add_input_info(input_info)
1833 traceback.print_exc(file=sys.stdout)
1836 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1840 def read_tx_from_file(self):
1841 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1845 with open(fileName, "r") as f:
1846 file_content = f.read()
1847 except (ValueError, IOError, os.error), reason:
1848 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1850 return self.tx_from_text(file_content)
1854 def sign_raw_transaction(self, tx, input_info, password):
1855 self.wallet.signrawtransaction(tx, input_info, [], password)
1857 def do_process_from_text(self):
1858 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1861 tx = self.tx_from_text(text)
1863 self.show_transaction(tx)
1865 def do_process_from_file(self):
1866 tx = self.read_tx_from_file()
1868 self.show_transaction(tx)
1870 def do_process_from_txid(self):
1871 from electrum import transaction
1872 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1874 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1876 tx = transaction.Transaction(r)
1878 self.show_transaction(tx)
1880 self.show_message("unknown transaction")
1882 def do_process_from_csvReader(self, csvReader):
1887 for position, row in enumerate(csvReader):
1889 if not is_valid(address):
1890 errors.append((position, address))
1892 amount = Decimal(row[1])
1893 amount = int(100000000*amount)
1894 outputs.append((address, amount))
1895 except (ValueError, IOError, os.error), reason:
1896 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1900 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1901 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1905 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1906 except Exception as e:
1907 self.show_message(str(e))
1910 self.show_transaction(tx)
1912 def do_process_from_csv_file(self):
1913 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1917 with open(fileName, "r") as f:
1918 csvReader = csv.reader(f)
1919 self.do_process_from_csvReader(csvReader)
1920 except (ValueError, IOError, os.error), reason:
1921 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1924 def do_process_from_csv_text(self):
1925 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1926 + _("Format: address, amount. One output per line"), _("Load CSV"))
1929 f = StringIO.StringIO(text)
1930 csvReader = csv.reader(f)
1931 self.do_process_from_csvReader(csvReader)
1936 def export_privkeys_dialog(self, password):
1937 if self.wallet.is_watching_only():
1938 self.show_message(_("This is a watching-only wallet"))
1942 d.setWindowTitle(_('Private keys'))
1943 d.setMinimumSize(850, 300)
1944 vbox = QVBoxLayout(d)
1946 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
1947 _("Exposing a single private key can compromise your entire wallet!"),
1948 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1949 vbox.addWidget(QLabel(msg))
1955 defaultname = 'electrum-private-keys.csv'
1956 select_msg = _('Select file to export your private keys to')
1957 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1958 vbox.addLayout(hbox)
1960 h, b = ok_cancel_buttons2(d, _('Export'))
1965 addresses = self.wallet.addresses(True)
1967 def privkeys_thread():
1968 for addr in addresses:
1972 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1973 d.emit(SIGNAL('computing_privkeys'))
1974 d.emit(SIGNAL('show_privkeys'))
1976 def show_privkeys():
1977 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
1981 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
1982 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
1983 threading.Thread(target=privkeys_thread).start()
1989 filename = filename_e.text()
1994 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
1995 except (IOError, os.error), reason:
1996 export_error_label = _("Electrum was unable to produce a private key-export.")
1997 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1999 except Exception as e:
2000 self.show_message(str(e))
2003 self.show_message(_("Private keys exported."))
2006 def do_export_privkeys(self, fileName, pklist, is_csv):
2007 with open(fileName, "w+") as f:
2009 transaction = csv.writer(f)
2010 transaction.writerow(["address", "private_key"])
2011 for addr, pk in pklist.items():
2012 transaction.writerow(["%34s"%addr,pk])
2015 f.write(json.dumps(pklist, indent = 4))
2018 def do_import_labels(self):
2019 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2020 if not labelsFile: return
2022 f = open(labelsFile, 'r')
2025 for key, value in json.loads(data).items():
2026 self.wallet.set_label(key, value)
2027 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2028 except (IOError, os.error), reason:
2029 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2032 def do_export_labels(self):
2033 labels = self.wallet.labels
2035 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2037 with open(fileName, 'w+') as f:
2038 json.dump(labels, f)
2039 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2040 except (IOError, os.error), reason:
2041 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2044 def export_history_dialog(self):
2047 d.setWindowTitle(_('Export History'))
2048 d.setMinimumSize(400, 200)
2049 vbox = QVBoxLayout(d)
2051 defaultname = os.path.expanduser('~/electrum-history.csv')
2052 select_msg = _('Select file to export your wallet transactions to')
2054 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2055 vbox.addLayout(hbox)
2059 h, b = ok_cancel_buttons2(d, _('Export'))
2064 filename = filename_e.text()
2069 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2070 except (IOError, os.error), reason:
2071 export_error_label = _("Electrum was unable to produce a transaction export.")
2072 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2075 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2078 def do_export_history(self, wallet, fileName, is_csv):
2079 history = wallet.get_tx_history()
2081 for item in history:
2082 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2084 if timestamp is not None:
2086 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2087 except [RuntimeError, TypeError, NameError] as reason:
2088 time_string = "unknown"
2091 time_string = "unknown"
2093 time_string = "pending"
2095 if value is not None:
2096 value_string = format_satoshis(value, True)
2101 fee_string = format_satoshis(fee, True)
2106 label, is_default_label = wallet.get_label(tx_hash)
2107 label = label.encode('utf-8')
2111 balance_string = format_satoshis(balance, False)
2113 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2115 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2117 with open(fileName, "w+") as f:
2119 transaction = csv.writer(f)
2120 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2122 transaction.writerow(line)
2125 f.write(json.dumps(lines, indent = 4))
2128 def sweep_key_dialog(self):
2130 d.setWindowTitle(_('Sweep private keys'))
2131 d.setMinimumSize(600, 300)
2133 vbox = QVBoxLayout(d)
2134 vbox.addWidget(QLabel(_("Enter private keys")))
2136 keys_e = QTextEdit()
2137 keys_e.setTabChangesFocus(True)
2138 vbox.addWidget(keys_e)
2140 h, address_e = address_field(self.wallet.addresses())
2144 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2145 vbox.addLayout(hbox)
2146 button.setEnabled(False)
2149 addr = str(address_e.text())
2150 if bitcoin.is_address(addr):
2154 pk = str(keys_e.toPlainText()).strip()
2155 if Wallet.is_private_key(pk):
2158 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2159 keys_e.textChanged.connect(f)
2160 address_e.textChanged.connect(f)
2164 fee = self.wallet.fee
2165 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2166 self.show_transaction(tx)
2170 def do_import_privkey(self, password):
2171 if not self.wallet.has_imported_keys():
2172 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2173 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2174 + _('Are you sure you understand what you are doing?'), 3, 4)
2177 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2180 text = str(text).split()
2185 addr = self.wallet.import_key(key, password)
2186 except Exception as e:
2192 addrlist.append(addr)
2194 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2196 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2197 self.update_receive_tab()
2198 self.update_history_tab()
2201 def settings_dialog(self):
2203 d.setWindowTitle(_('Electrum Settings'))
2205 vbox = QVBoxLayout()
2206 grid = QGridLayout()
2207 grid.setColumnStretch(0,1)
2209 nz_label = QLabel(_('Display zeros') + ':')
2210 grid.addWidget(nz_label, 0, 0)
2211 nz_e = AmountEdit(None,True)
2212 nz_e.setText("%d"% self.num_zeros)
2213 grid.addWidget(nz_e, 0, 1)
2214 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2215 grid.addWidget(HelpButton(msg), 0, 2)
2216 if not self.config.is_modifiable('num_zeros'):
2217 for w in [nz_e, nz_label]: w.setEnabled(False)
2219 lang_label=QLabel(_('Language') + ':')
2220 grid.addWidget(lang_label, 1, 0)
2221 lang_combo = QComboBox()
2222 from electrum.i18n import languages
2223 lang_combo.addItems(languages.values())
2225 index = languages.keys().index(self.config.get("language",''))
2228 lang_combo.setCurrentIndex(index)
2229 grid.addWidget(lang_combo, 1, 1)
2230 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2231 if not self.config.is_modifiable('language'):
2232 for w in [lang_combo, lang_label]: w.setEnabled(False)
2235 fee_label = QLabel(_('Transaction fee') + ':')
2236 grid.addWidget(fee_label, 2, 0)
2237 fee_e = AmountEdit(self.get_decimal_point)
2238 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2239 grid.addWidget(fee_e, 2, 1)
2240 msg = _('Fee per kilobyte of transaction.') + ' ' \
2241 + _('Recommended value') + ': ' + self.format_amount(20000)
2242 grid.addWidget(HelpButton(msg), 2, 2)
2243 if not self.config.is_modifiable('fee_per_kb'):
2244 for w in [fee_e, fee_label]: w.setEnabled(False)
2246 units = ['BTC', 'mBTC']
2247 unit_label = QLabel(_('Base unit') + ':')
2248 grid.addWidget(unit_label, 3, 0)
2249 unit_combo = QComboBox()
2250 unit_combo.addItems(units)
2251 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2252 grid.addWidget(unit_combo, 3, 1)
2253 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2254 + '\n1BTC=1000mBTC.\n' \
2255 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2257 usechange_cb = QCheckBox(_('Use change addresses'))
2258 usechange_cb.setChecked(self.wallet.use_change)
2259 grid.addWidget(usechange_cb, 4, 0)
2260 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2261 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2263 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2264 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2265 grid.addWidget(block_ex_label, 5, 0)
2266 block_ex_combo = QComboBox()
2267 block_ex_combo.addItems(block_explorers)
2268 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2269 grid.addWidget(block_ex_combo, 5, 1)
2270 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2272 show_tx = self.config.get('show_before_broadcast', False)
2273 showtx_cb = QCheckBox(_('Show before broadcast'))
2274 showtx_cb.setChecked(show_tx)
2275 grid.addWidget(showtx_cb, 6, 0)
2276 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2278 vbox.addLayout(grid)
2280 vbox.addLayout(ok_cancel_buttons(d))
2284 if not d.exec_(): return
2287 fee = self.fee_e.get_amount()
2289 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2292 self.wallet.set_fee(fee)
2294 nz = unicode(nz_e.text())
2299 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2302 if self.num_zeros != nz:
2304 self.config.set_key('num_zeros', nz, True)
2305 self.update_history_tab()
2306 self.update_receive_tab()
2308 usechange_result = usechange_cb.isChecked()
2309 if self.wallet.use_change != usechange_result:
2310 self.wallet.use_change = usechange_result
2311 self.wallet.storage.put('use_change', self.wallet.use_change)
2313 if showtx_cb.isChecked() != show_tx:
2314 self.config.set_key('show_before_broadcast', not show_tx)
2316 unit_result = units[unit_combo.currentIndex()]
2317 if self.base_unit() != unit_result:
2318 self.decimal_point = 8 if unit_result == 'BTC' else 5
2319 self.config.set_key('decimal_point', self.decimal_point, True)
2320 self.update_history_tab()
2321 self.update_status()
2323 need_restart = False
2325 lang_request = languages.keys()[lang_combo.currentIndex()]
2326 if lang_request != self.config.get('language'):
2327 self.config.set_key("language", lang_request, True)
2330 be_result = block_explorers[block_ex_combo.currentIndex()]
2331 self.config.set_key('block_explorer', be_result, True)
2333 run_hook('close_settings_dialog')
2336 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2339 def run_network_dialog(self):
2340 if not self.network:
2342 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2344 def closeEvent(self, event):
2346 self.config.set_key("is_maximized", self.isMaximized())
2347 if not self.isMaximized():
2349 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2350 self.save_column_widths()
2351 self.config.set_key("console-history", self.console.history[-50:], True)
2352 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2356 def plugins_dialog(self):
2357 from electrum.plugins import plugins
2360 d.setWindowTitle(_('Electrum Plugins'))
2363 vbox = QVBoxLayout(d)
2366 scroll = QScrollArea()
2367 scroll.setEnabled(True)
2368 scroll.setWidgetResizable(True)
2369 scroll.setMinimumSize(400,250)
2370 vbox.addWidget(scroll)
2374 w.setMinimumHeight(len(plugins)*35)
2376 grid = QGridLayout()
2377 grid.setColumnStretch(0,1)
2380 def do_toggle(cb, p, w):
2383 if w: w.setEnabled(r)
2385 def mk_toggle(cb, p, w):
2386 return lambda: do_toggle(cb,p,w)
2388 for i, p in enumerate(plugins):
2390 cb = QCheckBox(p.fullname())
2391 cb.setDisabled(not p.is_available())
2392 cb.setChecked(p.is_enabled())
2393 grid.addWidget(cb, i, 0)
2394 if p.requires_settings():
2395 w = p.settings_widget(self)
2396 w.setEnabled( p.is_enabled() )
2397 grid.addWidget(w, i, 1)
2400 cb.clicked.connect(mk_toggle(cb,p,w))
2401 grid.addWidget(HelpButton(p.description()), i, 2)
2403 print_msg(_("Error: cannot display plugin"), p)
2404 traceback.print_exc(file=sys.stdout)
2405 grid.setRowStretch(i+1,1)
2407 vbox.addLayout(close_button(d))
2412 def show_account_details(self, k):
2413 account = self.wallet.accounts[k]
2416 d.setWindowTitle(_('Account Details'))
2419 vbox = QVBoxLayout(d)
2420 name = self.wallet.get_account_name(k)
2421 label = QLabel('Name: ' + name)
2422 vbox.addWidget(label)
2424 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2426 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2428 vbox.addWidget(QLabel(_('Master Public Key:')))
2431 text.setReadOnly(True)
2432 text.setMaximumHeight(170)
2433 vbox.addWidget(text)
2435 mpk_text = '\n'.join( account.get_master_pubkeys() )
2436 text.setText(mpk_text)
2438 vbox.addLayout(close_button(d))