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_console_tab(), _('Console') )
133 tabs.setMinimumSize(600, 400)
134 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
135 self.setCentralWidget(tabs)
137 g = self.config.get("winpos-qt",[100, 100, 840, 400])
138 self.setGeometry(g[0], g[1], g[2], g[3])
139 if self.config.get("is_maximized"):
142 self.setWindowIcon(QIcon(":icons/electrum.png"))
145 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
146 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
147 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
148 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
149 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
151 for i in range(tabs.count()):
152 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
154 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
155 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
156 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
157 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
158 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
160 self.history_list.setFocus(True)
164 self.network.register_callback('updated', lambda: self.need_update.set())
165 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
166 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
167 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
168 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
170 # set initial message
171 self.console.showMessage(self.network.banner)
176 def update_account_selector(self):
178 accounts = self.wallet.get_account_names()
179 self.account_selector.clear()
180 if len(accounts) > 1:
181 self.account_selector.addItems([_("All accounts")] + accounts.values())
182 self.account_selector.setCurrentIndex(0)
183 self.account_selector.show()
185 self.account_selector.hide()
188 def load_wallet(self, wallet):
192 self.update_wallet_format()
194 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
195 self.current_account = self.wallet.storage.get("current_account", None)
196 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
197 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
198 self.setWindowTitle( title )
200 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
201 self.notify_transactions()
202 self.update_account_selector()
204 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
205 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
206 self.password_menu.setEnabled(not self.wallet.is_watching_only())
207 self.seed_menu.setEnabled(self.wallet.has_seed())
208 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
209 self.import_menu.setEnabled(self.wallet.can_import())
211 self.update_lock_icon()
212 self.update_buttons_on_seed()
213 self.update_console()
215 run_hook('load_wallet', wallet)
218 def update_wallet_format(self):
219 # convert old-format imported keys
220 if self.wallet.imported_keys:
221 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
223 self.wallet.convert_imported_keys(password)
225 self.show_message("error")
228 def open_wallet(self):
229 wallet_folder = self.wallet.storage.path
230 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
234 storage = WalletStorage({'wallet_path': filename})
235 if not storage.file_exists:
236 self.show_message("file not found "+ filename)
239 self.wallet.stop_threads()
242 wallet = Wallet(storage)
243 wallet.start_threads(self.network)
245 self.load_wallet(wallet)
249 def backup_wallet(self):
251 path = self.wallet.storage.path
252 wallet_folder = os.path.dirname(path)
253 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
257 new_path = os.path.join(wallet_folder, filename)
260 shutil.copy2(path, new_path)
261 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
262 except (IOError, os.error), reason:
263 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
266 def new_wallet(self):
269 wallet_folder = os.path.dirname(self.wallet.storage.path)
270 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
273 filename = os.path.join(wallet_folder, filename)
275 storage = WalletStorage({'wallet_path': filename})
276 if storage.file_exists:
277 QMessageBox.critical(None, "Error", _("File exists"))
280 wizard = installwizard.InstallWizard(self.config, self.network, storage)
281 wallet = wizard.run('new')
283 self.load_wallet(wallet)
287 def init_menubar(self):
290 file_menu = menubar.addMenu(_("&File"))
291 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
292 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
293 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
294 file_menu.addAction(_("&Quit"), self.close)
296 wallet_menu = menubar.addMenu(_("&Wallet"))
297 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
298 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
300 wallet_menu.addSeparator()
302 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
303 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
304 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
306 wallet_menu.addSeparator()
307 labels_menu = wallet_menu.addMenu(_("&Labels"))
308 labels_menu.addAction(_("&Import"), self.do_import_labels)
309 labels_menu.addAction(_("&Export"), self.do_export_labels)
311 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
312 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
313 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
314 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
315 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
317 tools_menu = menubar.addMenu(_("&Tools"))
319 # Settings / Preferences are all reserved keywords in OSX using this as work around
320 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
321 tools_menu.addAction(_("&Network"), self.run_network_dialog)
322 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
323 tools_menu.addSeparator()
324 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
325 #tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
326 tools_menu.addSeparator()
328 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
329 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
330 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
332 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
333 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
334 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
335 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
337 help_menu = menubar.addMenu(_("&Help"))
338 help_menu.addAction(_("&About"), self.show_about)
339 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
340 help_menu.addSeparator()
341 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
342 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
344 self.setMenuBar(menubar)
346 def show_about(self):
347 QMessageBox.about(self, "Electrum",
348 _("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."))
350 def show_report_bug(self):
351 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
352 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
355 def notify_transactions(self):
356 if not self.network or not self.network.is_connected():
359 print_error("Notifying GUI")
360 if len(self.network.pending_transactions_for_notifications) > 0:
361 # Combine the transactions if there are more then three
362 tx_amount = len(self.network.pending_transactions_for_notifications)
365 for tx in self.network.pending_transactions_for_notifications:
366 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
370 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
371 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
373 self.network.pending_transactions_for_notifications = []
375 for tx in self.network.pending_transactions_for_notifications:
377 self.network.pending_transactions_for_notifications.remove(tx)
378 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
380 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
382 def notify(self, message):
383 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
387 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
388 def getOpenFileName(self, title, filter = ""):
389 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
390 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
391 if fileName and directory != os.path.dirname(fileName):
392 self.config.set_key('io_dir', os.path.dirname(fileName), True)
395 def getSaveFileName(self, title, filename, filter = ""):
396 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
397 path = os.path.join( directory, filename )
398 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
399 if fileName and directory != os.path.dirname(fileName):
400 self.config.set_key('io_dir', os.path.dirname(fileName), True)
404 QMainWindow.close(self)
405 run_hook('close_main_window')
407 def connect_slots(self, sender):
408 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
409 self.previous_payto_e=''
411 def timer_actions(self):
412 if self.need_update.is_set():
414 self.need_update.clear()
415 run_hook('timer_actions')
417 def format_amount(self, x, is_diff=False, whitespaces=False):
418 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
421 def get_decimal_point(self):
422 return self.decimal_point
426 assert self.decimal_point in [5,8]
427 return "BTC" if self.decimal_point == 8 else "mBTC"
430 def update_status(self):
431 if self.network is None or not self.network.is_running():
433 icon = QIcon(":icons/status_disconnected.png")
435 elif self.network.is_connected():
436 if not self.wallet.up_to_date:
437 text = _("Synchronizing...")
438 icon = QIcon(":icons/status_waiting.png")
439 elif self.network.server_lag > 1:
440 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
441 icon = QIcon(":icons/status_lagging.png")
443 c, u = self.wallet.get_account_balance(self.current_account)
444 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
445 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
447 # append fiat balance and price from exchange rate plugin
449 run_hook('get_fiat_status_text', c+u, r)
454 self.tray.setToolTip(text)
455 icon = QIcon(":icons/status_connected.png")
457 text = _("Not connected")
458 icon = QIcon(":icons/status_disconnected.png")
460 self.balance_label.setText(text)
461 self.status_button.setIcon( icon )
464 def update_wallet(self):
466 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
467 self.update_history_tab()
468 self.update_receive_tab()
469 self.update_contacts_tab()
470 self.update_completions()
473 def create_history_tab(self):
474 self.history_list = l = MyTreeWidget(self)
476 for i,width in enumerate(self.column_widths['history']):
477 l.setColumnWidth(i, width)
478 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
479 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
480 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
482 l.customContextMenuRequested.connect(self.create_history_menu)
486 def create_history_menu(self, position):
487 self.history_list.selectedIndexes()
488 item = self.history_list.currentItem()
489 be = self.config.get('block_explorer', 'Blockchain.info')
490 if be == 'Blockchain.info':
491 block_explorer = 'https://blockchain.info/tx/'
492 elif be == 'Blockr.io':
493 block_explorer = 'https://blockr.io/tx/info/'
494 elif be == 'Insight.is':
495 block_explorer = 'http://live.insight.is/tx/'
497 tx_hash = str(item.data(0, Qt.UserRole).toString())
498 if not tx_hash: return
500 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
501 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
502 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
503 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
504 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
507 def show_transaction(self, tx):
508 import transaction_dialog
509 d = transaction_dialog.TxDialog(tx, self)
512 def tx_label_clicked(self, item, column):
513 if column==2 and item.isSelected():
515 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
516 self.history_list.editItem( item, column )
517 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
520 def tx_label_changed(self, item, column):
524 tx_hash = str(item.data(0, Qt.UserRole).toString())
525 tx = self.wallet.transactions.get(tx_hash)
526 text = unicode( item.text(2) )
527 self.wallet.set_label(tx_hash, text)
529 item.setForeground(2, QBrush(QColor('black')))
531 text = self.wallet.get_default_label(tx_hash)
532 item.setText(2, text)
533 item.setForeground(2, QBrush(QColor('gray')))
537 def edit_label(self, is_recv):
538 l = self.receive_list if is_recv else self.contacts_list
539 item = l.currentItem()
540 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
541 l.editItem( item, 1 )
542 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
546 def address_label_clicked(self, item, column, l, column_addr, column_label):
547 if column == column_label and item.isSelected():
548 is_editable = item.data(0, 32).toBool()
551 addr = unicode( item.text(column_addr) )
552 label = unicode( item.text(column_label) )
553 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
554 l.editItem( item, column )
555 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
558 def address_label_changed(self, item, column, l, column_addr, column_label):
559 if column == column_label:
560 addr = unicode( item.text(column_addr) )
561 text = unicode( item.text(column_label) )
562 is_editable = item.data(0, 32).toBool()
566 changed = self.wallet.set_label(addr, text)
568 self.update_history_tab()
569 self.update_completions()
571 self.current_item_changed(item)
573 run_hook('item_changed', item, column)
576 def current_item_changed(self, a):
577 run_hook('current_item_changed', a)
581 def update_history_tab(self):
583 self.history_list.clear()
584 for item in self.wallet.get_tx_history(self.current_account):
585 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
586 time_str = _("unknown")
589 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
591 time_str = _("error")
594 time_str = 'unverified'
595 icon = QIcon(":icons/unconfirmed.png")
598 icon = QIcon(":icons/unconfirmed.png")
600 icon = QIcon(":icons/clock%d.png"%conf)
602 icon = QIcon(":icons/confirmed.png")
604 if value is not None:
605 v_str = self.format_amount(value, True, whitespaces=True)
609 balance_str = self.format_amount(balance, whitespaces=True)
612 label, is_default_label = self.wallet.get_label(tx_hash)
614 label = _('Pruned transaction outputs')
615 is_default_label = False
617 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
618 item.setFont(2, QFont(MONOSPACE_FONT))
619 item.setFont(3, QFont(MONOSPACE_FONT))
620 item.setFont(4, QFont(MONOSPACE_FONT))
622 item.setForeground(3, QBrush(QColor("#BC1E1E")))
624 item.setData(0, Qt.UserRole, tx_hash)
625 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
627 item.setForeground(2, QBrush(QColor('grey')))
629 item.setIcon(0, icon)
630 self.history_list.insertTopLevelItem(0,item)
633 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
634 run_hook('history_tab_update')
637 def create_send_tab(self):
640 grid = QGridLayout(w)
642 grid.setColumnMinimumWidth(3,300)
643 grid.setColumnStretch(5,1)
644 grid.setRowStretch(8, 1)
646 from paytoedit import PayToEdit
647 self.amount_e = AmountEdit(self.get_decimal_point)
648 self.payto_e = PayToEdit(self.amount_e)
649 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)'))
650 grid.addWidget(QLabel(_('Pay to')), 1, 0)
651 grid.addWidget(self.payto_e, 1, 1, 1, 3)
652 grid.addWidget(self.payto_help, 1, 4)
654 completer = QCompleter()
655 completer.setCaseSensitivity(False)
656 self.payto_e.setCompleter(completer)
657 completer.setModel(self.completions)
659 self.message_e = MyLineEdit()
660 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.'))
661 grid.addWidget(QLabel(_('Description')), 2, 0)
662 grid.addWidget(self.message_e, 2, 1, 1, 3)
663 grid.addWidget(self.message_help, 2, 4)
665 self.from_label = QLabel(_('From'))
666 grid.addWidget(self.from_label, 3, 0)
667 self.from_list = MyTreeWidget(self)
668 self.from_list.setColumnCount(2)
669 self.from_list.setColumnWidth(0, 350)
670 self.from_list.setColumnWidth(1, 50)
671 self.from_list.setHeaderHidden(True)
672 self.from_list.setMaximumHeight(80)
673 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
674 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
675 grid.addWidget(self.from_list, 3, 1, 1, 3)
676 self.set_pay_from([])
678 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
679 + _('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.') \
680 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
681 grid.addWidget(QLabel(_('Amount')), 4, 0)
682 grid.addWidget(self.amount_e, 4, 1, 1, 2)
683 grid.addWidget(self.amount_help, 4, 3)
685 self.fee_e = AmountEdit(self.get_decimal_point)
686 grid.addWidget(QLabel(_('Fee')), 5, 0)
687 grid.addWidget(self.fee_e, 5, 1, 1, 2)
688 grid.addWidget(HelpButton(
689 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
690 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
691 + _('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)
693 run_hook('exchange_rate_button', grid)
695 self.send_button = EnterButton(_("Send"), self.do_send)
696 grid.addWidget(self.send_button, 6, 1)
698 b = EnterButton(_("Clear"), self.do_clear)
699 grid.addWidget(b, 6, 2)
701 self.payto_sig = QLabel('')
702 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
704 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
705 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
708 def entry_changed( is_fee ):
709 self.funds_error = False
711 if self.amount_e.is_shortcut:
712 self.amount_e.is_shortcut = False
713 sendable = self.get_sendable_balance()
714 # there is only one output because we are completely spending inputs
715 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
716 fee = self.wallet.estimated_fee(inputs, 1)
718 self.amount_e.setText( self.format_amount(amount) )
719 self.fee_e.setText( self.format_amount( fee ) )
722 amount = self.amount_e.get_amount()
723 fee = self.fee_e.get_amount()
725 if not is_fee: fee = None
728 # assume that there will be 2 outputs (one for change)
729 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
731 self.fee_e.setText( self.format_amount( fee ) )
734 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
738 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
739 self.funds_error = True
740 text = _( "Not enough funds" )
741 c, u = self.wallet.get_frozen_balance()
742 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
744 self.statusBar().showMessage(text)
745 self.amount_e.setPalette(palette)
746 self.fee_e.setPalette(palette)
748 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
749 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
751 run_hook('create_send_tab', grid)
754 def from_list_delete(self, item):
755 i = self.from_list.indexOfTopLevelItem(item)
757 self.redraw_from_list()
759 def from_list_menu(self, position):
760 item = self.from_list.itemAt(position)
762 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
763 menu.exec_(self.from_list.viewport().mapToGlobal(position))
765 def set_pay_from(self, domain = None):
766 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
767 self.redraw_from_list()
769 def redraw_from_list(self):
770 self.from_list.clear()
771 self.from_label.setHidden(len(self.pay_from) == 0)
772 self.from_list.setHidden(len(self.pay_from) == 0)
775 h = x.get('prevout_hash')
776 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
778 for item in self.pay_from:
779 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
781 def update_completions(self):
783 for addr,label in self.wallet.labels.items():
784 if addr in self.wallet.addressbook:
785 l.append( label + ' <' + addr + '>')
787 run_hook('update_completions', l)
788 self.completions.setStringList(l)
792 return lambda s, *args: s.do_protect(func, args)
796 label = unicode( self.message_e.text() )
798 if self.gui_object.payment_request:
799 outputs = self.gui_object.payment_request.outputs
801 outputs = self.payto_e.get_outputs()
804 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
807 for addr, x in outputs:
808 if addr is None or not bitcoin.is_address(addr):
809 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
811 if type(x) is not int:
812 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
815 amount = sum(map(lambda x:x[1], outputs))
818 fee = self.fee_e.get_amount()
820 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
823 confirm_amount = self.config.get('confirm_amount', 100000000)
824 if amount >= confirm_amount:
825 o = '\n'.join(map(lambda x:x[0], outputs))
826 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
829 confirm_fee = self.config.get('confirm_fee', 100000)
830 if fee >= confirm_fee:
831 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()}):
834 self.send_tx(outputs, fee, label)
839 def send_tx(self, outputs, fee, label, password):
840 self.send_button.setDisabled(True)
842 # first, create an unsigned tx
843 coins = self.get_coins()
845 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
847 except Exception as e:
848 traceback.print_exc(file=sys.stdout)
849 self.show_message(str(e))
850 self.send_button.setDisabled(False)
853 # call hook to see if plugin needs gui interaction
854 run_hook('send_tx', tx)
860 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
861 self.wallet.sign_transaction(tx, keypairs, password)
862 return tx, fee, label
864 def sign_done(tx, fee, label):
866 self.show_message(tx.error)
867 self.send_button.setDisabled(False)
869 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
870 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
871 self.send_button.setDisabled(False)
874 self.wallet.set_label(tx.hash(), label)
876 if not tx.is_complete() or self.config.get('show_before_broadcast'):
877 self.show_transaction(tx)
879 self.send_button.setDisabled(False)
882 self.broadcast_transaction(tx)
884 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
885 self.waiting_dialog.start()
889 def broadcast_transaction(self, tx):
891 def broadcast_thread():
892 if self.gui_object.payment_request:
893 refund_address = self.wallet.addresses()[0]
894 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
895 self.gui_object.payment_request = None
897 status, msg = self.wallet.sendtx(tx)
900 def broadcast_done(status, msg):
902 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
905 QMessageBox.warning(self, _('Error'), msg, _('OK'))
906 self.send_button.setDisabled(False)
908 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
909 self.waiting_dialog.start()
913 def prepare_for_payment_request(self):
914 self.tabs.setCurrentIndex(1)
915 for e in [self.payto_e, self.amount_e, self.message_e]:
917 for h in [self.payto_help, self.amount_help, self.message_help]:
919 self.payto_e.setText(_("please wait..."))
922 def payment_request_ok(self):
923 pr = self.gui_object.payment_request
924 self.payto_help.show()
925 self.payto_help.set_alt(pr.status)
926 self.payto_e.setGreen()
927 self.payto_e.setText(pr.domain)
928 self.amount_e.setText(self.format_amount(pr.get_amount()))
929 self.message_e.setText(pr.memo)
931 def payment_request_error(self):
933 self.show_message(self.gui_object.payment_request.error)
934 self.gui_object.payment_request = None
936 def set_send(self, address, amount, label, message):
938 if label and self.wallet.labels.get(address) != label:
939 if self.question('Give label "%s" to address %s ?'%(label,address)):
940 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
941 self.wallet.addressbook.append(address)
942 self.wallet.set_label(address, label)
944 self.tabs.setCurrentIndex(1)
945 label = self.wallet.labels.get(address)
946 m_addr = label + ' <'+ address +'>' if label else address
947 self.payto_e.setText(m_addr)
949 self.message_e.setText(message)
951 self.amount_e.setText(amount)
955 self.payto_sig.setVisible(False)
956 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
960 for h in [self.payto_help, self.amount_help, self.message_help]:
963 self.payto_help.set_alt(None)
965 self.set_pay_from([])
970 def set_addrs_frozen(self,addrs,freeze):
972 if not addr: continue
973 if addr in self.wallet.frozen_addresses and not freeze:
974 self.wallet.unfreeze(addr)
975 elif addr not in self.wallet.frozen_addresses and freeze:
976 self.wallet.freeze(addr)
977 self.update_receive_tab()
981 def create_list_tab(self, headers):
982 "generic tab creation method"
983 l = MyTreeWidget(self)
984 l.setColumnCount( len(headers) )
985 l.setHeaderLabels( headers )
995 vbox.addWidget(buttons)
1000 buttons.setLayout(hbox)
1005 def create_receive_tab(self):
1006 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1007 l.setContextMenuPolicy(Qt.CustomContextMenu)
1008 l.customContextMenuRequested.connect(self.create_receive_menu)
1009 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1010 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1011 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1012 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1013 self.receive_list = l
1014 self.receive_buttons_hbox = hbox
1021 def save_column_widths(self):
1022 self.column_widths["receive"] = []
1023 for i in range(self.receive_list.columnCount() -1):
1024 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1026 self.column_widths["history"] = []
1027 for i in range(self.history_list.columnCount() - 1):
1028 self.column_widths["history"].append(self.history_list.columnWidth(i))
1030 self.column_widths["contacts"] = []
1031 for i in range(self.contacts_list.columnCount() - 1):
1032 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1034 self.config.set_key("column_widths_2", self.column_widths, True)
1037 def create_contacts_tab(self):
1038 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1039 l.setContextMenuPolicy(Qt.CustomContextMenu)
1040 l.customContextMenuRequested.connect(self.create_contact_menu)
1041 for i,width in enumerate(self.column_widths['contacts']):
1042 l.setColumnWidth(i, width)
1044 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1045 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1046 self.contacts_list = l
1047 self.contacts_buttons_hbox = hbox
1052 def delete_imported_key(self, addr):
1053 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1054 self.wallet.delete_imported_key(addr)
1055 self.update_receive_tab()
1056 self.update_history_tab()
1058 def edit_account_label(self, k):
1059 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1061 label = unicode(text)
1062 self.wallet.set_label(k,label)
1063 self.update_receive_tab()
1065 def account_set_expanded(self, item, k, b):
1067 self.accounts_expanded[k] = b
1069 def create_account_menu(self, position, k, item):
1071 if item.isExpanded():
1072 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1074 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1075 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1076 if self.wallet.seed_version > 4:
1077 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1078 if self.wallet.account_is_pending(k):
1079 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1080 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1082 def delete_pending_account(self, k):
1083 self.wallet.delete_pending_account(k)
1084 self.update_receive_tab()
1086 def create_receive_menu(self, position):
1087 # fixme: this function apparently has a side effect.
1088 # if it is not called the menu pops up several times
1089 #self.receive_list.selectedIndexes()
1091 selected = self.receive_list.selectedItems()
1092 multi_select = len(selected) > 1
1093 addrs = [unicode(item.text(0)) for item in selected]
1094 if not multi_select:
1095 item = self.receive_list.itemAt(position)
1099 if not is_valid(addr):
1100 k = str(item.data(0,32).toString())
1102 self.create_account_menu(position, k, item)
1104 item.setExpanded(not item.isExpanded())
1108 if not multi_select:
1109 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1110 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1111 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1112 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1113 if not self.wallet.is_watching_only():
1114 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1115 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1116 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1117 if self.wallet.is_imported(addr):
1118 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1120 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1121 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1122 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1123 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1125 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1126 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1128 run_hook('receive_menu', menu, addrs)
1129 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1132 def get_sendable_balance(self):
1133 return sum(map(lambda x:x['value'], self.get_coins()))
1136 def get_coins(self):
1138 return self.pay_from
1140 domain = self.wallet.get_account_addresses(self.current_account)
1141 for i in self.wallet.frozen_addresses:
1142 if i in domain: domain.remove(i)
1143 return self.wallet.get_unspent_coins(domain)
1146 def send_from_addresses(self, addrs):
1147 self.set_pay_from( addrs )
1148 self.tabs.setCurrentIndex(1)
1151 def payto(self, addr):
1153 label = self.wallet.labels.get(addr)
1154 m_addr = label + ' <' + addr + '>' if label else addr
1155 self.tabs.setCurrentIndex(1)
1156 self.payto_e.setText(m_addr)
1157 self.amount_e.setFocus()
1160 def delete_contact(self, x):
1161 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1162 self.wallet.delete_contact(x)
1163 self.wallet.set_label(x, None)
1164 self.update_history_tab()
1165 self.update_contacts_tab()
1166 self.update_completions()
1169 def create_contact_menu(self, position):
1170 item = self.contacts_list.itemAt(position)
1173 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1175 addr = unicode(item.text(0))
1176 label = unicode(item.text(1))
1177 is_editable = item.data(0,32).toBool()
1178 payto_addr = item.data(0,33).toString()
1179 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1180 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1181 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1183 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1184 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1186 run_hook('create_contact_menu', menu, item)
1187 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1190 def update_receive_item(self, item):
1191 item.setFont(0, QFont(MONOSPACE_FONT))
1192 address = str(item.data(0,0).toString())
1193 label = self.wallet.labels.get(address,'')
1194 item.setData(1,0,label)
1195 item.setData(0,32, True) # is editable
1197 run_hook('update_receive_item', address, item)
1199 if not self.wallet.is_mine(address): return
1201 c, u = self.wallet.get_addr_balance(address)
1202 balance = self.format_amount(c + u)
1203 item.setData(2,0,balance)
1205 if address in self.wallet.frozen_addresses:
1206 item.setBackgroundColor(0, QColor('lightblue'))
1209 def update_receive_tab(self):
1210 l = self.receive_list
1211 # extend the syntax for consistency
1212 l.addChild = l.addTopLevelItem
1213 l.insertChild = l.insertTopLevelItem
1216 for i,width in enumerate(self.column_widths['receive']):
1217 l.setColumnWidth(i, width)
1219 accounts = self.wallet.get_accounts()
1220 if self.current_account is None:
1221 account_items = sorted(accounts.items())
1223 account_items = [(self.current_account, accounts.get(self.current_account))]
1226 for k, account in account_items:
1228 if len(accounts) > 1:
1229 name = self.wallet.get_account_name(k)
1230 c,u = self.wallet.get_account_balance(k)
1231 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1232 l.addTopLevelItem(account_item)
1233 account_item.setExpanded(self.accounts_expanded.get(k, True))
1234 account_item.setData(0, 32, k)
1238 sequences = [0,1] if account.has_change() else [0]
1239 for is_change in sequences:
1240 if len(sequences) > 1:
1241 name = _("Receiving") if not is_change else _("Change")
1242 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1243 account_item.addChild(seq_item)
1245 seq_item.setExpanded(True)
1247 seq_item = account_item
1249 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1255 for address in account.get_addresses(is_change):
1257 num, is_used = self.wallet.is_used(address)
1260 if gap > self.wallet.gap_limit:
1265 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1266 self.update_receive_item(item)
1268 item.setBackgroundColor(1, QColor('red'))
1272 seq_item.insertChild(0,used_item)
1274 used_item.addChild(item)
1276 seq_item.addChild(item)
1278 # we use column 1 because column 0 may be hidden
1279 l.setCurrentItem(l.topLevelItem(0),1)
1282 def update_contacts_tab(self):
1283 l = self.contacts_list
1286 for address in self.wallet.addressbook:
1287 label = self.wallet.labels.get(address,'')
1288 n = self.wallet.get_num_tx(address)
1289 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1290 item.setFont(0, QFont(MONOSPACE_FONT))
1291 # 32 = label can be edited (bool)
1292 item.setData(0,32, True)
1294 item.setData(0,33, address)
1295 l.addTopLevelItem(item)
1297 run_hook('update_contacts_tab', l)
1298 l.setCurrentItem(l.topLevelItem(0))
1302 def create_console_tab(self):
1303 from console import Console
1304 self.console = console = Console()
1308 def update_console(self):
1309 console = self.console
1310 console.history = self.config.get("console-history",[])
1311 console.history_index = len(console.history)
1313 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1314 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1316 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1318 def mkfunc(f, method):
1319 return lambda *args: apply( f, (method, args, self.password_dialog ))
1321 if m[0]=='_' or m in ['network','wallet']: continue
1322 methods[m] = mkfunc(c._run, m)
1324 console.updateNamespace(methods)
1327 def change_account(self,s):
1328 if s == _("All accounts"):
1329 self.current_account = None
1331 accounts = self.wallet.get_account_names()
1332 for k, v in accounts.items():
1334 self.current_account = k
1335 self.update_history_tab()
1336 self.update_status()
1337 self.update_receive_tab()
1339 def create_status_bar(self):
1342 sb.setFixedHeight(35)
1343 qtVersion = qVersion()
1345 self.balance_label = QLabel("")
1346 sb.addWidget(self.balance_label)
1348 from version_getter import UpdateLabel
1349 self.updatelabel = UpdateLabel(self.config, sb)
1351 self.account_selector = QComboBox()
1352 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1353 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1354 sb.addPermanentWidget(self.account_selector)
1356 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1357 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1359 self.lock_icon = QIcon()
1360 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1361 sb.addPermanentWidget( self.password_button )
1363 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1364 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1365 sb.addPermanentWidget( self.seed_button )
1366 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1367 sb.addPermanentWidget( self.status_button )
1369 run_hook('create_status_bar', (sb,))
1371 self.setStatusBar(sb)
1374 def update_lock_icon(self):
1375 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1376 self.password_button.setIcon( icon )
1379 def update_buttons_on_seed(self):
1380 if self.wallet.has_seed():
1381 self.seed_button.show()
1383 self.seed_button.hide()
1385 if not self.wallet.is_watching_only():
1386 self.password_button.show()
1387 self.send_button.setText(_("Send"))
1389 self.password_button.hide()
1390 self.send_button.setText(_("Create unsigned transaction"))
1393 def change_password_dialog(self):
1394 from password_dialog import PasswordDialog
1395 d = PasswordDialog(self.wallet, self)
1397 self.update_lock_icon()
1400 def new_contact_dialog(self):
1403 d.setWindowTitle(_("New Contact"))
1404 vbox = QVBoxLayout(d)
1405 vbox.addWidget(QLabel(_('New Contact')+':'))
1407 grid = QGridLayout()
1410 grid.addWidget(QLabel(_("Address")), 1, 0)
1411 grid.addWidget(line1, 1, 1)
1412 grid.addWidget(QLabel(_("Name")), 2, 0)
1413 grid.addWidget(line2, 2, 1)
1415 vbox.addLayout(grid)
1416 vbox.addLayout(ok_cancel_buttons(d))
1421 address = str(line1.text())
1422 label = unicode(line2.text())
1424 if not is_valid(address):
1425 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1428 self.wallet.add_contact(address)
1430 self.wallet.set_label(address, label)
1432 self.update_contacts_tab()
1433 self.update_history_tab()
1434 self.update_completions()
1435 self.tabs.setCurrentIndex(3)
1439 def new_account_dialog(self, password):
1441 dialog = QDialog(self)
1443 dialog.setWindowTitle(_("New Account"))
1445 vbox = QVBoxLayout()
1446 vbox.addWidget(QLabel(_('Account name')+':'))
1449 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1450 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1455 vbox.addLayout(ok_cancel_buttons(dialog))
1456 dialog.setLayout(vbox)
1460 name = str(e.text())
1463 self.wallet.create_pending_account(name, password)
1464 self.update_receive_tab()
1465 self.tabs.setCurrentIndex(2)
1470 def show_master_public_keys(self):
1472 dialog = QDialog(self)
1474 dialog.setWindowTitle(_("Master Public Keys"))
1476 main_layout = QGridLayout()
1477 mpk_dict = self.wallet.get_master_public_keys()
1479 for key, value in mpk_dict.items():
1480 main_layout.addWidget(QLabel(key), i, 0)
1481 mpk_text = QTextEdit()
1482 mpk_text.setReadOnly(True)
1483 mpk_text.setMaximumHeight(170)
1484 mpk_text.setText(value)
1485 main_layout.addWidget(mpk_text, i + 1, 0)
1488 vbox = QVBoxLayout()
1489 vbox.addLayout(main_layout)
1490 vbox.addLayout(close_button(dialog))
1492 dialog.setLayout(vbox)
1497 def show_seed_dialog(self, password):
1498 if not self.wallet.has_seed():
1499 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1503 mnemonic = self.wallet.get_mnemonic(password)
1505 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1507 from seed_dialog import SeedDialog
1508 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1513 def show_qrcode(self, data, title = _("QR code")):
1517 d.setWindowTitle(title)
1518 d.setMinimumSize(270, 300)
1519 vbox = QVBoxLayout()
1520 qrw = QRCodeWidget(data)
1521 vbox.addWidget(qrw, 1)
1522 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1523 hbox = QHBoxLayout()
1526 filename = os.path.join(self.config.path, "qrcode.bmp")
1529 bmp.save_qrcode(qrw.qr, filename)
1530 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1532 def copy_to_clipboard():
1533 bmp.save_qrcode(qrw.qr, filename)
1534 self.app.clipboard().setImage(QImage(filename))
1535 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1537 b = QPushButton(_("Copy"))
1539 b.clicked.connect(copy_to_clipboard)
1541 b = QPushButton(_("Save"))
1543 b.clicked.connect(print_qr)
1545 b = QPushButton(_("Close"))
1547 b.clicked.connect(d.accept)
1550 vbox.addLayout(hbox)
1555 def do_protect(self, func, args):
1556 if self.wallet.use_encryption:
1557 password = self.password_dialog()
1563 if args != (False,):
1564 args = (self,) + args + (password,)
1566 args = (self,password)
1570 def show_public_keys(self, address):
1571 if not address: return
1573 pubkey_list = self.wallet.get_public_keys(address)
1574 except Exception as e:
1575 traceback.print_exc(file=sys.stdout)
1576 self.show_message(str(e))
1580 d.setMinimumSize(600, 200)
1582 vbox = QVBoxLayout()
1583 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1584 vbox.addWidget( QLabel(_("Public key") + ':'))
1586 keys.setReadOnly(True)
1587 keys.setText('\n'.join(pubkey_list))
1588 vbox.addWidget(keys)
1589 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1590 vbox.addLayout(close_button(d))
1595 def show_private_key(self, address, password):
1596 if not address: return
1598 pk_list = self.wallet.get_private_key(address, password)
1599 except Exception as e:
1600 traceback.print_exc(file=sys.stdout)
1601 self.show_message(str(e))
1605 d.setMinimumSize(600, 200)
1607 vbox = QVBoxLayout()
1608 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1609 vbox.addWidget( QLabel(_("Private key") + ':'))
1611 keys.setReadOnly(True)
1612 keys.setText('\n'.join(pk_list))
1613 vbox.addWidget(keys)
1614 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1615 vbox.addLayout(close_button(d))
1621 def do_sign(self, address, message, signature, password):
1622 message = unicode(message.toPlainText())
1623 message = message.encode('utf-8')
1625 sig = self.wallet.sign_message(str(address.text()), message, password)
1626 signature.setText(sig)
1627 except Exception as e:
1628 self.show_message(str(e))
1630 def do_verify(self, address, message, signature):
1631 message = unicode(message.toPlainText())
1632 message = message.encode('utf-8')
1633 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1634 self.show_message(_("Signature verified"))
1636 self.show_message(_("Error: wrong signature"))
1639 def sign_verify_message(self, address=''):
1642 d.setWindowTitle(_('Sign/verify Message'))
1643 d.setMinimumSize(410, 290)
1645 layout = QGridLayout(d)
1647 message_e = QTextEdit()
1648 layout.addWidget(QLabel(_('Message')), 1, 0)
1649 layout.addWidget(message_e, 1, 1)
1650 layout.setRowStretch(2,3)
1652 address_e = QLineEdit()
1653 address_e.setText(address)
1654 layout.addWidget(QLabel(_('Address')), 2, 0)
1655 layout.addWidget(address_e, 2, 1)
1657 signature_e = QTextEdit()
1658 layout.addWidget(QLabel(_('Signature')), 3, 0)
1659 layout.addWidget(signature_e, 3, 1)
1660 layout.setRowStretch(3,1)
1662 hbox = QHBoxLayout()
1664 b = QPushButton(_("Sign"))
1665 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1668 b = QPushButton(_("Verify"))
1669 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1672 b = QPushButton(_("Close"))
1673 b.clicked.connect(d.accept)
1675 layout.addLayout(hbox, 4, 1)
1680 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1682 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1683 message_e.setText(decrypted)
1684 except Exception as e:
1685 self.show_message(str(e))
1688 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1689 message = unicode(message_e.toPlainText())
1690 message = message.encode('utf-8')
1692 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1693 encrypted_e.setText(encrypted)
1694 except Exception as e:
1695 self.show_message(str(e))
1699 def encrypt_message(self, address = ''):
1702 d.setWindowTitle(_('Encrypt/decrypt Message'))
1703 d.setMinimumSize(610, 490)
1705 layout = QGridLayout(d)
1707 message_e = QTextEdit()
1708 layout.addWidget(QLabel(_('Message')), 1, 0)
1709 layout.addWidget(message_e, 1, 1)
1710 layout.setRowStretch(2,3)
1712 pubkey_e = QLineEdit()
1714 pubkey = self.wallet.getpubkeys(address)[0]
1715 pubkey_e.setText(pubkey)
1716 layout.addWidget(QLabel(_('Public key')), 2, 0)
1717 layout.addWidget(pubkey_e, 2, 1)
1719 encrypted_e = QTextEdit()
1720 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1721 layout.addWidget(encrypted_e, 3, 1)
1722 layout.setRowStretch(3,1)
1724 hbox = QHBoxLayout()
1725 b = QPushButton(_("Encrypt"))
1726 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1729 b = QPushButton(_("Decrypt"))
1730 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1733 b = QPushButton(_("Close"))
1734 b.clicked.connect(d.accept)
1737 layout.addLayout(hbox, 4, 1)
1741 def question(self, msg):
1742 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1744 def show_message(self, msg):
1745 QMessageBox.information(self, _('Message'), msg, _('OK'))
1747 def password_dialog(self, msg=None):
1750 d.setWindowTitle(_("Enter Password"))
1755 vbox = QVBoxLayout()
1757 msg = _('Please enter your password')
1758 vbox.addWidget(QLabel(msg))
1760 grid = QGridLayout()
1762 grid.addWidget(QLabel(_('Password')), 1, 0)
1763 grid.addWidget(pw, 1, 1)
1764 vbox.addLayout(grid)
1766 vbox.addLayout(ok_cancel_buttons(d))
1769 run_hook('password_dialog', pw, grid, 1)
1770 if not d.exec_(): return
1771 return unicode(pw.text())
1780 def tx_from_text(self, txt):
1781 "json or raw hexadecimal"
1784 tx = Transaction(txt)
1790 tx_dict = json.loads(str(txt))
1791 assert "hex" in tx_dict.keys()
1792 tx = Transaction(tx_dict["hex"])
1793 if tx_dict.has_key("input_info"):
1794 input_info = json.loads(tx_dict['input_info'])
1795 tx.add_input_info(input_info)
1798 traceback.print_exc(file=sys.stdout)
1801 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1805 def read_tx_from_file(self):
1806 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1810 with open(fileName, "r") as f:
1811 file_content = f.read()
1812 except (ValueError, IOError, os.error), reason:
1813 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1815 return self.tx_from_text(file_content)
1819 def sign_raw_transaction(self, tx, input_info, password):
1820 self.wallet.signrawtransaction(tx, input_info, [], password)
1822 def do_process_from_text(self):
1823 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1826 tx = self.tx_from_text(text)
1828 self.show_transaction(tx)
1830 def do_process_from_file(self):
1831 tx = self.read_tx_from_file()
1833 self.show_transaction(tx)
1835 def do_process_from_txid(self):
1836 from electrum import transaction
1837 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1839 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1841 tx = transaction.Transaction(r)
1843 self.show_transaction(tx)
1845 self.show_message("unknown transaction")
1847 def do_process_from_csvReader(self, csvReader):
1852 for position, row in enumerate(csvReader):
1854 if not is_valid(address):
1855 errors.append((position, address))
1857 amount = Decimal(row[1])
1858 amount = int(100000000*amount)
1859 outputs.append((address, amount))
1860 except (ValueError, IOError, os.error), reason:
1861 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1865 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1866 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1870 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1871 except Exception as e:
1872 self.show_message(str(e))
1875 self.show_transaction(tx)
1877 def do_process_from_csv_file(self):
1878 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1882 with open(fileName, "r") as f:
1883 csvReader = csv.reader(f)
1884 self.do_process_from_csvReader(csvReader)
1885 except (ValueError, IOError, os.error), reason:
1886 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1889 def do_process_from_csv_text(self):
1890 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1891 + _("Format: address, amount. One output per line"), _("Load CSV"))
1894 f = StringIO.StringIO(text)
1895 csvReader = csv.reader(f)
1896 self.do_process_from_csvReader(csvReader)
1901 def export_privkeys_dialog(self, password):
1902 if self.wallet.is_watching_only():
1903 self.show_message(_("This is a watching-only wallet"))
1907 d.setWindowTitle(_('Private keys'))
1908 d.setMinimumSize(850, 300)
1909 vbox = QVBoxLayout(d)
1911 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
1912 _("Exposing a single private key can compromise your entire wallet!"),
1913 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1914 vbox.addWidget(QLabel(msg))
1920 defaultname = 'electrum-private-keys.csv'
1921 select_msg = _('Select file to export your private keys to')
1922 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1923 vbox.addLayout(hbox)
1925 h, b = ok_cancel_buttons2(d, _('Export'))
1930 addresses = self.wallet.addresses(True)
1932 def privkeys_thread():
1933 for addr in addresses:
1937 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1938 d.emit(SIGNAL('computing_privkeys'))
1939 d.emit(SIGNAL('show_privkeys'))
1941 def show_privkeys():
1942 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
1946 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
1947 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
1948 threading.Thread(target=privkeys_thread).start()
1954 filename = filename_e.text()
1959 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
1960 except (IOError, os.error), reason:
1961 export_error_label = _("Electrum was unable to produce a private key-export.")
1962 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1964 except Exception as e:
1965 self.show_message(str(e))
1968 self.show_message(_("Private keys exported."))
1971 def do_export_privkeys(self, fileName, pklist, is_csv):
1972 with open(fileName, "w+") as f:
1974 transaction = csv.writer(f)
1975 transaction.writerow(["address", "private_key"])
1976 for addr, pk in pklist.items():
1977 transaction.writerow(["%34s"%addr,pk])
1980 f.write(json.dumps(pklist, indent = 4))
1983 def do_import_labels(self):
1984 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1985 if not labelsFile: return
1987 f = open(labelsFile, 'r')
1990 for key, value in json.loads(data).items():
1991 self.wallet.set_label(key, value)
1992 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1993 except (IOError, os.error), reason:
1994 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1997 def do_export_labels(self):
1998 labels = self.wallet.labels
2000 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2002 with open(fileName, 'w+') as f:
2003 json.dump(labels, f)
2004 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2005 except (IOError, os.error), reason:
2006 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2009 def export_history_dialog(self):
2012 d.setWindowTitle(_('Export History'))
2013 d.setMinimumSize(400, 200)
2014 vbox = QVBoxLayout(d)
2016 defaultname = os.path.expanduser('~/electrum-history.csv')
2017 select_msg = _('Select file to export your wallet transactions to')
2019 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2020 vbox.addLayout(hbox)
2024 h, b = ok_cancel_buttons2(d, _('Export'))
2029 filename = filename_e.text()
2034 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2035 except (IOError, os.error), reason:
2036 export_error_label = _("Electrum was unable to produce a transaction export.")
2037 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2040 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2043 def do_export_history(self, wallet, fileName, is_csv):
2044 history = wallet.get_tx_history()
2046 for item in history:
2047 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2049 if timestamp is not None:
2051 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2052 except [RuntimeError, TypeError, NameError] as reason:
2053 time_string = "unknown"
2056 time_string = "unknown"
2058 time_string = "pending"
2060 if value is not None:
2061 value_string = format_satoshis(value, True)
2066 fee_string = format_satoshis(fee, True)
2071 label, is_default_label = wallet.get_label(tx_hash)
2072 label = label.encode('utf-8')
2076 balance_string = format_satoshis(balance, False)
2078 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2080 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2082 with open(fileName, "w+") as f:
2084 transaction = csv.writer(f)
2085 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2087 transaction.writerow(line)
2090 f.write(json.dumps(lines, indent = 4))
2093 def sweep_key_dialog(self):
2095 d.setWindowTitle(_('Sweep private keys'))
2096 d.setMinimumSize(600, 300)
2098 vbox = QVBoxLayout(d)
2099 vbox.addWidget(QLabel(_("Enter private keys")))
2101 keys_e = QTextEdit()
2102 keys_e.setTabChangesFocus(True)
2103 vbox.addWidget(keys_e)
2105 h, address_e = address_field(self.wallet.addresses())
2109 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2110 vbox.addLayout(hbox)
2111 button.setEnabled(False)
2114 addr = str(address_e.text())
2115 if bitcoin.is_address(addr):
2119 pk = str(keys_e.toPlainText()).strip()
2120 if Wallet.is_private_key(pk):
2123 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2124 keys_e.textChanged.connect(f)
2125 address_e.textChanged.connect(f)
2129 fee = self.wallet.fee
2130 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2131 self.show_transaction(tx)
2135 def do_import_privkey(self, password):
2136 if not self.wallet.has_imported_keys():
2137 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2138 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2139 + _('Are you sure you understand what you are doing?'), 3, 4)
2142 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2145 text = str(text).split()
2150 addr = self.wallet.import_key(key, password)
2151 except Exception as e:
2157 addrlist.append(addr)
2159 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2161 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2162 self.update_receive_tab()
2163 self.update_history_tab()
2166 def settings_dialog(self):
2168 d.setWindowTitle(_('Electrum Settings'))
2170 vbox = QVBoxLayout()
2171 grid = QGridLayout()
2172 grid.setColumnStretch(0,1)
2174 nz_label = QLabel(_('Display zeros') + ':')
2175 grid.addWidget(nz_label, 0, 0)
2176 nz_e = AmountEdit(None,True)
2177 nz_e.setText("%d"% self.num_zeros)
2178 grid.addWidget(nz_e, 0, 1)
2179 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2180 grid.addWidget(HelpButton(msg), 0, 2)
2181 if not self.config.is_modifiable('num_zeros'):
2182 for w in [nz_e, nz_label]: w.setEnabled(False)
2184 lang_label=QLabel(_('Language') + ':')
2185 grid.addWidget(lang_label, 1, 0)
2186 lang_combo = QComboBox()
2187 from electrum.i18n import languages
2188 lang_combo.addItems(languages.values())
2190 index = languages.keys().index(self.config.get("language",''))
2193 lang_combo.setCurrentIndex(index)
2194 grid.addWidget(lang_combo, 1, 1)
2195 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2196 if not self.config.is_modifiable('language'):
2197 for w in [lang_combo, lang_label]: w.setEnabled(False)
2200 fee_label = QLabel(_('Transaction fee') + ':')
2201 grid.addWidget(fee_label, 2, 0)
2202 fee_e = AmountEdit(self.get_decimal_point)
2203 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2204 grid.addWidget(fee_e, 2, 1)
2205 msg = _('Fee per kilobyte of transaction.') + ' ' \
2206 + _('Recommended value') + ': ' + self.format_amount(20000)
2207 grid.addWidget(HelpButton(msg), 2, 2)
2208 if not self.config.is_modifiable('fee_per_kb'):
2209 for w in [fee_e, fee_label]: w.setEnabled(False)
2211 units = ['BTC', 'mBTC']
2212 unit_label = QLabel(_('Base unit') + ':')
2213 grid.addWidget(unit_label, 3, 0)
2214 unit_combo = QComboBox()
2215 unit_combo.addItems(units)
2216 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2217 grid.addWidget(unit_combo, 3, 1)
2218 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2219 + '\n1BTC=1000mBTC.\n' \
2220 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2222 usechange_cb = QCheckBox(_('Use change addresses'))
2223 usechange_cb.setChecked(self.wallet.use_change)
2224 grid.addWidget(usechange_cb, 4, 0)
2225 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2226 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2228 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2229 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2230 grid.addWidget(block_ex_label, 5, 0)
2231 block_ex_combo = QComboBox()
2232 block_ex_combo.addItems(block_explorers)
2233 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2234 grid.addWidget(block_ex_combo, 5, 1)
2235 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2237 show_tx = self.config.get('show_before_broadcast', False)
2238 showtx_cb = QCheckBox(_('Show before broadcast'))
2239 showtx_cb.setChecked(show_tx)
2240 grid.addWidget(showtx_cb, 6, 0)
2241 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2243 vbox.addLayout(grid)
2245 vbox.addLayout(ok_cancel_buttons(d))
2249 if not d.exec_(): return
2252 fee = self.fee_e.get_amount()
2254 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2257 self.wallet.set_fee(fee)
2259 nz = unicode(nz_e.text())
2264 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2267 if self.num_zeros != nz:
2269 self.config.set_key('num_zeros', nz, True)
2270 self.update_history_tab()
2271 self.update_receive_tab()
2273 usechange_result = usechange_cb.isChecked()
2274 if self.wallet.use_change != usechange_result:
2275 self.wallet.use_change = usechange_result
2276 self.wallet.storage.put('use_change', self.wallet.use_change)
2278 if showtx_cb.isChecked() != show_tx:
2279 self.config.set_key('show_before_broadcast', not show_tx)
2281 unit_result = units[unit_combo.currentIndex()]
2282 if self.base_unit() != unit_result:
2283 self.decimal_point = 8 if unit_result == 'BTC' else 5
2284 self.config.set_key('decimal_point', self.decimal_point, True)
2285 self.update_history_tab()
2286 self.update_status()
2288 need_restart = False
2290 lang_request = languages.keys()[lang_combo.currentIndex()]
2291 if lang_request != self.config.get('language'):
2292 self.config.set_key("language", lang_request, True)
2295 be_result = block_explorers[block_ex_combo.currentIndex()]
2296 self.config.set_key('block_explorer', be_result, True)
2298 run_hook('close_settings_dialog')
2301 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2304 def run_network_dialog(self):
2305 if not self.network:
2307 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2309 def closeEvent(self, event):
2311 self.config.set_key("is_maximized", self.isMaximized())
2312 if not self.isMaximized():
2314 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2315 self.save_column_widths()
2316 self.config.set_key("console-history", self.console.history[-50:], True)
2317 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2321 def plugins_dialog(self):
2322 from electrum.plugins import plugins
2325 d.setWindowTitle(_('Electrum Plugins'))
2328 vbox = QVBoxLayout(d)
2331 scroll = QScrollArea()
2332 scroll.setEnabled(True)
2333 scroll.setWidgetResizable(True)
2334 scroll.setMinimumSize(400,250)
2335 vbox.addWidget(scroll)
2339 w.setMinimumHeight(len(plugins)*35)
2341 grid = QGridLayout()
2342 grid.setColumnStretch(0,1)
2345 def do_toggle(cb, p, w):
2348 if w: w.setEnabled(r)
2350 def mk_toggle(cb, p, w):
2351 return lambda: do_toggle(cb,p,w)
2353 for i, p in enumerate(plugins):
2355 cb = QCheckBox(p.fullname())
2356 cb.setDisabled(not p.is_available())
2357 cb.setChecked(p.is_enabled())
2358 grid.addWidget(cb, i, 0)
2359 if p.requires_settings():
2360 w = p.settings_widget(self)
2361 w.setEnabled( p.is_enabled() )
2362 grid.addWidget(w, i, 1)
2365 cb.clicked.connect(mk_toggle(cb,p,w))
2366 grid.addWidget(HelpButton(p.description()), i, 2)
2368 print_msg(_("Error: cannot display plugin"), p)
2369 traceback.print_exc(file=sys.stdout)
2370 grid.setRowStretch(i+1,1)
2372 vbox.addLayout(close_button(d))
2377 def show_account_details(self, k):
2378 account = self.wallet.accounts[k]
2381 d.setWindowTitle(_('Account Details'))
2384 vbox = QVBoxLayout(d)
2385 name = self.wallet.get_account_name(k)
2386 label = QLabel('Name: ' + name)
2387 vbox.addWidget(label)
2389 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2391 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2393 vbox.addWidget(QLabel(_('Master Public Key:')))
2396 text.setReadOnly(True)
2397 text.setMaximumHeight(170)
2398 vbox.addWidget(text)
2400 mpk_text = '\n'.join( account.get_master_pubkeys() )
2401 text.setText(mpk_text)
2403 vbox.addLayout(close_button(d))