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))
122 set_language(config.get('language'))
124 self.funds_error = False
125 self.completions = QStringListModel()
127 self.tabs = tabs = QTabWidget(self)
128 self.column_widths = self.config.get("column_widths_2", default_column_widths )
129 tabs.addTab(self.create_history_tab(), _('History') )
130 tabs.addTab(self.create_send_tab(), _('Send') )
131 tabs.addTab(self.create_receive_tab(), _('Receive') )
132 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
133 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
134 tabs.addTab(self.create_console_tab(), _('Console') )
135 tabs.setMinimumSize(600, 400)
136 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
137 self.setCentralWidget(tabs)
139 g = self.config.get("winpos-qt",[100, 100, 840, 400])
140 self.setGeometry(g[0], g[1], g[2], g[3])
141 if self.config.get("is_maximized"):
144 self.setWindowIcon(QIcon(":icons/electrum.png"))
147 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
148 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
149 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
150 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
151 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
153 for i in range(tabs.count()):
154 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
156 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
157 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
158 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
159 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
160 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
162 self.history_list.setFocus(True)
166 self.network.register_callback('updated', lambda: self.need_update.set())
167 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
168 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
169 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
170 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
172 # set initial message
173 self.console.showMessage(self.network.banner)
178 def update_account_selector(self):
180 accounts = self.wallet.get_account_names()
181 self.account_selector.clear()
182 if len(accounts) > 1:
183 self.account_selector.addItems([_("All accounts")] + accounts.values())
184 self.account_selector.setCurrentIndex(0)
185 self.account_selector.show()
187 self.account_selector.hide()
190 def load_wallet(self, wallet):
194 self.update_wallet_format()
196 self.invoices = self.wallet.storage.get('invoices', {})
197 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
198 self.current_account = self.wallet.storage.get("current_account", None)
199 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
200 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
201 self.setWindowTitle( title )
203 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
204 self.notify_transactions()
205 self.update_account_selector()
207 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
208 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
209 self.password_menu.setEnabled(not self.wallet.is_watching_only())
210 self.seed_menu.setEnabled(self.wallet.has_seed())
211 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
212 self.import_menu.setEnabled(self.wallet.can_import())
214 self.update_lock_icon()
215 self.update_buttons_on_seed()
216 self.update_console()
218 run_hook('load_wallet', wallet)
221 def update_wallet_format(self):
222 # convert old-format imported keys
223 if self.wallet.imported_keys:
224 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
226 self.wallet.convert_imported_keys(password)
228 self.show_message("error")
231 def open_wallet(self):
232 wallet_folder = self.wallet.storage.path
233 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
237 storage = WalletStorage({'wallet_path': filename})
238 if not storage.file_exists:
239 self.show_message("file not found "+ filename)
242 self.wallet.stop_threads()
245 wallet = Wallet(storage)
246 wallet.start_threads(self.network)
248 self.load_wallet(wallet)
252 def backup_wallet(self):
254 path = self.wallet.storage.path
255 wallet_folder = os.path.dirname(path)
256 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
260 new_path = os.path.join(wallet_folder, filename)
263 shutil.copy2(path, new_path)
264 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
265 except (IOError, os.error), reason:
266 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
269 def new_wallet(self):
272 wallet_folder = os.path.dirname(self.wallet.storage.path)
273 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
276 filename = os.path.join(wallet_folder, filename)
278 storage = WalletStorage({'wallet_path': filename})
279 if storage.file_exists:
280 QMessageBox.critical(None, "Error", _("File exists"))
283 wizard = installwizard.InstallWizard(self.config, self.network, storage)
284 wallet = wizard.run('new')
286 self.load_wallet(wallet)
290 def init_menubar(self):
293 file_menu = menubar.addMenu(_("&File"))
294 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
295 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
296 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
297 file_menu.addAction(_("&Quit"), self.close)
299 wallet_menu = menubar.addMenu(_("&Wallet"))
300 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
301 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
303 wallet_menu.addSeparator()
305 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
306 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
307 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
309 wallet_menu.addSeparator()
310 labels_menu = wallet_menu.addMenu(_("&Labels"))
311 labels_menu.addAction(_("&Import"), self.do_import_labels)
312 labels_menu.addAction(_("&Export"), self.do_export_labels)
314 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
315 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
316 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
317 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
318 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
320 tools_menu = menubar.addMenu(_("&Tools"))
322 # Settings / Preferences are all reserved keywords in OSX using this as work around
323 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
324 tools_menu.addAction(_("&Network"), self.run_network_dialog)
325 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
326 tools_menu.addSeparator()
327 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
328 #tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
329 tools_menu.addSeparator()
331 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
332 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
333 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
335 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
336 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
337 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
338 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
340 help_menu = menubar.addMenu(_("&Help"))
341 help_menu.addAction(_("&About"), self.show_about)
342 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
343 help_menu.addSeparator()
344 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
345 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
347 self.setMenuBar(menubar)
349 def show_about(self):
350 QMessageBox.about(self, "Electrum",
351 _("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."))
353 def show_report_bug(self):
354 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
355 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
358 def notify_transactions(self):
359 if not self.network or not self.network.is_connected():
362 print_error("Notifying GUI")
363 if len(self.network.pending_transactions_for_notifications) > 0:
364 # Combine the transactions if there are more then three
365 tx_amount = len(self.network.pending_transactions_for_notifications)
368 for tx in self.network.pending_transactions_for_notifications:
369 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
373 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
374 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
376 self.network.pending_transactions_for_notifications = []
378 for tx in self.network.pending_transactions_for_notifications:
380 self.network.pending_transactions_for_notifications.remove(tx)
381 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
383 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
385 def notify(self, message):
386 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
390 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
391 def getOpenFileName(self, title, filter = ""):
392 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
393 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
394 if fileName and directory != os.path.dirname(fileName):
395 self.config.set_key('io_dir', os.path.dirname(fileName), True)
398 def getSaveFileName(self, title, filename, filter = ""):
399 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
400 path = os.path.join( directory, filename )
401 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
402 if fileName and directory != os.path.dirname(fileName):
403 self.config.set_key('io_dir', os.path.dirname(fileName), True)
407 QMainWindow.close(self)
408 run_hook('close_main_window')
410 def connect_slots(self, sender):
411 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
412 self.previous_payto_e=''
414 def timer_actions(self):
415 if self.need_update.is_set():
417 self.need_update.clear()
418 run_hook('timer_actions')
420 def format_amount(self, x, is_diff=False, whitespaces=False):
421 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
424 def get_decimal_point(self):
425 return self.decimal_point
429 assert self.decimal_point in [5,8]
430 return "BTC" if self.decimal_point == 8 else "mBTC"
433 def update_status(self):
434 if self.network is None or not self.network.is_running():
436 icon = QIcon(":icons/status_disconnected.png")
438 elif self.network.is_connected():
439 if not self.wallet.up_to_date:
440 text = _("Synchronizing...")
441 icon = QIcon(":icons/status_waiting.png")
442 elif self.network.server_lag > 1:
443 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
444 icon = QIcon(":icons/status_lagging.png")
446 c, u = self.wallet.get_account_balance(self.current_account)
447 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
448 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
450 # append fiat balance and price from exchange rate plugin
452 run_hook('get_fiat_status_text', c+u, r)
457 self.tray.setToolTip(text)
458 icon = QIcon(":icons/status_connected.png")
460 text = _("Not connected")
461 icon = QIcon(":icons/status_disconnected.png")
463 self.balance_label.setText(text)
464 self.status_button.setIcon( icon )
467 def update_wallet(self):
469 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
470 self.update_history_tab()
471 self.update_receive_tab()
472 self.update_contacts_tab()
473 self.update_completions()
474 self.update_invoices_tab()
477 def create_history_tab(self):
478 self.history_list = l = MyTreeWidget(self)
480 for i,width in enumerate(self.column_widths['history']):
481 l.setColumnWidth(i, width)
482 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
483 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
484 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
486 l.customContextMenuRequested.connect(self.create_history_menu)
490 def create_history_menu(self, position):
491 self.history_list.selectedIndexes()
492 item = self.history_list.currentItem()
493 be = self.config.get('block_explorer', 'Blockchain.info')
494 if be == 'Blockchain.info':
495 block_explorer = 'https://blockchain.info/tx/'
496 elif be == 'Blockr.io':
497 block_explorer = 'https://blockr.io/tx/info/'
498 elif be == 'Insight.is':
499 block_explorer = 'http://live.insight.is/tx/'
501 tx_hash = str(item.data(0, Qt.UserRole).toString())
502 if not tx_hash: return
504 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
505 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
506 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
507 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
508 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
511 def show_transaction(self, tx):
512 import transaction_dialog
513 d = transaction_dialog.TxDialog(tx, self)
516 def tx_label_clicked(self, item, column):
517 if column==2 and item.isSelected():
519 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
520 self.history_list.editItem( item, column )
521 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
524 def tx_label_changed(self, item, column):
528 tx_hash = str(item.data(0, Qt.UserRole).toString())
529 tx = self.wallet.transactions.get(tx_hash)
530 text = unicode( item.text(2) )
531 self.wallet.set_label(tx_hash, text)
533 item.setForeground(2, QBrush(QColor('black')))
535 text = self.wallet.get_default_label(tx_hash)
536 item.setText(2, text)
537 item.setForeground(2, QBrush(QColor('gray')))
541 def edit_label(self, is_recv):
542 l = self.receive_list if is_recv else self.contacts_list
543 item = l.currentItem()
544 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
545 l.editItem( item, 1 )
546 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
550 def address_label_clicked(self, item, column, l, column_addr, column_label):
551 if column == column_label and item.isSelected():
552 is_editable = item.data(0, 32).toBool()
555 addr = unicode( item.text(column_addr) )
556 label = unicode( item.text(column_label) )
557 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
558 l.editItem( item, column )
559 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
562 def address_label_changed(self, item, column, l, column_addr, column_label):
563 if column == column_label:
564 addr = unicode( item.text(column_addr) )
565 text = unicode( item.text(column_label) )
566 is_editable = item.data(0, 32).toBool()
570 changed = self.wallet.set_label(addr, text)
572 self.update_history_tab()
573 self.update_completions()
575 self.current_item_changed(item)
577 run_hook('item_changed', item, column)
580 def current_item_changed(self, a):
581 run_hook('current_item_changed', a)
585 def update_history_tab(self):
587 self.history_list.clear()
588 for item in self.wallet.get_tx_history(self.current_account):
589 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
590 time_str = _("unknown")
593 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
595 time_str = _("error")
598 time_str = 'unverified'
599 icon = QIcon(":icons/unconfirmed.png")
602 icon = QIcon(":icons/unconfirmed.png")
604 icon = QIcon(":icons/clock%d.png"%conf)
606 icon = QIcon(":icons/confirmed.png")
608 if value is not None:
609 v_str = self.format_amount(value, True, whitespaces=True)
613 balance_str = self.format_amount(balance, whitespaces=True)
616 label, is_default_label = self.wallet.get_label(tx_hash)
618 label = _('Pruned transaction outputs')
619 is_default_label = False
621 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
622 item.setFont(2, QFont(MONOSPACE_FONT))
623 item.setFont(3, QFont(MONOSPACE_FONT))
624 item.setFont(4, QFont(MONOSPACE_FONT))
626 item.setForeground(3, QBrush(QColor("#BC1E1E")))
628 item.setData(0, Qt.UserRole, tx_hash)
629 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
631 item.setForeground(2, QBrush(QColor('grey')))
633 item.setIcon(0, icon)
634 self.history_list.insertTopLevelItem(0,item)
637 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
638 run_hook('history_tab_update')
641 def create_send_tab(self):
644 grid = QGridLayout(w)
646 grid.setColumnMinimumWidth(3,300)
647 grid.setColumnStretch(5,1)
648 grid.setRowStretch(8, 1)
650 from paytoedit import PayToEdit
651 self.amount_e = AmountEdit(self.get_decimal_point)
652 self.payto_e = PayToEdit(self.amount_e)
653 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)'))
654 grid.addWidget(QLabel(_('Pay to')), 1, 0)
655 grid.addWidget(self.payto_e, 1, 1, 1, 3)
656 grid.addWidget(self.payto_help, 1, 4)
658 completer = QCompleter()
659 completer.setCaseSensitivity(False)
660 self.payto_e.setCompleter(completer)
661 completer.setModel(self.completions)
663 self.message_e = MyLineEdit()
664 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.'))
665 grid.addWidget(QLabel(_('Description')), 2, 0)
666 grid.addWidget(self.message_e, 2, 1, 1, 3)
667 grid.addWidget(self.message_help, 2, 4)
669 self.from_label = QLabel(_('From'))
670 grid.addWidget(self.from_label, 3, 0)
671 self.from_list = MyTreeWidget(self)
672 self.from_list.setColumnCount(2)
673 self.from_list.setColumnWidth(0, 350)
674 self.from_list.setColumnWidth(1, 50)
675 self.from_list.setHeaderHidden(True)
676 self.from_list.setMaximumHeight(80)
677 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
678 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
679 grid.addWidget(self.from_list, 3, 1, 1, 3)
680 self.set_pay_from([])
682 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
683 + _('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.') \
684 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
685 grid.addWidget(QLabel(_('Amount')), 4, 0)
686 grid.addWidget(self.amount_e, 4, 1, 1, 2)
687 grid.addWidget(self.amount_help, 4, 3)
689 self.fee_e = AmountEdit(self.get_decimal_point)
690 grid.addWidget(QLabel(_('Fee')), 5, 0)
691 grid.addWidget(self.fee_e, 5, 1, 1, 2)
692 grid.addWidget(HelpButton(
693 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
694 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
695 + _('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)
697 run_hook('exchange_rate_button', grid)
699 self.send_button = EnterButton(_("Send"), self.do_send)
700 grid.addWidget(self.send_button, 6, 1)
702 b = EnterButton(_("Clear"), self.do_clear)
703 grid.addWidget(b, 6, 2)
705 self.payto_sig = QLabel('')
706 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
708 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
709 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
712 def entry_changed( is_fee ):
713 self.funds_error = False
715 if self.amount_e.is_shortcut:
716 self.amount_e.is_shortcut = False
717 sendable = self.get_sendable_balance()
718 # there is only one output because we are completely spending inputs
719 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
720 fee = self.wallet.estimated_fee(inputs, 1)
722 self.amount_e.setText( self.format_amount(amount) )
723 self.fee_e.setText( self.format_amount( fee ) )
726 amount = self.amount_e.get_amount()
727 fee = self.fee_e.get_amount()
729 if not is_fee: fee = None
732 # assume that there will be 2 outputs (one for change)
733 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
735 self.fee_e.setText( self.format_amount( fee ) )
738 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
742 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
743 self.funds_error = True
744 text = _( "Not enough funds" )
745 c, u = self.wallet.get_frozen_balance()
746 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
748 self.statusBar().showMessage(text)
749 self.amount_e.setPalette(palette)
750 self.fee_e.setPalette(palette)
752 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
753 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
755 run_hook('create_send_tab', grid)
758 def from_list_delete(self, item):
759 i = self.from_list.indexOfTopLevelItem(item)
761 self.redraw_from_list()
763 def from_list_menu(self, position):
764 item = self.from_list.itemAt(position)
766 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
767 menu.exec_(self.from_list.viewport().mapToGlobal(position))
769 def set_pay_from(self, domain = None):
770 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
771 self.redraw_from_list()
773 def redraw_from_list(self):
774 self.from_list.clear()
775 self.from_label.setHidden(len(self.pay_from) == 0)
776 self.from_list.setHidden(len(self.pay_from) == 0)
779 h = x.get('prevout_hash')
780 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
782 for item in self.pay_from:
783 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
785 def update_completions(self):
787 for addr,label in self.wallet.labels.items():
788 if addr in self.wallet.addressbook:
789 l.append( label + ' <' + addr + '>')
791 run_hook('update_completions', l)
792 self.completions.setStringList(l)
796 return lambda s, *args: s.do_protect(func, args)
800 label = unicode( self.message_e.text() )
802 if self.gui_object.payment_request:
803 outputs = self.gui_object.payment_request.outputs
805 outputs = self.payto_e.get_outputs()
808 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
811 for addr, x in outputs:
812 if addr is None or not bitcoin.is_address(addr):
813 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
816 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
819 amount = sum(map(lambda x:x[1], outputs))
822 fee = self.fee_e.get_amount()
824 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
827 confirm_amount = self.config.get('confirm_amount', 100000000)
828 if amount >= confirm_amount:
829 o = '\n'.join(map(lambda x:x[0], outputs))
830 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
833 confirm_fee = self.config.get('confirm_fee', 100000)
834 if fee >= confirm_fee:
835 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()}):
838 self.send_tx(outputs, fee, label)
843 def send_tx(self, outputs, fee, label, password):
844 self.send_button.setDisabled(True)
846 # first, create an unsigned tx
847 coins = self.get_coins()
849 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
851 except Exception as e:
852 traceback.print_exc(file=sys.stdout)
853 self.show_message(str(e))
854 self.send_button.setDisabled(False)
857 # call hook to see if plugin needs gui interaction
858 run_hook('send_tx', tx)
864 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
865 self.wallet.sign_transaction(tx, keypairs, password)
866 return tx, fee, label
868 def sign_done(tx, fee, label):
870 self.show_message(tx.error)
871 self.send_button.setDisabled(False)
873 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
874 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
875 self.send_button.setDisabled(False)
878 self.wallet.set_label(tx.hash(), label)
880 if not tx.is_complete() or self.config.get('show_before_broadcast'):
881 self.show_transaction(tx)
883 self.send_button.setDisabled(False)
886 self.broadcast_transaction(tx)
888 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
889 self.waiting_dialog.start()
893 def broadcast_transaction(self, tx):
895 def broadcast_thread():
896 if self.gui_object.payment_request:
897 refund_address = self.wallet.addresses()[0]
898 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
899 self.gui_object.payment_request = None
901 status, msg = self.wallet.sendtx(tx)
904 def broadcast_done(status, msg):
906 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
909 QMessageBox.warning(self, _('Error'), msg, _('OK'))
910 self.send_button.setDisabled(False)
912 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
913 self.waiting_dialog.start()
917 def prepare_for_payment_request(self):
918 self.tabs.setCurrentIndex(1)
919 self.payto_e.is_pr = True
920 for e in [self.payto_e, self.amount_e, self.message_e]:
922 for h in [self.payto_help, self.amount_help, self.message_help]:
924 self.payto_e.setText(_("please wait..."))
927 def payment_request_ok(self):
928 pr = self.gui_object.payment_request
931 self.invoices[pr_id] = (pr.get_domain(), pr.get_amount())
932 self.wallet.storage.put('invoices', self.invoices)
933 self.update_invoices_tab()
935 self.payto_help.show()
936 self.payto_help.set_alt(pr.status)
937 self.payto_e.setGreen()
938 self.payto_e.setText(pr.domain)
939 self.amount_e.setText(self.format_amount(pr.get_amount()))
940 self.message_e.setText(pr.memo)
942 def payment_request_error(self):
944 self.show_message(self.gui_object.payment_request.error)
945 self.gui_object.payment_request = None
947 def set_send(self, address, amount, label, message):
949 if label and self.wallet.labels.get(address) != label:
950 if self.question('Give label "%s" to address %s ?'%(label,address)):
951 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
952 self.wallet.addressbook.append(address)
953 self.wallet.set_label(address, label)
955 self.tabs.setCurrentIndex(1)
956 label = self.wallet.labels.get(address)
957 m_addr = label + ' <'+ address +'>' if label else address
958 self.payto_e.setText(m_addr)
960 self.message_e.setText(message)
962 self.amount_e.setText(amount)
966 self.payto_e.is_pr = False
967 self.payto_sig.setVisible(False)
968 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
972 for h in [self.payto_help, self.amount_help, self.message_help]:
975 self.payto_help.set_alt(None)
977 self.set_pay_from([])
982 def set_addrs_frozen(self,addrs,freeze):
984 if not addr: continue
985 if addr in self.wallet.frozen_addresses and not freeze:
986 self.wallet.unfreeze(addr)
987 elif addr not in self.wallet.frozen_addresses and freeze:
988 self.wallet.freeze(addr)
989 self.update_receive_tab()
993 def create_list_tab(self, headers):
994 "generic tab creation method"
995 l = MyTreeWidget(self)
996 l.setColumnCount( len(headers) )
997 l.setHeaderLabels( headers )
1000 vbox = QVBoxLayout()
1007 vbox.addWidget(buttons)
1012 def create_receive_tab(self):
1013 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1014 l.setContextMenuPolicy(Qt.CustomContextMenu)
1015 l.customContextMenuRequested.connect(self.create_receive_menu)
1016 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1017 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1018 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1019 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1020 self.receive_list = l
1026 def save_column_widths(self):
1027 self.column_widths["receive"] = []
1028 for i in range(self.receive_list.columnCount() -1):
1029 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1031 self.column_widths["history"] = []
1032 for i in range(self.history_list.columnCount() - 1):
1033 self.column_widths["history"].append(self.history_list.columnWidth(i))
1035 self.column_widths["contacts"] = []
1036 for i in range(self.contacts_list.columnCount() - 1):
1037 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1039 self.config.set_key("column_widths_2", self.column_widths, True)
1042 def create_contacts_tab(self):
1043 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1044 l.setContextMenuPolicy(Qt.CustomContextMenu)
1045 l.customContextMenuRequested.connect(self.create_contact_menu)
1046 for i,width in enumerate(self.column_widths['contacts']):
1047 l.setColumnWidth(i, width)
1049 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1050 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1051 self.contacts_list = l
1055 def create_invoices_tab(self):
1056 l, w = self.create_list_tab([_('Requestor'), _('Amount'), _('Status')])
1057 l.setContextMenuPolicy(Qt.CustomContextMenu)
1058 l.customContextMenuRequested.connect(self.create_invoice_menu)
1059 self.invoices_list = l
1062 def update_invoices_tab(self):
1063 invoices = self.wallet.storage.get('invoices', {})
1064 l = self.invoices_list
1067 for item, value in invoices.items():
1068 domain, amount = value
1069 item = QTreeWidgetItem( [ domain, self.format_amount(amount), ""] )
1070 l.addTopLevelItem(item)
1072 l.setCurrentItem(l.topLevelItem(0))
1076 def delete_imported_key(self, addr):
1077 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1078 self.wallet.delete_imported_key(addr)
1079 self.update_receive_tab()
1080 self.update_history_tab()
1082 def edit_account_label(self, k):
1083 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1085 label = unicode(text)
1086 self.wallet.set_label(k,label)
1087 self.update_receive_tab()
1089 def account_set_expanded(self, item, k, b):
1091 self.accounts_expanded[k] = b
1093 def create_account_menu(self, position, k, item):
1095 if item.isExpanded():
1096 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1098 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1099 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1100 if self.wallet.seed_version > 4:
1101 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1102 if self.wallet.account_is_pending(k):
1103 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1104 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1106 def delete_pending_account(self, k):
1107 self.wallet.delete_pending_account(k)
1108 self.update_receive_tab()
1110 def create_receive_menu(self, position):
1111 # fixme: this function apparently has a side effect.
1112 # if it is not called the menu pops up several times
1113 #self.receive_list.selectedIndexes()
1115 selected = self.receive_list.selectedItems()
1116 multi_select = len(selected) > 1
1117 addrs = [unicode(item.text(0)) for item in selected]
1118 if not multi_select:
1119 item = self.receive_list.itemAt(position)
1123 if not is_valid(addr):
1124 k = str(item.data(0,32).toString())
1126 self.create_account_menu(position, k, item)
1128 item.setExpanded(not item.isExpanded())
1132 if not multi_select:
1133 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1134 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1135 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1136 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1137 if not self.wallet.is_watching_only():
1138 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1139 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1140 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1141 if self.wallet.is_imported(addr):
1142 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1144 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1145 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1146 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1147 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1149 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1150 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1152 run_hook('receive_menu', menu, addrs)
1153 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1156 def get_sendable_balance(self):
1157 return sum(map(lambda x:x['value'], self.get_coins()))
1160 def get_coins(self):
1162 return self.pay_from
1164 domain = self.wallet.get_account_addresses(self.current_account)
1165 for i in self.wallet.frozen_addresses:
1166 if i in domain: domain.remove(i)
1167 return self.wallet.get_unspent_coins(domain)
1170 def send_from_addresses(self, addrs):
1171 self.set_pay_from( addrs )
1172 self.tabs.setCurrentIndex(1)
1175 def payto(self, addr):
1177 label = self.wallet.labels.get(addr)
1178 m_addr = label + ' <' + addr + '>' if label else addr
1179 self.tabs.setCurrentIndex(1)
1180 self.payto_e.setText(m_addr)
1181 self.amount_e.setFocus()
1184 def delete_contact(self, x):
1185 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1186 self.wallet.delete_contact(x)
1187 self.wallet.set_label(x, None)
1188 self.update_history_tab()
1189 self.update_contacts_tab()
1190 self.update_completions()
1193 def create_contact_menu(self, position):
1194 item = self.contacts_list.itemAt(position)
1197 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1199 addr = unicode(item.text(0))
1200 label = unicode(item.text(1))
1201 is_editable = item.data(0,32).toBool()
1202 payto_addr = item.data(0,33).toString()
1203 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1204 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1205 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1207 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1208 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1210 run_hook('create_contact_menu', menu, item)
1211 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1213 def delete_invoice(self, item):
1214 k = self.invoices_list.indexOfTopLevelItem(item)
1215 key = self.invoices.keys()[k]
1216 self.invoices.pop(key)
1217 self.wallet.storage.put('invoices', self.invoices)
1218 self.update_invoices_tab()
1220 def create_invoice_menu(self, position):
1221 item = self.invoices_list.itemAt(position)
1225 menu.addAction(_("Delete"), lambda: self.delete_invoice(item))
1226 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1229 def update_receive_item(self, item):
1230 item.setFont(0, QFont(MONOSPACE_FONT))
1231 address = str(item.data(0,0).toString())
1232 label = self.wallet.labels.get(address,'')
1233 item.setData(1,0,label)
1234 item.setData(0,32, True) # is editable
1236 run_hook('update_receive_item', address, item)
1238 if not self.wallet.is_mine(address): return
1240 c, u = self.wallet.get_addr_balance(address)
1241 balance = self.format_amount(c + u)
1242 item.setData(2,0,balance)
1244 if address in self.wallet.frozen_addresses:
1245 item.setBackgroundColor(0, QColor('lightblue'))
1248 def update_receive_tab(self):
1249 l = self.receive_list
1250 # extend the syntax for consistency
1251 l.addChild = l.addTopLevelItem
1252 l.insertChild = l.insertTopLevelItem
1255 for i,width in enumerate(self.column_widths['receive']):
1256 l.setColumnWidth(i, width)
1258 accounts = self.wallet.get_accounts()
1259 if self.current_account is None:
1260 account_items = sorted(accounts.items())
1262 account_items = [(self.current_account, accounts.get(self.current_account))]
1265 for k, account in account_items:
1267 if len(accounts) > 1:
1268 name = self.wallet.get_account_name(k)
1269 c,u = self.wallet.get_account_balance(k)
1270 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1271 l.addTopLevelItem(account_item)
1272 account_item.setExpanded(self.accounts_expanded.get(k, True))
1273 account_item.setData(0, 32, k)
1277 sequences = [0,1] if account.has_change() else [0]
1278 for is_change in sequences:
1279 if len(sequences) > 1:
1280 name = _("Receiving") if not is_change else _("Change")
1281 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1282 account_item.addChild(seq_item)
1284 seq_item.setExpanded(True)
1286 seq_item = account_item
1288 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1294 for address in account.get_addresses(is_change):
1296 num, is_used = self.wallet.is_used(address)
1299 if gap > self.wallet.gap_limit:
1304 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1305 self.update_receive_item(item)
1307 item.setBackgroundColor(1, QColor('red'))
1311 seq_item.insertChild(0,used_item)
1313 used_item.addChild(item)
1315 seq_item.addChild(item)
1317 # we use column 1 because column 0 may be hidden
1318 l.setCurrentItem(l.topLevelItem(0),1)
1321 def update_contacts_tab(self):
1322 l = self.contacts_list
1325 for address in self.wallet.addressbook:
1326 label = self.wallet.labels.get(address,'')
1327 n = self.wallet.get_num_tx(address)
1328 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1329 item.setFont(0, QFont(MONOSPACE_FONT))
1330 # 32 = label can be edited (bool)
1331 item.setData(0,32, True)
1333 item.setData(0,33, address)
1334 l.addTopLevelItem(item)
1336 run_hook('update_contacts_tab', l)
1337 l.setCurrentItem(l.topLevelItem(0))
1341 def create_console_tab(self):
1342 from console import Console
1343 self.console = console = Console()
1347 def update_console(self):
1348 console = self.console
1349 console.history = self.config.get("console-history",[])
1350 console.history_index = len(console.history)
1352 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1353 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1355 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1357 def mkfunc(f, method):
1358 return lambda *args: apply( f, (method, args, self.password_dialog ))
1360 if m[0]=='_' or m in ['network','wallet']: continue
1361 methods[m] = mkfunc(c._run, m)
1363 console.updateNamespace(methods)
1366 def change_account(self,s):
1367 if s == _("All accounts"):
1368 self.current_account = None
1370 accounts = self.wallet.get_account_names()
1371 for k, v in accounts.items():
1373 self.current_account = k
1374 self.update_history_tab()
1375 self.update_status()
1376 self.update_receive_tab()
1378 def create_status_bar(self):
1381 sb.setFixedHeight(35)
1382 qtVersion = qVersion()
1384 self.balance_label = QLabel("")
1385 sb.addWidget(self.balance_label)
1387 from version_getter import UpdateLabel
1388 self.updatelabel = UpdateLabel(self.config, sb)
1390 self.account_selector = QComboBox()
1391 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1392 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1393 sb.addPermanentWidget(self.account_selector)
1395 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1396 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1398 self.lock_icon = QIcon()
1399 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1400 sb.addPermanentWidget( self.password_button )
1402 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1403 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1404 sb.addPermanentWidget( self.seed_button )
1405 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1406 sb.addPermanentWidget( self.status_button )
1408 run_hook('create_status_bar', (sb,))
1410 self.setStatusBar(sb)
1413 def update_lock_icon(self):
1414 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1415 self.password_button.setIcon( icon )
1418 def update_buttons_on_seed(self):
1419 if self.wallet.has_seed():
1420 self.seed_button.show()
1422 self.seed_button.hide()
1424 if not self.wallet.is_watching_only():
1425 self.password_button.show()
1426 self.send_button.setText(_("Send"))
1428 self.password_button.hide()
1429 self.send_button.setText(_("Create unsigned transaction"))
1432 def change_password_dialog(self):
1433 from password_dialog import PasswordDialog
1434 d = PasswordDialog(self.wallet, self)
1436 self.update_lock_icon()
1439 def new_contact_dialog(self):
1442 d.setWindowTitle(_("New Contact"))
1443 vbox = QVBoxLayout(d)
1444 vbox.addWidget(QLabel(_('New Contact')+':'))
1446 grid = QGridLayout()
1449 grid.addWidget(QLabel(_("Address")), 1, 0)
1450 grid.addWidget(line1, 1, 1)
1451 grid.addWidget(QLabel(_("Name")), 2, 0)
1452 grid.addWidget(line2, 2, 1)
1454 vbox.addLayout(grid)
1455 vbox.addLayout(ok_cancel_buttons(d))
1460 address = str(line1.text())
1461 label = unicode(line2.text())
1463 if not is_valid(address):
1464 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1467 self.wallet.add_contact(address)
1469 self.wallet.set_label(address, label)
1471 self.update_contacts_tab()
1472 self.update_history_tab()
1473 self.update_completions()
1474 self.tabs.setCurrentIndex(3)
1478 def new_account_dialog(self, password):
1480 dialog = QDialog(self)
1482 dialog.setWindowTitle(_("New Account"))
1484 vbox = QVBoxLayout()
1485 vbox.addWidget(QLabel(_('Account name')+':'))
1488 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1489 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1494 vbox.addLayout(ok_cancel_buttons(dialog))
1495 dialog.setLayout(vbox)
1499 name = str(e.text())
1502 self.wallet.create_pending_account(name, password)
1503 self.update_receive_tab()
1504 self.tabs.setCurrentIndex(2)
1509 def show_master_public_keys(self):
1511 dialog = QDialog(self)
1513 dialog.setWindowTitle(_("Master Public Keys"))
1515 main_layout = QGridLayout()
1516 mpk_dict = self.wallet.get_master_public_keys()
1518 for key, value in mpk_dict.items():
1519 main_layout.addWidget(QLabel(key), i, 0)
1520 mpk_text = QTextEdit()
1521 mpk_text.setReadOnly(True)
1522 mpk_text.setMaximumHeight(170)
1523 mpk_text.setText(value)
1524 main_layout.addWidget(mpk_text, i + 1, 0)
1527 vbox = QVBoxLayout()
1528 vbox.addLayout(main_layout)
1529 vbox.addLayout(close_button(dialog))
1531 dialog.setLayout(vbox)
1536 def show_seed_dialog(self, password):
1537 if not self.wallet.has_seed():
1538 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1542 mnemonic = self.wallet.get_mnemonic(password)
1544 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1546 from seed_dialog import SeedDialog
1547 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1552 def show_qrcode(self, data, title = _("QR code")):
1556 d.setWindowTitle(title)
1557 d.setMinimumSize(270, 300)
1558 vbox = QVBoxLayout()
1559 qrw = QRCodeWidget(data)
1560 vbox.addWidget(qrw, 1)
1561 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1562 hbox = QHBoxLayout()
1565 filename = os.path.join(self.config.path, "qrcode.bmp")
1568 bmp.save_qrcode(qrw.qr, filename)
1569 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1571 def copy_to_clipboard():
1572 bmp.save_qrcode(qrw.qr, filename)
1573 self.app.clipboard().setImage(QImage(filename))
1574 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1576 b = QPushButton(_("Copy"))
1578 b.clicked.connect(copy_to_clipboard)
1580 b = QPushButton(_("Save"))
1582 b.clicked.connect(print_qr)
1584 b = QPushButton(_("Close"))
1586 b.clicked.connect(d.accept)
1589 vbox.addLayout(hbox)
1594 def do_protect(self, func, args):
1595 if self.wallet.use_encryption:
1596 password = self.password_dialog()
1602 if args != (False,):
1603 args = (self,) + args + (password,)
1605 args = (self,password)
1609 def show_public_keys(self, address):
1610 if not address: return
1612 pubkey_list = self.wallet.get_public_keys(address)
1613 except Exception as e:
1614 traceback.print_exc(file=sys.stdout)
1615 self.show_message(str(e))
1619 d.setMinimumSize(600, 200)
1621 vbox = QVBoxLayout()
1622 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1623 vbox.addWidget( QLabel(_("Public key") + ':'))
1625 keys.setReadOnly(True)
1626 keys.setText('\n'.join(pubkey_list))
1627 vbox.addWidget(keys)
1628 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1629 vbox.addLayout(close_button(d))
1634 def show_private_key(self, address, password):
1635 if not address: return
1637 pk_list = self.wallet.get_private_key(address, password)
1638 except Exception as e:
1639 traceback.print_exc(file=sys.stdout)
1640 self.show_message(str(e))
1644 d.setMinimumSize(600, 200)
1646 vbox = QVBoxLayout()
1647 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1648 vbox.addWidget( QLabel(_("Private key") + ':'))
1650 keys.setReadOnly(True)
1651 keys.setText('\n'.join(pk_list))
1652 vbox.addWidget(keys)
1653 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1654 vbox.addLayout(close_button(d))
1660 def do_sign(self, address, message, signature, password):
1661 message = unicode(message.toPlainText())
1662 message = message.encode('utf-8')
1664 sig = self.wallet.sign_message(str(address.text()), message, password)
1665 signature.setText(sig)
1666 except Exception as e:
1667 self.show_message(str(e))
1669 def do_verify(self, address, message, signature):
1670 message = unicode(message.toPlainText())
1671 message = message.encode('utf-8')
1672 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1673 self.show_message(_("Signature verified"))
1675 self.show_message(_("Error: wrong signature"))
1678 def sign_verify_message(self, address=''):
1681 d.setWindowTitle(_('Sign/verify Message'))
1682 d.setMinimumSize(410, 290)
1684 layout = QGridLayout(d)
1686 message_e = QTextEdit()
1687 layout.addWidget(QLabel(_('Message')), 1, 0)
1688 layout.addWidget(message_e, 1, 1)
1689 layout.setRowStretch(2,3)
1691 address_e = QLineEdit()
1692 address_e.setText(address)
1693 layout.addWidget(QLabel(_('Address')), 2, 0)
1694 layout.addWidget(address_e, 2, 1)
1696 signature_e = QTextEdit()
1697 layout.addWidget(QLabel(_('Signature')), 3, 0)
1698 layout.addWidget(signature_e, 3, 1)
1699 layout.setRowStretch(3,1)
1701 hbox = QHBoxLayout()
1703 b = QPushButton(_("Sign"))
1704 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1707 b = QPushButton(_("Verify"))
1708 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1711 b = QPushButton(_("Close"))
1712 b.clicked.connect(d.accept)
1714 layout.addLayout(hbox, 4, 1)
1719 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1721 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1722 message_e.setText(decrypted)
1723 except Exception as e:
1724 self.show_message(str(e))
1727 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1728 message = unicode(message_e.toPlainText())
1729 message = message.encode('utf-8')
1731 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1732 encrypted_e.setText(encrypted)
1733 except Exception as e:
1734 self.show_message(str(e))
1738 def encrypt_message(self, address = ''):
1741 d.setWindowTitle(_('Encrypt/decrypt Message'))
1742 d.setMinimumSize(610, 490)
1744 layout = QGridLayout(d)
1746 message_e = QTextEdit()
1747 layout.addWidget(QLabel(_('Message')), 1, 0)
1748 layout.addWidget(message_e, 1, 1)
1749 layout.setRowStretch(2,3)
1751 pubkey_e = QLineEdit()
1753 pubkey = self.wallet.getpubkeys(address)[0]
1754 pubkey_e.setText(pubkey)
1755 layout.addWidget(QLabel(_('Public key')), 2, 0)
1756 layout.addWidget(pubkey_e, 2, 1)
1758 encrypted_e = QTextEdit()
1759 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1760 layout.addWidget(encrypted_e, 3, 1)
1761 layout.setRowStretch(3,1)
1763 hbox = QHBoxLayout()
1764 b = QPushButton(_("Encrypt"))
1765 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1768 b = QPushButton(_("Decrypt"))
1769 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1772 b = QPushButton(_("Close"))
1773 b.clicked.connect(d.accept)
1776 layout.addLayout(hbox, 4, 1)
1780 def question(self, msg):
1781 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1783 def show_message(self, msg):
1784 QMessageBox.information(self, _('Message'), msg, _('OK'))
1786 def password_dialog(self, msg=None):
1789 d.setWindowTitle(_("Enter Password"))
1794 vbox = QVBoxLayout()
1796 msg = _('Please enter your password')
1797 vbox.addWidget(QLabel(msg))
1799 grid = QGridLayout()
1801 grid.addWidget(QLabel(_('Password')), 1, 0)
1802 grid.addWidget(pw, 1, 1)
1803 vbox.addLayout(grid)
1805 vbox.addLayout(ok_cancel_buttons(d))
1808 run_hook('password_dialog', pw, grid, 1)
1809 if not d.exec_(): return
1810 return unicode(pw.text())
1819 def tx_from_text(self, txt):
1820 "json or raw hexadecimal"
1823 tx = Transaction(txt)
1829 tx_dict = json.loads(str(txt))
1830 assert "hex" in tx_dict.keys()
1831 tx = Transaction(tx_dict["hex"])
1832 if tx_dict.has_key("input_info"):
1833 input_info = json.loads(tx_dict['input_info'])
1834 tx.add_input_info(input_info)
1837 traceback.print_exc(file=sys.stdout)
1840 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1844 def read_tx_from_file(self):
1845 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1849 with open(fileName, "r") as f:
1850 file_content = f.read()
1851 except (ValueError, IOError, os.error), reason:
1852 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1854 return self.tx_from_text(file_content)
1858 def sign_raw_transaction(self, tx, input_info, password):
1859 self.wallet.signrawtransaction(tx, input_info, [], password)
1861 def do_process_from_text(self):
1862 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1865 tx = self.tx_from_text(text)
1867 self.show_transaction(tx)
1869 def do_process_from_file(self):
1870 tx = self.read_tx_from_file()
1872 self.show_transaction(tx)
1874 def do_process_from_txid(self):
1875 from electrum import transaction
1876 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1878 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1880 tx = transaction.Transaction(r)
1882 self.show_transaction(tx)
1884 self.show_message("unknown transaction")
1886 def do_process_from_csvReader(self, csvReader):
1891 for position, row in enumerate(csvReader):
1893 if not is_valid(address):
1894 errors.append((position, address))
1896 amount = Decimal(row[1])
1897 amount = int(100000000*amount)
1898 outputs.append((address, amount))
1899 except (ValueError, IOError, os.error), reason:
1900 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1904 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1905 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1909 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1910 except Exception as e:
1911 self.show_message(str(e))
1914 self.show_transaction(tx)
1916 def do_process_from_csv_file(self):
1917 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1921 with open(fileName, "r") as f:
1922 csvReader = csv.reader(f)
1923 self.do_process_from_csvReader(csvReader)
1924 except (ValueError, IOError, os.error), reason:
1925 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1928 def do_process_from_csv_text(self):
1929 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1930 + _("Format: address, amount. One output per line"), _("Load CSV"))
1933 f = StringIO.StringIO(text)
1934 csvReader = csv.reader(f)
1935 self.do_process_from_csvReader(csvReader)
1940 def export_privkeys_dialog(self, password):
1941 if self.wallet.is_watching_only():
1942 self.show_message(_("This is a watching-only wallet"))
1946 d.setWindowTitle(_('Private keys'))
1947 d.setMinimumSize(850, 300)
1948 vbox = QVBoxLayout(d)
1950 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
1951 _("Exposing a single private key can compromise your entire wallet!"),
1952 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1953 vbox.addWidget(QLabel(msg))
1959 defaultname = 'electrum-private-keys.csv'
1960 select_msg = _('Select file to export your private keys to')
1961 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1962 vbox.addLayout(hbox)
1964 h, b = ok_cancel_buttons2(d, _('Export'))
1969 addresses = self.wallet.addresses(True)
1971 def privkeys_thread():
1972 for addr in addresses:
1976 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1977 d.emit(SIGNAL('computing_privkeys'))
1978 d.emit(SIGNAL('show_privkeys'))
1980 def show_privkeys():
1981 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
1985 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
1986 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
1987 threading.Thread(target=privkeys_thread).start()
1993 filename = filename_e.text()
1998 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
1999 except (IOError, os.error), reason:
2000 export_error_label = _("Electrum was unable to produce a private key-export.")
2001 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2003 except Exception as e:
2004 self.show_message(str(e))
2007 self.show_message(_("Private keys exported."))
2010 def do_export_privkeys(self, fileName, pklist, is_csv):
2011 with open(fileName, "w+") as f:
2013 transaction = csv.writer(f)
2014 transaction.writerow(["address", "private_key"])
2015 for addr, pk in pklist.items():
2016 transaction.writerow(["%34s"%addr,pk])
2019 f.write(json.dumps(pklist, indent = 4))
2022 def do_import_labels(self):
2023 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2024 if not labelsFile: return
2026 f = open(labelsFile, 'r')
2029 for key, value in json.loads(data).items():
2030 self.wallet.set_label(key, value)
2031 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2032 except (IOError, os.error), reason:
2033 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2036 def do_export_labels(self):
2037 labels = self.wallet.labels
2039 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2041 with open(fileName, 'w+') as f:
2042 json.dump(labels, f)
2043 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2044 except (IOError, os.error), reason:
2045 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2048 def export_history_dialog(self):
2051 d.setWindowTitle(_('Export History'))
2052 d.setMinimumSize(400, 200)
2053 vbox = QVBoxLayout(d)
2055 defaultname = os.path.expanduser('~/electrum-history.csv')
2056 select_msg = _('Select file to export your wallet transactions to')
2058 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2059 vbox.addLayout(hbox)
2063 h, b = ok_cancel_buttons2(d, _('Export'))
2068 filename = filename_e.text()
2073 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2074 except (IOError, os.error), reason:
2075 export_error_label = _("Electrum was unable to produce a transaction export.")
2076 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2079 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2082 def do_export_history(self, wallet, fileName, is_csv):
2083 history = wallet.get_tx_history()
2085 for item in history:
2086 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2088 if timestamp is not None:
2090 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2091 except [RuntimeError, TypeError, NameError] as reason:
2092 time_string = "unknown"
2095 time_string = "unknown"
2097 time_string = "pending"
2099 if value is not None:
2100 value_string = format_satoshis(value, True)
2105 fee_string = format_satoshis(fee, True)
2110 label, is_default_label = wallet.get_label(tx_hash)
2111 label = label.encode('utf-8')
2115 balance_string = format_satoshis(balance, False)
2117 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2119 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2121 with open(fileName, "w+") as f:
2123 transaction = csv.writer(f)
2124 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2126 transaction.writerow(line)
2129 f.write(json.dumps(lines, indent = 4))
2132 def sweep_key_dialog(self):
2134 d.setWindowTitle(_('Sweep private keys'))
2135 d.setMinimumSize(600, 300)
2137 vbox = QVBoxLayout(d)
2138 vbox.addWidget(QLabel(_("Enter private keys")))
2140 keys_e = QTextEdit()
2141 keys_e.setTabChangesFocus(True)
2142 vbox.addWidget(keys_e)
2144 h, address_e = address_field(self.wallet.addresses())
2148 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2149 vbox.addLayout(hbox)
2150 button.setEnabled(False)
2153 addr = str(address_e.text())
2154 if bitcoin.is_address(addr):
2158 pk = str(keys_e.toPlainText()).strip()
2159 if Wallet.is_private_key(pk):
2162 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2163 keys_e.textChanged.connect(f)
2164 address_e.textChanged.connect(f)
2168 fee = self.wallet.fee
2169 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2170 self.show_transaction(tx)
2174 def do_import_privkey(self, password):
2175 if not self.wallet.has_imported_keys():
2176 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2177 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2178 + _('Are you sure you understand what you are doing?'), 3, 4)
2181 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2184 text = str(text).split()
2189 addr = self.wallet.import_key(key, password)
2190 except Exception as e:
2196 addrlist.append(addr)
2198 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2200 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2201 self.update_receive_tab()
2202 self.update_history_tab()
2205 def settings_dialog(self):
2207 d.setWindowTitle(_('Electrum Settings'))
2209 vbox = QVBoxLayout()
2210 grid = QGridLayout()
2211 grid.setColumnStretch(0,1)
2213 nz_label = QLabel(_('Display zeros') + ':')
2214 grid.addWidget(nz_label, 0, 0)
2215 nz_e = AmountEdit(None,True)
2216 nz_e.setText("%d"% self.num_zeros)
2217 grid.addWidget(nz_e, 0, 1)
2218 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2219 grid.addWidget(HelpButton(msg), 0, 2)
2220 if not self.config.is_modifiable('num_zeros'):
2221 for w in [nz_e, nz_label]: w.setEnabled(False)
2223 lang_label=QLabel(_('Language') + ':')
2224 grid.addWidget(lang_label, 1, 0)
2225 lang_combo = QComboBox()
2226 from electrum.i18n import languages
2227 lang_combo.addItems(languages.values())
2229 index = languages.keys().index(self.config.get("language",''))
2232 lang_combo.setCurrentIndex(index)
2233 grid.addWidget(lang_combo, 1, 1)
2234 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2235 if not self.config.is_modifiable('language'):
2236 for w in [lang_combo, lang_label]: w.setEnabled(False)
2239 fee_label = QLabel(_('Transaction fee') + ':')
2240 grid.addWidget(fee_label, 2, 0)
2241 fee_e = AmountEdit(self.get_decimal_point)
2242 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2243 grid.addWidget(fee_e, 2, 1)
2244 msg = _('Fee per kilobyte of transaction.') + ' ' \
2245 + _('Recommended value') + ': ' + self.format_amount(20000)
2246 grid.addWidget(HelpButton(msg), 2, 2)
2247 if not self.config.is_modifiable('fee_per_kb'):
2248 for w in [fee_e, fee_label]: w.setEnabled(False)
2250 units = ['BTC', 'mBTC']
2251 unit_label = QLabel(_('Base unit') + ':')
2252 grid.addWidget(unit_label, 3, 0)
2253 unit_combo = QComboBox()
2254 unit_combo.addItems(units)
2255 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2256 grid.addWidget(unit_combo, 3, 1)
2257 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2258 + '\n1BTC=1000mBTC.\n' \
2259 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2261 usechange_cb = QCheckBox(_('Use change addresses'))
2262 usechange_cb.setChecked(self.wallet.use_change)
2263 grid.addWidget(usechange_cb, 4, 0)
2264 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2265 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2267 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2268 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2269 grid.addWidget(block_ex_label, 5, 0)
2270 block_ex_combo = QComboBox()
2271 block_ex_combo.addItems(block_explorers)
2272 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2273 grid.addWidget(block_ex_combo, 5, 1)
2274 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2276 show_tx = self.config.get('show_before_broadcast', False)
2277 showtx_cb = QCheckBox(_('Show before broadcast'))
2278 showtx_cb.setChecked(show_tx)
2279 grid.addWidget(showtx_cb, 6, 0)
2280 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2282 vbox.addLayout(grid)
2284 vbox.addLayout(ok_cancel_buttons(d))
2288 if not d.exec_(): return
2291 fee = self.fee_e.get_amount()
2293 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2296 self.wallet.set_fee(fee)
2298 nz = unicode(nz_e.text())
2303 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2306 if self.num_zeros != nz:
2308 self.config.set_key('num_zeros', nz, True)
2309 self.update_history_tab()
2310 self.update_receive_tab()
2312 usechange_result = usechange_cb.isChecked()
2313 if self.wallet.use_change != usechange_result:
2314 self.wallet.use_change = usechange_result
2315 self.wallet.storage.put('use_change', self.wallet.use_change)
2317 if showtx_cb.isChecked() != show_tx:
2318 self.config.set_key('show_before_broadcast', not show_tx)
2320 unit_result = units[unit_combo.currentIndex()]
2321 if self.base_unit() != unit_result:
2322 self.decimal_point = 8 if unit_result == 'BTC' else 5
2323 self.config.set_key('decimal_point', self.decimal_point, True)
2324 self.update_history_tab()
2325 self.update_status()
2327 need_restart = False
2329 lang_request = languages.keys()[lang_combo.currentIndex()]
2330 if lang_request != self.config.get('language'):
2331 self.config.set_key("language", lang_request, True)
2334 be_result = block_explorers[block_ex_combo.currentIndex()]
2335 self.config.set_key('block_explorer', be_result, True)
2337 run_hook('close_settings_dialog')
2340 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2343 def run_network_dialog(self):
2344 if not self.network:
2346 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2348 def closeEvent(self, event):
2350 self.config.set_key("is_maximized", self.isMaximized())
2351 if not self.isMaximized():
2353 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2354 self.save_column_widths()
2355 self.config.set_key("console-history", self.console.history[-50:], True)
2356 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2360 def plugins_dialog(self):
2361 from electrum.plugins import plugins
2364 d.setWindowTitle(_('Electrum Plugins'))
2367 vbox = QVBoxLayout(d)
2370 scroll = QScrollArea()
2371 scroll.setEnabled(True)
2372 scroll.setWidgetResizable(True)
2373 scroll.setMinimumSize(400,250)
2374 vbox.addWidget(scroll)
2378 w.setMinimumHeight(len(plugins)*35)
2380 grid = QGridLayout()
2381 grid.setColumnStretch(0,1)
2384 def do_toggle(cb, p, w):
2387 if w: w.setEnabled(r)
2389 def mk_toggle(cb, p, w):
2390 return lambda: do_toggle(cb,p,w)
2392 for i, p in enumerate(plugins):
2394 cb = QCheckBox(p.fullname())
2395 cb.setDisabled(not p.is_available())
2396 cb.setChecked(p.is_enabled())
2397 grid.addWidget(cb, i, 0)
2398 if p.requires_settings():
2399 w = p.settings_widget(self)
2400 w.setEnabled( p.is_enabled() )
2401 grid.addWidget(w, i, 1)
2404 cb.clicked.connect(mk_toggle(cb,p,w))
2405 grid.addWidget(HelpButton(p.description()), i, 2)
2407 print_msg(_("Error: cannot display plugin"), p)
2408 traceback.print_exc(file=sys.stdout)
2409 grid.setRowStretch(i+1,1)
2411 vbox.addLayout(close_button(d))
2416 def show_account_details(self, k):
2417 account = self.wallet.accounts[k]
2420 d.setWindowTitle(_('Account Details'))
2423 vbox = QVBoxLayout(d)
2424 name = self.wallet.get_account_name(k)
2425 label = QLabel('Name: ' + name)
2426 vbox.addWidget(label)
2428 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2430 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2432 vbox.addWidget(QLabel(_('Master Public Key:')))
2435 text.setReadOnly(True)
2436 text.setMaximumHeight(170)
2437 vbox.addWidget(text)
2439 mpk_text = '\n'.join( account.get_master_pubkeys() )
2440 text.setText(mpk_text)
2442 vbox.addLayout(close_button(d))