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.util import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
44 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
45 from network_dialog import NetworkDialog
46 from qrcodewidget import QRCodeWidget, QRDialog
47 from qrtextedit import QRTextEdit
49 from decimal import Decimal
57 if platform.system() == 'Windows':
58 MONOSPACE_FONT = 'Lucida Console'
59 elif platform.system() == 'Darwin':
60 MONOSPACE_FONT = 'Monaco'
62 MONOSPACE_FONT = 'monospace'
66 # status of payment requests
69 PR_SENT = 2 # sent but not propagated
70 PR_PAID = 3 # send and propagated
71 PR_ERROR = 4 # could not parse
74 from electrum import ELECTRUM_VERSION
89 class StatusBarButton(QPushButton):
90 def __init__(self, icon, tooltip, func):
91 QPushButton.__init__(self, icon, '')
92 self.setToolTip(tooltip)
94 self.setMaximumWidth(25)
95 self.clicked.connect(func)
97 self.setIconSize(QSize(25,25))
99 def keyPressEvent(self, e):
100 if e.key() == QtCore.Qt.Key_Return:
112 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
114 class ElectrumWindow(QMainWindow):
118 def __init__(self, config, network, gui_object):
119 QMainWindow.__init__(self)
122 self.network = network
123 self.gui_object = gui_object
124 self.tray = gui_object.tray
125 self.go_lite = gui_object.go_lite
128 self.create_status_bar()
129 self.need_update = threading.Event()
131 self.decimal_point = config.get('decimal_point', 5)
132 self.num_zeros = int(config.get('num_zeros',0))
135 set_language(config.get('language'))
137 self.completions = QStringListModel()
139 self.tabs = tabs = QTabWidget(self)
140 self.column_widths = self.config.get("column_widths_2", default_column_widths )
141 tabs.addTab(self.create_history_tab(), _('History') )
142 tabs.addTab(self.create_send_tab(), _('Send') )
143 tabs.addTab(self.create_receive_tab(), _('Receive') )
144 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
145 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
146 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
147 tabs.addTab(self.create_console_tab(), _('Console') )
148 tabs.setMinimumSize(600, 400)
149 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
150 self.setCentralWidget(tabs)
152 g = self.config.get("winpos-qt",[100, 100, 840, 400])
153 self.setGeometry(g[0], g[1], g[2], g[3])
154 if self.config.get("is_maximized"):
157 self.setWindowIcon(QIcon(":icons/electrum.png"))
160 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
161 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
163 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
164 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
166 for i in range(tabs.count()):
167 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
169 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
170 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
171 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
172 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
173 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
175 self.history_list.setFocus(True)
179 self.network.register_callback('updated', lambda: self.need_update.set())
180 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
181 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
182 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
185 # set initial message
186 self.console.showMessage(self.network.banner)
189 self.payment_request = None
191 def update_account_selector(self):
193 accounts = self.wallet.get_account_names()
194 self.account_selector.clear()
195 if len(accounts) > 1:
196 self.account_selector.addItems([_("All accounts")] + accounts.values())
197 self.account_selector.setCurrentIndex(0)
198 self.account_selector.show()
200 self.account_selector.hide()
203 def load_wallet(self, wallet):
207 self.update_wallet_format()
209 self.invoices = self.wallet.storage.get('invoices', {})
210 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
211 self.current_account = self.wallet.storage.get("current_account", None)
212 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
213 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
214 self.setWindowTitle( title )
216 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
217 self.notify_transactions()
218 self.update_account_selector()
220 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
221 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
222 self.password_menu.setEnabled(not self.wallet.is_watching_only())
223 self.seed_menu.setEnabled(self.wallet.has_seed())
224 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
225 self.import_menu.setEnabled(self.wallet.can_import())
227 self.update_lock_icon()
228 self.update_buttons_on_seed()
229 self.update_console()
231 self.clear_receive_tab()
232 self.update_receive_tab()
233 run_hook('load_wallet', wallet)
236 def update_wallet_format(self):
237 # convert old-format imported keys
238 if self.wallet.imported_keys:
239 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
241 self.wallet.convert_imported_keys(password)
243 self.show_message("error")
246 def open_wallet(self):
247 wallet_folder = self.wallet.storage.path
248 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
252 storage = WalletStorage({'wallet_path': filename})
253 if not storage.file_exists:
254 self.show_message("file not found "+ filename)
257 self.wallet.stop_threads()
260 wallet = Wallet(storage)
261 wallet.start_threads(self.network)
263 self.load_wallet(wallet)
267 def backup_wallet(self):
269 path = self.wallet.storage.path
270 wallet_folder = os.path.dirname(path)
271 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
275 new_path = os.path.join(wallet_folder, filename)
278 shutil.copy2(path, new_path)
279 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
280 except (IOError, os.error), reason:
281 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
284 def new_wallet(self):
287 wallet_folder = os.path.dirname(self.wallet.storage.path)
288 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
291 filename = os.path.join(wallet_folder, filename)
293 storage = WalletStorage({'wallet_path': filename})
294 if storage.file_exists:
295 QMessageBox.critical(None, "Error", _("File exists"))
298 wizard = installwizard.InstallWizard(self.config, self.network, storage)
299 wallet = wizard.run('new')
301 self.load_wallet(wallet)
305 def init_menubar(self):
308 file_menu = menubar.addMenu(_("&File"))
309 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
310 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
311 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
312 file_menu.addAction(_("&Quit"), self.close)
314 wallet_menu = menubar.addMenu(_("&Wallet"))
315 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
316 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
318 wallet_menu.addSeparator()
320 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
321 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
322 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
324 wallet_menu.addSeparator()
325 labels_menu = wallet_menu.addMenu(_("&Labels"))
326 labels_menu.addAction(_("&Import"), self.do_import_labels)
327 labels_menu.addAction(_("&Export"), self.do_export_labels)
329 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
330 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
331 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
332 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
333 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
335 tools_menu = menubar.addMenu(_("&Tools"))
337 # Settings / Preferences are all reserved keywords in OSX using this as work around
338 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
339 tools_menu.addAction(_("&Network"), self.run_network_dialog)
340 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
341 tools_menu.addSeparator()
342 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
343 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
344 tools_menu.addSeparator()
346 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
347 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
348 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
350 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
351 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
352 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
353 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
354 self.raw_transaction_menu = raw_transaction_menu
356 help_menu = menubar.addMenu(_("&Help"))
357 help_menu.addAction(_("&About"), self.show_about)
358 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
359 help_menu.addSeparator()
360 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
361 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
363 self.setMenuBar(menubar)
365 def show_about(self):
366 QMessageBox.about(self, "Electrum",
367 _("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."))
369 def show_report_bug(self):
370 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
371 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
374 def notify_transactions(self):
375 if not self.network or not self.network.is_connected():
378 print_error("Notifying GUI")
379 if len(self.network.pending_transactions_for_notifications) > 0:
380 # Combine the transactions if there are more then three
381 tx_amount = len(self.network.pending_transactions_for_notifications)
384 for tx in self.network.pending_transactions_for_notifications:
385 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
389 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
390 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
392 self.network.pending_transactions_for_notifications = []
394 for tx in self.network.pending_transactions_for_notifications:
396 self.network.pending_transactions_for_notifications.remove(tx)
397 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
399 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
401 def notify(self, message):
402 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
406 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
407 def getOpenFileName(self, title, filter = ""):
408 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
409 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
410 if fileName and directory != os.path.dirname(fileName):
411 self.config.set_key('io_dir', os.path.dirname(fileName), True)
414 def getSaveFileName(self, title, filename, filter = ""):
415 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
416 path = os.path.join( directory, filename )
417 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
418 if fileName and directory != os.path.dirname(fileName):
419 self.config.set_key('io_dir', os.path.dirname(fileName), True)
423 QMainWindow.close(self)
424 run_hook('close_main_window')
426 def connect_slots(self, sender):
427 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
428 self.previous_payto_e=''
430 def timer_actions(self):
431 if self.need_update.is_set():
433 self.need_update.clear()
435 run_hook('timer_actions')
437 def format_amount(self, x, is_diff=False, whitespaces=False):
438 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
441 def get_decimal_point(self):
442 return self.decimal_point
446 assert self.decimal_point in [5,8]
447 return "BTC" if self.decimal_point == 8 else "mBTC"
450 def update_status(self):
451 if self.network is None or not self.network.is_running():
453 icon = QIcon(":icons/status_disconnected.png")
455 elif self.network.is_connected():
456 if not self.wallet.up_to_date:
457 text = _("Synchronizing...")
458 icon = QIcon(":icons/status_waiting.png")
459 elif self.network.server_lag > 1:
460 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
461 icon = QIcon(":icons/status_lagging.png")
463 c, u = self.wallet.get_account_balance(self.current_account)
464 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
465 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
467 # append fiat balance and price from exchange rate plugin
469 run_hook('get_fiat_status_text', c+u, r)
474 self.tray.setToolTip(text)
475 icon = QIcon(":icons/status_connected.png")
477 text = _("Not connected")
478 icon = QIcon(":icons/status_disconnected.png")
480 self.balance_label.setText(text)
481 self.status_button.setIcon( icon )
484 def update_wallet(self):
486 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
487 self.update_history_tab()
488 self.update_receive_tab()
489 self.update_address_tab()
490 self.update_contacts_tab()
491 self.update_completions()
492 self.update_invoices_tab()
495 def create_history_tab(self):
496 self.history_list = l = MyTreeWidget(self)
498 for i,width in enumerate(self.column_widths['history']):
499 l.setColumnWidth(i, width)
500 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
501 l.itemDoubleClicked.connect(self.tx_label_clicked)
502 l.itemChanged.connect(self.tx_label_changed)
503 l.customContextMenuRequested.connect(self.create_history_menu)
507 def create_history_menu(self, position):
508 self.history_list.selectedIndexes()
509 item = self.history_list.currentItem()
510 be = self.config.get('block_explorer', 'Blockchain.info')
511 if be == 'Blockchain.info':
512 block_explorer = 'https://blockchain.info/tx/'
513 elif be == 'Blockr.io':
514 block_explorer = 'https://blockr.io/tx/info/'
515 elif be == 'Insight.is':
516 block_explorer = 'http://live.insight.is/tx/'
518 tx_hash = str(item.data(0, Qt.UserRole).toString())
519 if not tx_hash: return
521 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
522 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
523 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
524 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
525 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
528 def show_transaction(self, tx):
529 import transaction_dialog
530 d = transaction_dialog.TxDialog(tx, self)
533 def tx_label_clicked(self, item, column):
534 if column==2 and item.isSelected():
536 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
537 self.history_list.editItem( item, column )
538 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
541 def tx_label_changed(self, item, column):
545 tx_hash = str(item.data(0, Qt.UserRole).toString())
546 tx = self.wallet.transactions.get(tx_hash)
547 text = unicode( item.text(2) )
548 self.wallet.set_label(tx_hash, text)
550 item.setForeground(2, QBrush(QColor('black')))
552 text = self.wallet.get_default_label(tx_hash)
553 item.setText(2, text)
554 item.setForeground(2, QBrush(QColor('gray')))
558 def edit_label(self, is_recv):
559 l = self.address_list if is_recv else self.contacts_list
560 item = l.currentItem()
561 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
562 l.editItem( item, 1 )
563 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 def address_label_clicked(self, item, column, l, column_addr, column_label):
568 if column == column_label and item.isSelected():
569 is_editable = item.data(0, 32).toBool()
572 addr = unicode( item.text(column_addr) )
573 label = unicode( item.text(column_label) )
574 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
575 l.editItem( item, column )
576 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
579 def address_label_changed(self, item, column, l, column_addr, column_label):
580 if column == column_label:
581 addr = unicode( item.text(column_addr) )
582 text = unicode( item.text(column_label) )
583 is_editable = item.data(0, 32).toBool()
587 changed = self.wallet.set_label(addr, text)
589 self.update_history_tab()
590 self.update_completions()
592 self.current_item_changed(item)
594 run_hook('item_changed', item, column)
597 def current_item_changed(self, a):
598 run_hook('current_item_changed', a)
602 def update_history_tab(self):
604 self.history_list.clear()
605 for item in self.wallet.get_tx_history(self.current_account):
606 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
607 time_str = _("unknown")
610 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
612 time_str = _("error")
615 time_str = 'unverified'
616 icon = QIcon(":icons/unconfirmed.png")
619 icon = QIcon(":icons/unconfirmed.png")
621 icon = QIcon(":icons/clock%d.png"%conf)
623 icon = QIcon(":icons/confirmed.png")
625 if value is not None:
626 v_str = self.format_amount(value, True, whitespaces=True)
630 balance_str = self.format_amount(balance, whitespaces=True)
633 label, is_default_label = self.wallet.get_label(tx_hash)
635 label = _('Pruned transaction outputs')
636 is_default_label = False
638 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
639 item.setFont(2, QFont(MONOSPACE_FONT))
640 item.setFont(3, QFont(MONOSPACE_FONT))
641 item.setFont(4, QFont(MONOSPACE_FONT))
643 item.setForeground(3, QBrush(QColor("#BC1E1E")))
645 item.setData(0, Qt.UserRole, tx_hash)
646 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
648 item.setForeground(2, QBrush(QColor('grey')))
650 item.setIcon(0, icon)
651 self.history_list.insertTopLevelItem(0,item)
654 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
655 run_hook('history_tab_update')
658 def create_receive_tab(self):
660 grid = QGridLayout(w)
661 grid.setColumnMinimumWidth(3, 300)
662 grid.setColumnStretch(5, 1)
664 self.receive_address_e = QLineEdit()
665 self.receive_address_e.setReadOnly(True)
666 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
667 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
668 self.receive_address_e.textChanged.connect(self.update_receive_qr)
670 self.receive_message_e = QLineEdit()
671 grid.addWidget(QLabel(_('Message')), 1, 0)
672 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
673 self.receive_message_e.textChanged.connect(self.update_receive_qr)
675 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
676 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
677 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
678 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
680 self.save_request_button = QPushButton(_('Save'))
681 self.save_request_button.clicked.connect(self.save_payment_request)
682 grid.addWidget(self.save_request_button, 3, 1)
683 clear_button = QPushButton(_('New'))
684 clear_button.clicked.connect(self.clear_receive_tab)
685 grid.addWidget(clear_button, 3, 2)
686 grid.setRowStretch(4, 1)
688 self.receive_qr = QRCodeWidget(fixedSize=200)
689 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
691 grid.setRowStretch(5, 1)
693 self.receive_requests_label = QLabel(_('Saved Requests'))
694 self.receive_list = MyTreeWidget(self)
695 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
696 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
697 self.receive_list.itemClicked.connect(self.receive_item_changed)
698 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
699 self.receive_list.setColumnWidth(0, 340)
700 h = self.receive_list.header()
701 h.setStretchLastSection(False)
702 h.setResizeMode(1, QHeaderView.Stretch)
704 grid.addWidget(self.receive_requests_label, 6, 0)
705 grid.addWidget(self.receive_list, 7, 0, 1, 6)
708 def receive_item_changed(self, item):
711 addr = str(item.text(0))
712 amount, message = self.receive_requests[addr]
713 self.receive_address_e.setText(addr)
714 self.receive_message_e.setText(message)
715 self.receive_amount_e.setAmount(amount)
718 def receive_list_delete(self, item):
719 addr = str(item.text(0))
720 self.receive_requests.pop(addr)
721 self.update_receive_tab()
722 self.clear_receive_tab()
724 def receive_list_menu(self, position):
725 item = self.receive_list.itemAt(position)
727 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
728 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
730 def save_payment_request(self):
731 addr = str(self.receive_address_e.text())
732 amount = self.receive_amount_e.get_amount()
733 message = str(self.receive_message_e.text())
734 if not message and not amount:
735 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
737 self.receive_requests = self.wallet.storage.get('receive_requests',{})
738 self.receive_requests[addr] = (amount, message)
739 self.wallet.storage.put('receive_requests', self.receive_requests)
740 self.update_receive_tab()
742 def clear_receive_tab(self):
743 self.receive_requests = self.wallet.storage.get('receive_requests',{})
744 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
746 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
750 self.receive_address_e.setText(addr)
751 self.receive_message_e.setText('')
752 self.receive_amount_e.setAmount(None)
754 def receive_at(self, addr):
755 if not bitcoin.is_address(addr):
757 self.tabs.setCurrentIndex(2)
758 self.receive_address_e.setText(addr)
760 def update_receive_tab(self):
761 self.receive_requests = self.wallet.storage.get('receive_requests',{})
762 b = len(self.receive_requests) > 0
763 self.receive_list.setVisible(b)
764 self.receive_requests_label.setVisible(b)
766 self.receive_list.clear()
767 for address, v in self.receive_requests.items():
769 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
770 item.setFont(0, QFont(MONOSPACE_FONT))
771 self.receive_list.addTopLevelItem(item)
774 def update_receive_qr(self):
775 import urlparse, urllib
776 addr = str(self.receive_address_e.text())
777 amount = self.receive_amount_e.get_amount()
778 message = unicode(self.receive_message_e.text()).encode('utf8')
779 self.save_request_button.setEnabled((amount is not None) or (message != ""))
783 query.append('amount=%s'%format_satoshis(amount))
785 query.append('message=%s'%urllib.quote(message))
786 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
787 url = urlparse.urlunparse(p)
790 self.receive_qr.setData(url)
791 run_hook('update_receive_qr', addr, amount, message, url)
794 def create_send_tab(self):
797 self.send_grid = grid = QGridLayout(w)
799 grid.setColumnMinimumWidth(3,300)
800 grid.setColumnStretch(5,1)
801 grid.setRowStretch(8, 1)
803 from paytoedit import PayToEdit
804 self.amount_e = BTCAmountEdit(self.get_decimal_point)
805 self.payto_e = PayToEdit(self)
806 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)'))
807 grid.addWidget(QLabel(_('Pay to')), 1, 0)
808 grid.addWidget(self.payto_e, 1, 1, 1, 3)
809 grid.addWidget(self.payto_help, 1, 4)
811 completer = QCompleter()
812 completer.setCaseSensitivity(False)
813 self.payto_e.setCompleter(completer)
814 completer.setModel(self.completions)
816 self.message_e = MyLineEdit()
817 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.'))
818 grid.addWidget(QLabel(_('Description')), 2, 0)
819 grid.addWidget(self.message_e, 2, 1, 1, 3)
820 grid.addWidget(self.message_help, 2, 4)
822 self.from_label = QLabel(_('From'))
823 grid.addWidget(self.from_label, 3, 0)
824 self.from_list = MyTreeWidget(self)
825 self.from_list.setColumnCount(2)
826 self.from_list.setColumnWidth(0, 350)
827 self.from_list.setColumnWidth(1, 50)
828 self.from_list.setHeaderHidden(True)
829 self.from_list.setMaximumHeight(80)
830 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
831 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
832 grid.addWidget(self.from_list, 3, 1, 1, 3)
833 self.set_pay_from([])
835 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
836 + _('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.') \
837 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
838 grid.addWidget(QLabel(_('Amount')), 4, 0)
839 grid.addWidget(self.amount_e, 4, 1, 1, 2)
840 grid.addWidget(self.amount_help, 4, 3)
842 self.fee_e = BTCAmountEdit(self.get_decimal_point)
843 grid.addWidget(QLabel(_('Fee')), 5, 0)
844 grid.addWidget(self.fee_e, 5, 1, 1, 2)
845 grid.addWidget(HelpButton(
846 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
847 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
848 + _('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)
850 self.send_button = EnterButton(_("Send"), self.do_send)
851 grid.addWidget(self.send_button, 6, 1)
853 b = EnterButton(_("Clear"), self.do_clear)
854 grid.addWidget(b, 6, 2)
856 self.payto_sig = QLabel('')
857 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
859 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
860 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
863 def entry_changed( is_fee ):
865 if self.amount_e.is_shortcut:
866 self.amount_e.is_shortcut = False
867 sendable = self.get_sendable_balance()
868 # there is only one output because we are completely spending inputs
869 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
870 fee = self.wallet.estimated_fee(inputs, 1)
872 self.amount_e.setAmount(amount)
873 self.amount_e.textEdited.emit("")
874 self.fee_e.setAmount(fee)
877 amount = self.amount_e.get_amount()
878 fee = self.fee_e.get_amount()
880 if not is_fee: fee = None
882 self.fee_e.setAmount(None)
884 # assume that there will be 2 outputs (one for change)
885 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
887 self.fee_e.setAmount(fee)
890 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
894 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
895 text = _( "Not enough funds" )
896 c, u = self.wallet.get_frozen_balance()
897 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
899 self.statusBar().showMessage(text)
900 self.amount_e.setPalette(palette)
901 self.fee_e.setPalette(palette)
903 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
904 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
906 run_hook('create_send_tab', grid)
909 def from_list_delete(self, item):
910 i = self.from_list.indexOfTopLevelItem(item)
912 self.redraw_from_list()
914 def from_list_menu(self, position):
915 item = self.from_list.itemAt(position)
917 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
918 menu.exec_(self.from_list.viewport().mapToGlobal(position))
920 def set_pay_from(self, domain = None):
921 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
922 self.redraw_from_list()
924 def redraw_from_list(self):
925 self.from_list.clear()
926 self.from_label.setHidden(len(self.pay_from) == 0)
927 self.from_list.setHidden(len(self.pay_from) == 0)
930 h = x.get('prevout_hash')
931 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
933 for item in self.pay_from:
934 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
936 def update_completions(self):
938 for addr,label in self.wallet.labels.items():
939 if addr in self.wallet.addressbook:
940 l.append( label + ' <' + addr + '>')
942 run_hook('update_completions', l)
943 self.completions.setStringList(l)
947 return lambda s, *args: s.do_protect(func, args)
950 def read_send_tab(self):
952 if self.payment_request and self.payment_request.has_expired():
953 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
956 label = unicode( self.message_e.text() )
958 if self.payment_request:
959 outputs = self.payment_request.get_outputs()
961 outputs = self.payto_e.get_outputs()
964 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
967 for addr, x in outputs:
968 if addr is None or not bitcoin.is_address(addr):
969 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
972 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
975 amount = sum(map(lambda x:x[1], outputs))
977 fee = self.fee_e.get_amount()
979 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
982 confirm_amount = self.config.get('confirm_amount', 100000000)
983 if amount >= confirm_amount:
984 o = '\n'.join(map(lambda x:x[0], outputs))
985 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
988 confirm_fee = self.config.get('confirm_fee', 100000)
989 if fee >= confirm_fee:
990 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()}):
993 coins = self.get_coins()
994 return outputs, fee, label, coins
998 r = self.read_send_tab()
1001 outputs, fee, label, coins = r
1002 self.send_tx(outputs, fee, label, coins)
1006 def send_tx(self, outputs, fee, label, coins, password):
1007 self.send_button.setDisabled(True)
1009 # first, create an unsigned tx
1011 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1013 except Exception as e:
1014 traceback.print_exc(file=sys.stdout)
1015 self.show_message(str(e))
1016 self.send_button.setDisabled(False)
1019 # call hook to see if plugin needs gui interaction
1020 run_hook('send_tx', tx)
1026 self.wallet.add_keypairs(tx, keypairs, password)
1027 self.wallet.sign_transaction(tx, keypairs, password)
1028 return tx, fee, label
1030 def sign_done(tx, fee, label):
1032 self.show_message(tx.error)
1033 self.send_button.setDisabled(False)
1035 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1036 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1037 self.send_button.setDisabled(False)
1040 self.wallet.set_label(tx.hash(), label)
1042 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1043 self.show_transaction(tx)
1045 self.send_button.setDisabled(False)
1048 self.broadcast_transaction(tx)
1050 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1051 self.waiting_dialog.start()
1055 def broadcast_transaction(self, tx):
1057 def broadcast_thread():
1058 pr = self.payment_request
1060 return self.wallet.sendtx(tx)
1062 if pr.has_expired():
1063 self.payment_request = None
1064 return False, _("Payment request has expired")
1066 status, msg = self.wallet.sendtx(tx)
1070 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1071 self.wallet.storage.put('invoices', self.invoices)
1072 self.update_invoices_tab()
1073 self.payment_request = None
1074 refund_address = self.wallet.addresses()[0]
1075 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1081 def broadcast_done(status, msg):
1083 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1086 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1087 self.send_button.setDisabled(False)
1089 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1090 self.waiting_dialog.start()
1094 def prepare_for_payment_request(self):
1095 self.tabs.setCurrentIndex(1)
1096 self.payto_e.is_pr = True
1097 for e in [self.payto_e, self.amount_e, self.message_e]:
1099 for h in [self.payto_help, self.amount_help, self.message_help]:
1101 self.payto_e.setText(_("please wait..."))
1104 def payment_request_ok(self):
1105 pr = self.payment_request
1107 if pr_id not in self.invoices:
1108 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1109 self.wallet.storage.put('invoices', self.invoices)
1110 self.update_invoices_tab()
1112 print_error('invoice already in list')
1114 status = self.invoices[pr_id][4]
1115 if status == PR_PAID:
1117 self.show_message("invoice already paid")
1118 self.payment_request = None
1121 self.payto_help.show()
1122 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1124 if not pr.has_expired():
1125 self.payto_e.setGreen()
1127 self.payto_e.setExpired()
1129 self.payto_e.setText(pr.domain)
1130 self.amount_e.setText(self.format_amount(pr.get_amount()))
1131 self.message_e.setText(pr.get_memo())
1133 def payment_request_error(self):
1135 self.show_message(self.payment_request.error)
1136 self.payment_request = None
1138 def pay_from_URI(self,URI):
1141 address, amount, label, message, request_url = util.parse_URI(URI)
1143 address, amount, label, message, request_url = util.parse_URI(URI)
1144 except Exception as e:
1145 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1148 self.tabs.setCurrentIndex(1)
1152 if self.wallet.labels.get(address) != label:
1153 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1154 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1155 self.wallet.addressbook.append(address)
1156 self.wallet.set_label(address, label)
1158 label = self.wallet.labels.get(address)
1160 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1162 self.message_e.setText(message)
1164 self.amount_e.setAmount(amount)
1167 from electrum import paymentrequest
1168 def payment_request():
1169 self.payment_request = paymentrequest.PaymentRequest(self.config)
1170 self.payment_request.read(request_url)
1171 if self.payment_request.verify():
1172 self.emit(SIGNAL('payment_request_ok'))
1174 self.emit(SIGNAL('payment_request_error'))
1176 self.pr_thread = threading.Thread(target=payment_request).start()
1177 self.prepare_for_payment_request()
1182 self.payto_e.is_pr = False
1183 self.payto_sig.setVisible(False)
1184 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1188 for h in [self.payto_help, self.amount_help, self.message_help]:
1191 self.payto_help.set_alt(None)
1192 self.set_pay_from([])
1193 self.update_status()
1197 def set_addrs_frozen(self,addrs,freeze):
1199 if not addr: continue
1200 if addr in self.wallet.frozen_addresses and not freeze:
1201 self.wallet.unfreeze(addr)
1202 elif addr not in self.wallet.frozen_addresses and freeze:
1203 self.wallet.freeze(addr)
1204 self.update_address_tab()
1208 def create_list_tab(self, headers):
1209 "generic tab creation method"
1210 l = MyTreeWidget(self)
1211 l.setColumnCount( len(headers) )
1212 l.setHeaderLabels( headers )
1215 vbox = QVBoxLayout()
1222 vbox.addWidget(buttons)
1227 def create_addresses_tab(self):
1228 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1229 for i,width in enumerate(self.column_widths['receive']):
1230 l.setColumnWidth(i, width)
1231 l.setContextMenuPolicy(Qt.CustomContextMenu)
1232 l.customContextMenuRequested.connect(self.create_receive_menu)
1233 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1234 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1235 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1236 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1237 self.address_list = l
1243 def save_column_widths(self):
1244 self.column_widths["receive"] = []
1245 for i in range(self.address_list.columnCount() -1):
1246 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1248 self.column_widths["history"] = []
1249 for i in range(self.history_list.columnCount() - 1):
1250 self.column_widths["history"].append(self.history_list.columnWidth(i))
1252 self.column_widths["contacts"] = []
1253 for i in range(self.contacts_list.columnCount() - 1):
1254 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1256 self.config.set_key("column_widths_2", self.column_widths, True)
1259 def create_contacts_tab(self):
1260 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1261 l.setContextMenuPolicy(Qt.CustomContextMenu)
1262 l.customContextMenuRequested.connect(self.create_contact_menu)
1263 for i,width in enumerate(self.column_widths['contacts']):
1264 l.setColumnWidth(i, width)
1265 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1266 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1267 self.contacts_list = l
1271 def create_invoices_tab(self):
1272 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1273 l.setColumnWidth(0, 150)
1275 h.setStretchLastSection(False)
1276 h.setResizeMode(1, QHeaderView.Stretch)
1277 l.setContextMenuPolicy(Qt.CustomContextMenu)
1278 l.customContextMenuRequested.connect(self.create_invoice_menu)
1279 self.invoices_list = l
1282 def update_invoices_tab(self):
1283 invoices = self.wallet.storage.get('invoices', {})
1284 l = self.invoices_list
1286 for key, value in invoices.items():
1288 domain, memo, amount, expiration_date, status, tx_hash = value
1292 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1294 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1295 l.addTopLevelItem(item)
1297 l.setCurrentItem(l.topLevelItem(0))
1301 def delete_imported_key(self, addr):
1302 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1303 self.wallet.delete_imported_key(addr)
1304 self.update_address_tab()
1305 self.update_history_tab()
1307 def edit_account_label(self, k):
1308 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1310 label = unicode(text)
1311 self.wallet.set_label(k,label)
1312 self.update_address_tab()
1314 def account_set_expanded(self, item, k, b):
1316 self.accounts_expanded[k] = b
1318 def create_account_menu(self, position, k, item):
1320 if item.isExpanded():
1321 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1323 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1324 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1325 if self.wallet.seed_version > 4:
1326 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1327 if self.wallet.account_is_pending(k):
1328 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1329 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1331 def delete_pending_account(self, k):
1332 self.wallet.delete_pending_account(k)
1333 self.update_address_tab()
1335 def create_receive_menu(self, position):
1336 # fixme: this function apparently has a side effect.
1337 # if it is not called the menu pops up several times
1338 #self.address_list.selectedIndexes()
1340 selected = self.address_list.selectedItems()
1341 multi_select = len(selected) > 1
1342 addrs = [unicode(item.text(0)) for item in selected]
1343 if not multi_select:
1344 item = self.address_list.itemAt(position)
1348 if not is_valid(addr):
1349 k = str(item.data(0,32).toString())
1351 self.create_account_menu(position, k, item)
1353 item.setExpanded(not item.isExpanded())
1357 if not multi_select:
1358 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1359 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1360 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1361 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1362 if not self.wallet.is_watching_only():
1363 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1364 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1365 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1366 if self.wallet.is_imported(addr):
1367 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1369 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1370 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1371 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1372 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1375 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1376 if any(can_send(addr) for addr in addrs):
1377 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1379 run_hook('receive_menu', menu, addrs)
1380 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1383 def get_sendable_balance(self):
1384 return sum(map(lambda x:x['value'], self.get_coins()))
1387 def get_coins(self):
1389 return self.pay_from
1391 domain = self.wallet.get_account_addresses(self.current_account)
1392 for i in self.wallet.frozen_addresses:
1393 if i in domain: domain.remove(i)
1394 return self.wallet.get_unspent_coins(domain)
1397 def send_from_addresses(self, addrs):
1398 self.set_pay_from( addrs )
1399 self.tabs.setCurrentIndex(1)
1402 def payto(self, addr):
1404 label = self.wallet.labels.get(addr)
1405 m_addr = label + ' <' + addr + '>' if label else addr
1406 self.tabs.setCurrentIndex(1)
1407 self.payto_e.setText(m_addr)
1408 self.amount_e.setFocus()
1411 def delete_contact(self, x):
1412 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1413 self.wallet.delete_contact(x)
1414 self.wallet.set_label(x, None)
1415 self.update_history_tab()
1416 self.update_contacts_tab()
1417 self.update_completions()
1420 def create_contact_menu(self, position):
1421 item = self.contacts_list.itemAt(position)
1424 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1426 addr = unicode(item.text(0))
1427 label = unicode(item.text(1))
1428 is_editable = item.data(0,32).toBool()
1429 payto_addr = item.data(0,33).toString()
1430 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1431 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1432 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1434 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1435 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1437 run_hook('create_contact_menu', menu, item)
1438 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1440 def delete_invoice(self, key):
1441 self.invoices.pop(key)
1442 self.wallet.storage.put('invoices', self.invoices)
1443 self.update_invoices_tab()
1445 def show_invoice(self, key):
1446 from electrum.paymentrequest import PaymentRequest
1447 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1448 pr = PaymentRequest(self.config)
1452 self.show_pr_details(pr)
1454 def show_pr_details(self, pr):
1455 msg = 'Domain: ' + pr.domain
1456 msg += '\nStatus: ' + pr.get_status()
1457 msg += '\nMemo: ' + pr.get_memo()
1458 msg += '\nPayment URL: ' + pr.payment_url
1459 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1460 QMessageBox.information(self, 'Invoice', msg , 'OK')
1462 def do_pay_invoice(self, key):
1463 from electrum.paymentrequest import PaymentRequest
1464 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1465 pr = PaymentRequest(self.config)
1468 self.payment_request = pr
1469 self.prepare_for_payment_request()
1471 self.payment_request_ok()
1473 self.payment_request_error()
1476 def create_invoice_menu(self, position):
1477 item = self.invoices_list.itemAt(position)
1480 k = self.invoices_list.indexOfTopLevelItem(item)
1481 key = self.invoices.keys()[k]
1482 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1484 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1485 if status == PR_UNPAID:
1486 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1487 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1488 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1491 def update_address_item(self, item):
1492 item.setFont(0, QFont(MONOSPACE_FONT))
1493 address = str(item.data(0,0).toString())
1494 label = self.wallet.labels.get(address,'')
1495 item.setData(1,0,label)
1496 item.setData(0,32, True) # is editable
1498 run_hook('update_address_item', address, item)
1500 if not self.wallet.is_mine(address): return
1502 c, u = self.wallet.get_addr_balance(address)
1503 balance = self.format_amount(c + u)
1504 item.setData(2,0,balance)
1506 if address in self.wallet.frozen_addresses:
1507 item.setBackgroundColor(0, QColor('lightblue'))
1510 def update_address_tab(self):
1511 l = self.address_list
1512 # extend the syntax for consistency
1513 l.addChild = l.addTopLevelItem
1514 l.insertChild = l.insertTopLevelItem
1518 accounts = self.wallet.get_accounts()
1519 if self.current_account is None:
1520 account_items = sorted(accounts.items())
1522 account_items = [(self.current_account, accounts.get(self.current_account))]
1525 for k, account in account_items:
1527 if len(accounts) > 1:
1528 name = self.wallet.get_account_name(k)
1529 c,u = self.wallet.get_account_balance(k)
1530 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1531 l.addTopLevelItem(account_item)
1532 account_item.setExpanded(self.accounts_expanded.get(k, True))
1533 account_item.setData(0, 32, k)
1537 sequences = [0,1] if account.has_change() else [0]
1538 for is_change in sequences:
1539 if len(sequences) > 1:
1540 name = _("Receiving") if not is_change else _("Change")
1541 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1542 account_item.addChild(seq_item)
1544 seq_item.setExpanded(True)
1546 seq_item = account_item
1548 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1554 for address in account.get_addresses(is_change):
1556 num, is_used = self.wallet.is_used(address)
1559 if gap > self.wallet.gap_limit:
1564 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1565 self.update_address_item(item)
1567 item.setBackgroundColor(1, QColor('red'))
1571 seq_item.insertChild(0,used_item)
1573 used_item.addChild(item)
1575 seq_item.addChild(item)
1577 # we use column 1 because column 0 may be hidden
1578 l.setCurrentItem(l.topLevelItem(0),1)
1581 def update_contacts_tab(self):
1582 l = self.contacts_list
1585 for address in self.wallet.addressbook:
1586 label = self.wallet.labels.get(address,'')
1587 n = self.wallet.get_num_tx(address)
1588 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1589 item.setFont(0, QFont(MONOSPACE_FONT))
1590 # 32 = label can be edited (bool)
1591 item.setData(0,32, True)
1593 item.setData(0,33, address)
1594 l.addTopLevelItem(item)
1596 run_hook('update_contacts_tab', l)
1597 l.setCurrentItem(l.topLevelItem(0))
1601 def create_console_tab(self):
1602 from console import Console
1603 self.console = console = Console()
1607 def update_console(self):
1608 console = self.console
1609 console.history = self.config.get("console-history",[])
1610 console.history_index = len(console.history)
1612 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1613 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1615 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1617 def mkfunc(f, method):
1618 return lambda *args: apply( f, (method, args, self.password_dialog ))
1620 if m[0]=='_' or m in ['network','wallet']: continue
1621 methods[m] = mkfunc(c._run, m)
1623 console.updateNamespace(methods)
1626 def change_account(self,s):
1627 if s == _("All accounts"):
1628 self.current_account = None
1630 accounts = self.wallet.get_account_names()
1631 for k, v in accounts.items():
1633 self.current_account = k
1634 self.update_history_tab()
1635 self.update_status()
1636 self.update_address_tab()
1637 self.update_receive_tab()
1639 def create_status_bar(self):
1642 sb.setFixedHeight(35)
1643 qtVersion = qVersion()
1645 self.balance_label = QLabel("")
1646 sb.addWidget(self.balance_label)
1648 from version_getter import UpdateLabel
1649 self.updatelabel = UpdateLabel(self.config, sb)
1651 self.account_selector = QComboBox()
1652 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1653 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1654 sb.addPermanentWidget(self.account_selector)
1656 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1657 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1659 self.lock_icon = QIcon()
1660 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1661 sb.addPermanentWidget( self.password_button )
1663 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1664 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1665 sb.addPermanentWidget( self.seed_button )
1666 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1667 sb.addPermanentWidget( self.status_button )
1669 run_hook('create_status_bar', (sb,))
1671 self.setStatusBar(sb)
1674 def update_lock_icon(self):
1675 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1676 self.password_button.setIcon( icon )
1679 def update_buttons_on_seed(self):
1680 if self.wallet.has_seed():
1681 self.seed_button.show()
1683 self.seed_button.hide()
1685 if not self.wallet.is_watching_only():
1686 self.password_button.show()
1687 self.send_button.setText(_("Send"))
1689 self.password_button.hide()
1690 self.send_button.setText(_("Create unsigned transaction"))
1693 def change_password_dialog(self):
1694 from password_dialog import PasswordDialog
1695 d = PasswordDialog(self.wallet, self)
1697 self.update_lock_icon()
1700 def new_contact_dialog(self):
1703 d.setWindowTitle(_("New Contact"))
1704 vbox = QVBoxLayout(d)
1705 vbox.addWidget(QLabel(_('New Contact')+':'))
1707 grid = QGridLayout()
1710 grid.addWidget(QLabel(_("Address")), 1, 0)
1711 grid.addWidget(line1, 1, 1)
1712 grid.addWidget(QLabel(_("Name")), 2, 0)
1713 grid.addWidget(line2, 2, 1)
1715 vbox.addLayout(grid)
1716 vbox.addLayout(ok_cancel_buttons(d))
1721 address = str(line1.text())
1722 label = unicode(line2.text())
1724 if not is_valid(address):
1725 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1728 self.wallet.add_contact(address)
1730 self.wallet.set_label(address, label)
1732 self.update_contacts_tab()
1733 self.update_history_tab()
1734 self.update_completions()
1735 self.tabs.setCurrentIndex(3)
1739 def new_account_dialog(self, password):
1741 dialog = QDialog(self)
1743 dialog.setWindowTitle(_("New Account"))
1745 vbox = QVBoxLayout()
1746 vbox.addWidget(QLabel(_('Account name')+':'))
1749 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1750 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1755 vbox.addLayout(ok_cancel_buttons(dialog))
1756 dialog.setLayout(vbox)
1760 name = str(e.text())
1763 self.wallet.create_pending_account(name, password)
1764 self.update_address_tab()
1765 self.tabs.setCurrentIndex(2)
1770 def show_master_public_keys(self):
1772 dialog = QDialog(self)
1774 dialog.setWindowTitle(_("Master Public Keys"))
1776 main_layout = QGridLayout()
1777 mpk_dict = self.wallet.get_master_public_keys()
1779 for key, value in mpk_dict.items():
1780 main_layout.addWidget(QLabel(key), i, 0)
1781 mpk_text = QTextEdit()
1782 mpk_text.setReadOnly(True)
1783 mpk_text.setMaximumHeight(170)
1784 mpk_text.setText(value)
1785 main_layout.addWidget(mpk_text, i + 1, 0)
1788 vbox = QVBoxLayout()
1789 vbox.addLayout(main_layout)
1790 vbox.addLayout(close_button(dialog))
1792 dialog.setLayout(vbox)
1797 def show_seed_dialog(self, password):
1798 if not self.wallet.has_seed():
1799 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1803 mnemonic = self.wallet.get_mnemonic(password)
1805 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1807 from seed_dialog import SeedDialog
1808 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1813 def show_qrcode(self, data, title = _("QR code")):
1816 d = QRDialog(data, self, title)
1820 def do_protect(self, func, args):
1821 if self.wallet.use_encryption:
1822 password = self.password_dialog()
1828 if args != (False,):
1829 args = (self,) + args + (password,)
1831 args = (self,password)
1835 def show_public_keys(self, address):
1836 if not address: return
1838 pubkey_list = self.wallet.get_public_keys(address)
1839 except Exception as e:
1840 traceback.print_exc(file=sys.stdout)
1841 self.show_message(str(e))
1845 d.setMinimumSize(600, 200)
1847 vbox = QVBoxLayout()
1848 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1849 vbox.addWidget( QLabel(_("Public key") + ':'))
1851 keys.setReadOnly(True)
1852 keys.setText('\n'.join(pubkey_list))
1853 vbox.addWidget(keys)
1854 vbox.addLayout(close_button(d))
1859 def show_private_key(self, address, password):
1860 if not address: return
1862 pk_list = self.wallet.get_private_key(address, password)
1863 except Exception as e:
1864 traceback.print_exc(file=sys.stdout)
1865 self.show_message(str(e))
1869 d.setMinimumSize(600, 200)
1871 vbox = QVBoxLayout()
1872 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1873 vbox.addWidget( QLabel(_("Private key") + ':'))
1875 keys.setReadOnly(True)
1876 keys.setText('\n'.join(pk_list))
1877 vbox.addWidget(keys)
1878 vbox.addLayout(close_button(d))
1884 def do_sign(self, address, message, signature, password):
1885 message = unicode(message.toPlainText())
1886 message = message.encode('utf-8')
1888 sig = self.wallet.sign_message(str(address.text()), message, password)
1889 signature.setText(sig)
1890 except Exception as e:
1891 self.show_message(str(e))
1893 def do_verify(self, address, message, signature):
1894 message = unicode(message.toPlainText())
1895 message = message.encode('utf-8')
1896 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1897 self.show_message(_("Signature verified"))
1899 self.show_message(_("Error: wrong signature"))
1902 def sign_verify_message(self, address=''):
1905 d.setWindowTitle(_('Sign/verify Message'))
1906 d.setMinimumSize(410, 290)
1908 layout = QGridLayout(d)
1910 message_e = QTextEdit()
1911 layout.addWidget(QLabel(_('Message')), 1, 0)
1912 layout.addWidget(message_e, 1, 1)
1913 layout.setRowStretch(2,3)
1915 address_e = QLineEdit()
1916 address_e.setText(address)
1917 layout.addWidget(QLabel(_('Address')), 2, 0)
1918 layout.addWidget(address_e, 2, 1)
1920 signature_e = QTextEdit()
1921 layout.addWidget(QLabel(_('Signature')), 3, 0)
1922 layout.addWidget(signature_e, 3, 1)
1923 layout.setRowStretch(3,1)
1925 hbox = QHBoxLayout()
1927 b = QPushButton(_("Sign"))
1928 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1931 b = QPushButton(_("Verify"))
1932 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1935 b = QPushButton(_("Close"))
1936 b.clicked.connect(d.accept)
1938 layout.addLayout(hbox, 4, 1)
1943 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1945 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1946 message_e.setText(decrypted)
1947 except Exception as e:
1948 self.show_message(str(e))
1951 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1952 message = unicode(message_e.toPlainText())
1953 message = message.encode('utf-8')
1955 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1956 encrypted_e.setText(encrypted)
1957 except Exception as e:
1958 self.show_message(str(e))
1962 def encrypt_message(self, address = ''):
1965 d.setWindowTitle(_('Encrypt/decrypt Message'))
1966 d.setMinimumSize(610, 490)
1968 layout = QGridLayout(d)
1970 message_e = QTextEdit()
1971 layout.addWidget(QLabel(_('Message')), 1, 0)
1972 layout.addWidget(message_e, 1, 1)
1973 layout.setRowStretch(2,3)
1975 pubkey_e = QLineEdit()
1977 pubkey = self.wallet.getpubkeys(address)[0]
1978 pubkey_e.setText(pubkey)
1979 layout.addWidget(QLabel(_('Public key')), 2, 0)
1980 layout.addWidget(pubkey_e, 2, 1)
1982 encrypted_e = QTextEdit()
1983 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1984 layout.addWidget(encrypted_e, 3, 1)
1985 layout.setRowStretch(3,1)
1987 hbox = QHBoxLayout()
1988 b = QPushButton(_("Encrypt"))
1989 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1992 b = QPushButton(_("Decrypt"))
1993 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1996 b = QPushButton(_("Close"))
1997 b.clicked.connect(d.accept)
2000 layout.addLayout(hbox, 4, 1)
2004 def question(self, msg):
2005 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2007 def show_message(self, msg):
2008 QMessageBox.information(self, _('Message'), msg, _('OK'))
2010 def password_dialog(self, msg=None):
2013 d.setWindowTitle(_("Enter Password"))
2018 vbox = QVBoxLayout()
2020 msg = _('Please enter your password')
2021 vbox.addWidget(QLabel(msg))
2023 grid = QGridLayout()
2025 grid.addWidget(QLabel(_('Password')), 1, 0)
2026 grid.addWidget(pw, 1, 1)
2027 vbox.addLayout(grid)
2029 vbox.addLayout(ok_cancel_buttons(d))
2032 run_hook('password_dialog', pw, grid, 1)
2033 if not d.exec_(): return
2034 return unicode(pw.text())
2043 def tx_from_text(self, txt):
2044 "json or raw hexadecimal"
2053 return Transaction(txt)
2055 traceback.print_exc(file=sys.stdout)
2056 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2060 tx_dict = json.loads(str(txt))
2061 assert "hex" in tx_dict.keys()
2062 tx = Transaction(tx_dict["hex"])
2063 #if tx_dict.has_key("input_info"):
2064 # input_info = json.loads(tx_dict['input_info'])
2065 # tx.add_input_info(input_info)
2068 traceback.print_exc(file=sys.stdout)
2069 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2073 def read_tx_from_file(self):
2074 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2078 with open(fileName, "r") as f:
2079 file_content = f.read()
2080 except (ValueError, IOError, os.error), reason:
2081 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2083 return self.tx_from_text(file_content)
2087 def sign_raw_transaction(self, tx, password):
2089 self.wallet.signrawtransaction(tx, [], password)
2090 except Exception as e:
2091 traceback.print_exc(file=sys.stdout)
2092 QMessageBox.warning(self, _("Error"), str(e))
2094 def do_process_from_text(self):
2095 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2098 tx = self.tx_from_text(text)
2100 self.show_transaction(tx)
2102 def do_process_from_file(self):
2103 tx = self.read_tx_from_file()
2105 self.show_transaction(tx)
2107 def do_process_from_txid(self):
2108 from electrum import transaction
2109 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2111 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2113 tx = transaction.Transaction(r)
2115 self.show_transaction(tx)
2117 self.show_message("unknown transaction")
2119 def do_process_from_csvReader(self, csvReader):
2124 for position, row in enumerate(csvReader):
2126 if not is_valid(address):
2127 errors.append((position, address))
2129 amount = Decimal(row[1])
2130 amount = int(100000000*amount)
2131 outputs.append((address, amount))
2132 except (ValueError, IOError, os.error), reason:
2133 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2137 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2138 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2142 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2143 except Exception as e:
2144 self.show_message(str(e))
2147 self.show_transaction(tx)
2149 def do_process_from_csv_file(self):
2150 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2154 with open(fileName, "r") as f:
2155 csvReader = csv.reader(f)
2156 self.do_process_from_csvReader(csvReader)
2157 except (ValueError, IOError, os.error), reason:
2158 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2161 def do_process_from_csv_text(self):
2162 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2163 + _("Format: address, amount. One output per line"), _("Load CSV"))
2166 f = StringIO.StringIO(text)
2167 csvReader = csv.reader(f)
2168 self.do_process_from_csvReader(csvReader)
2173 def export_privkeys_dialog(self, password):
2174 if self.wallet.is_watching_only():
2175 self.show_message(_("This is a watching-only wallet"))
2179 d.setWindowTitle(_('Private keys'))
2180 d.setMinimumSize(850, 300)
2181 vbox = QVBoxLayout(d)
2183 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2184 _("Exposing a single private key can compromise your entire wallet!"),
2185 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2186 vbox.addWidget(QLabel(msg))
2192 defaultname = 'electrum-private-keys.csv'
2193 select_msg = _('Select file to export your private keys to')
2194 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2195 vbox.addLayout(hbox)
2197 h, b = ok_cancel_buttons2(d, _('Export'))
2202 addresses = self.wallet.addresses(True)
2204 def privkeys_thread():
2205 for addr in addresses:
2209 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2210 d.emit(SIGNAL('computing_privkeys'))
2211 d.emit(SIGNAL('show_privkeys'))
2213 def show_privkeys():
2214 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2218 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2219 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2220 threading.Thread(target=privkeys_thread).start()
2226 filename = filename_e.text()
2231 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2232 except (IOError, os.error), reason:
2233 export_error_label = _("Electrum was unable to produce a private key-export.")
2234 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2236 except Exception as e:
2237 self.show_message(str(e))
2240 self.show_message(_("Private keys exported."))
2243 def do_export_privkeys(self, fileName, pklist, is_csv):
2244 with open(fileName, "w+") as f:
2246 transaction = csv.writer(f)
2247 transaction.writerow(["address", "private_key"])
2248 for addr, pk in pklist.items():
2249 transaction.writerow(["%34s"%addr,pk])
2252 f.write(json.dumps(pklist, indent = 4))
2255 def do_import_labels(self):
2256 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2257 if not labelsFile: return
2259 f = open(labelsFile, 'r')
2262 for key, value in json.loads(data).items():
2263 self.wallet.set_label(key, value)
2264 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2265 except (IOError, os.error), reason:
2266 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2269 def do_export_labels(self):
2270 labels = self.wallet.labels
2272 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2274 with open(fileName, 'w+') as f:
2275 json.dump(labels, f)
2276 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2277 except (IOError, os.error), reason:
2278 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2281 def export_history_dialog(self):
2284 d.setWindowTitle(_('Export History'))
2285 d.setMinimumSize(400, 200)
2286 vbox = QVBoxLayout(d)
2288 defaultname = os.path.expanduser('~/electrum-history.csv')
2289 select_msg = _('Select file to export your wallet transactions to')
2291 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2292 vbox.addLayout(hbox)
2296 h, b = ok_cancel_buttons2(d, _('Export'))
2301 filename = filename_e.text()
2306 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2307 except (IOError, os.error), reason:
2308 export_error_label = _("Electrum was unable to produce a transaction export.")
2309 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2312 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2315 def do_export_history(self, wallet, fileName, is_csv):
2316 history = wallet.get_tx_history()
2318 for item in history:
2319 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2321 if timestamp is not None:
2323 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2324 except [RuntimeError, TypeError, NameError] as reason:
2325 time_string = "unknown"
2328 time_string = "unknown"
2330 time_string = "pending"
2332 if value is not None:
2333 value_string = format_satoshis(value, True)
2338 fee_string = format_satoshis(fee, True)
2343 label, is_default_label = wallet.get_label(tx_hash)
2344 label = label.encode('utf-8')
2348 balance_string = format_satoshis(balance, False)
2350 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2352 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2354 with open(fileName, "w+") as f:
2356 transaction = csv.writer(f)
2357 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2359 transaction.writerow(line)
2362 f.write(json.dumps(lines, indent = 4))
2365 def sweep_key_dialog(self):
2367 d.setWindowTitle(_('Sweep private keys'))
2368 d.setMinimumSize(600, 300)
2370 vbox = QVBoxLayout(d)
2371 vbox.addWidget(QLabel(_("Enter private keys")))
2373 keys_e = QTextEdit()
2374 keys_e.setTabChangesFocus(True)
2375 vbox.addWidget(keys_e)
2377 h, address_e = address_field(self.wallet.addresses())
2381 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2382 vbox.addLayout(hbox)
2383 button.setEnabled(False)
2386 addr = str(address_e.text())
2387 if bitcoin.is_address(addr):
2391 pk = str(keys_e.toPlainText()).strip()
2392 if Wallet.is_private_key(pk):
2395 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2396 keys_e.textChanged.connect(f)
2397 address_e.textChanged.connect(f)
2401 fee = self.wallet.fee
2402 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2403 self.show_transaction(tx)
2407 def do_import_privkey(self, password):
2408 if not self.wallet.has_imported_keys():
2409 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2410 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2411 + _('Are you sure you understand what you are doing?'), 3, 4)
2414 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2417 text = str(text).split()
2422 addr = self.wallet.import_key(key, password)
2423 except Exception as e:
2429 addrlist.append(addr)
2431 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2433 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2434 self.update_address_tab()
2435 self.update_history_tab()
2438 def settings_dialog(self):
2440 d.setWindowTitle(_('Electrum Settings'))
2442 vbox = QVBoxLayout()
2443 grid = QGridLayout()
2444 grid.setColumnStretch(0,1)
2446 nz_label = QLabel(_('Display zeros') + ':')
2447 grid.addWidget(nz_label, 0, 0)
2448 nz_e = AmountEdit(None,True)
2449 nz_e.setText("%d"% self.num_zeros)
2450 grid.addWidget(nz_e, 0, 1)
2451 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2452 grid.addWidget(HelpButton(msg), 0, 2)
2453 if not self.config.is_modifiable('num_zeros'):
2454 for w in [nz_e, nz_label]: w.setEnabled(False)
2456 lang_label=QLabel(_('Language') + ':')
2457 grid.addWidget(lang_label, 1, 0)
2458 lang_combo = QComboBox()
2459 from electrum.i18n import languages
2460 lang_combo.addItems(languages.values())
2462 index = languages.keys().index(self.config.get("language",''))
2465 lang_combo.setCurrentIndex(index)
2466 grid.addWidget(lang_combo, 1, 1)
2467 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2468 if not self.config.is_modifiable('language'):
2469 for w in [lang_combo, lang_label]: w.setEnabled(False)
2472 fee_label = QLabel(_('Transaction fee') + ':')
2473 grid.addWidget(fee_label, 2, 0)
2474 fee_e = BTCAmountEdit(self.get_decimal_point)
2475 fee_e.setAmount(self.wallet.fee)
2476 grid.addWidget(fee_e, 2, 1)
2477 msg = _('Fee per kilobyte of transaction.') + '\n' \
2478 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2479 grid.addWidget(HelpButton(msg), 2, 2)
2480 if not self.config.is_modifiable('fee_per_kb'):
2481 for w in [fee_e, fee_label]: w.setEnabled(False)
2483 units = ['BTC', 'mBTC']
2484 unit_label = QLabel(_('Base unit') + ':')
2485 grid.addWidget(unit_label, 3, 0)
2486 unit_combo = QComboBox()
2487 unit_combo.addItems(units)
2488 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2489 grid.addWidget(unit_combo, 3, 1)
2490 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2491 + '\n1BTC=1000mBTC.\n' \
2492 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2494 usechange_cb = QCheckBox(_('Use change addresses'))
2495 usechange_cb.setChecked(self.wallet.use_change)
2496 grid.addWidget(usechange_cb, 4, 0)
2497 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2498 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2500 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2501 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2502 grid.addWidget(block_ex_label, 5, 0)
2503 block_ex_combo = QComboBox()
2504 block_ex_combo.addItems(block_explorers)
2505 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2506 grid.addWidget(block_ex_combo, 5, 1)
2507 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2509 show_tx = self.config.get('show_before_broadcast', False)
2510 showtx_cb = QCheckBox(_('Show before broadcast'))
2511 showtx_cb.setChecked(show_tx)
2512 grid.addWidget(showtx_cb, 6, 0)
2513 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2515 vbox.addLayout(grid)
2517 vbox.addLayout(ok_cancel_buttons(d))
2521 if not d.exec_(): return
2523 fee = fee_e.get_amount()
2525 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2528 self.wallet.set_fee(fee)
2530 nz = unicode(nz_e.text())
2535 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2538 if self.num_zeros != nz:
2540 self.config.set_key('num_zeros', nz, True)
2541 self.update_history_tab()
2542 self.update_address_tab()
2544 usechange_result = usechange_cb.isChecked()
2545 if self.wallet.use_change != usechange_result:
2546 self.wallet.use_change = usechange_result
2547 self.wallet.storage.put('use_change', self.wallet.use_change)
2549 if showtx_cb.isChecked() != show_tx:
2550 self.config.set_key('show_before_broadcast', not show_tx)
2552 unit_result = units[unit_combo.currentIndex()]
2553 if self.base_unit() != unit_result:
2554 self.decimal_point = 8 if unit_result == 'BTC' else 5
2555 self.config.set_key('decimal_point', self.decimal_point, True)
2556 self.update_history_tab()
2557 self.update_status()
2559 need_restart = False
2561 lang_request = languages.keys()[lang_combo.currentIndex()]
2562 if lang_request != self.config.get('language'):
2563 self.config.set_key("language", lang_request, True)
2566 be_result = block_explorers[block_ex_combo.currentIndex()]
2567 self.config.set_key('block_explorer', be_result, True)
2569 run_hook('close_settings_dialog')
2572 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2575 def run_network_dialog(self):
2576 if not self.network:
2578 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2580 def closeEvent(self, event):
2582 self.config.set_key("is_maximized", self.isMaximized())
2583 if not self.isMaximized():
2585 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2586 self.save_column_widths()
2587 self.config.set_key("console-history", self.console.history[-50:], True)
2588 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2592 def plugins_dialog(self):
2593 from electrum.plugins import plugins
2596 d.setWindowTitle(_('Electrum Plugins'))
2599 vbox = QVBoxLayout(d)
2602 scroll = QScrollArea()
2603 scroll.setEnabled(True)
2604 scroll.setWidgetResizable(True)
2605 scroll.setMinimumSize(400,250)
2606 vbox.addWidget(scroll)
2610 w.setMinimumHeight(len(plugins)*35)
2612 grid = QGridLayout()
2613 grid.setColumnStretch(0,1)
2616 def do_toggle(cb, p, w):
2619 if w: w.setEnabled(r)
2621 def mk_toggle(cb, p, w):
2622 return lambda: do_toggle(cb,p,w)
2624 for i, p in enumerate(plugins):
2626 cb = QCheckBox(p.fullname())
2627 cb.setDisabled(not p.is_available())
2628 cb.setChecked(p.is_enabled())
2629 grid.addWidget(cb, i, 0)
2630 if p.requires_settings():
2631 w = p.settings_widget(self)
2632 w.setEnabled( p.is_enabled() )
2633 grid.addWidget(w, i, 1)
2636 cb.clicked.connect(mk_toggle(cb,p,w))
2637 grid.addWidget(HelpButton(p.description()), i, 2)
2639 print_msg(_("Error: cannot display plugin"), p)
2640 traceback.print_exc(file=sys.stdout)
2641 grid.setRowStretch(i+1,1)
2643 vbox.addLayout(close_button(d))
2648 def show_account_details(self, k):
2649 account = self.wallet.accounts[k]
2652 d.setWindowTitle(_('Account Details'))
2655 vbox = QVBoxLayout(d)
2656 name = self.wallet.get_account_name(k)
2657 label = QLabel('Name: ' + name)
2658 vbox.addWidget(label)
2660 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2662 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2664 vbox.addWidget(QLabel(_('Master Public Key:')))
2667 text.setReadOnly(True)
2668 text.setMaximumHeight(170)
2669 vbox.addWidget(text)
2671 mpk_text = '\n'.join( account.get_master_pubkeys() )
2672 text.setText(mpk_text)
2674 vbox.addLayout(close_button(d))