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
43 from electrum import Imported_Wallet
45 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
46 from network_dialog import NetworkDialog
47 from qrcodewidget import QRCodeWidget, QRDialog
48 from qrtextedit import QRTextEdit
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
67 # status of payment requests
70 PR_SENT = 2 # sent but not propagated
71 PR_PAID = 3 # send and propagated
72 PR_ERROR = 4 # could not parse
75 from electrum import ELECTRUM_VERSION
78 from util import MyTreeWidget, HelpButton, EnterButton, line_dialog, text_dialog, ok_cancel_buttons, close_button, WaitingDialog
90 class StatusBarButton(QPushButton):
91 def __init__(self, icon, tooltip, func):
92 QPushButton.__init__(self, icon, '')
93 self.setToolTip(tooltip)
95 self.setMaximumWidth(25)
96 self.clicked.connect(func)
98 self.setIconSize(QSize(25,25))
100 def keyPressEvent(self, e):
101 if e.key() == QtCore.Qt.Key_Return:
113 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
115 class ElectrumWindow(QMainWindow):
119 def __init__(self, config, network, gui_object):
120 QMainWindow.__init__(self)
123 self.network = network
124 self.gui_object = gui_object
125 self.tray = gui_object.tray
126 self.go_lite = gui_object.go_lite
129 self.create_status_bar()
130 self.need_update = threading.Event()
132 self.decimal_point = config.get('decimal_point', 5)
133 self.num_zeros = int(config.get('num_zeros',0))
136 set_language(config.get('language'))
138 self.completions = QStringListModel()
140 self.tabs = tabs = QTabWidget(self)
141 self.column_widths = self.config.get("column_widths_2", default_column_widths )
142 tabs.addTab(self.create_history_tab(), _('History') )
143 tabs.addTab(self.create_send_tab(), _('Send') )
144 tabs.addTab(self.create_receive_tab(), _('Receive') )
145 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
146 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
147 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
148 tabs.addTab(self.create_console_tab(), _('Console') )
149 tabs.setMinimumSize(600, 400)
150 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
151 self.setCentralWidget(tabs)
153 g = self.config.get("winpos-qt",[100, 100, 840, 400])
154 self.setGeometry(g[0], g[1], g[2], g[3])
155 if self.config.get("is_maximized"):
158 self.setWindowIcon(QIcon(":icons/electrum.png"))
161 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
163 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
164 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
165 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
167 for i in range(tabs.count()):
168 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
170 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
171 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
172 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
173 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
174 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
176 self.history_list.setFocus(True)
180 self.network.register_callback('updated', lambda: self.need_update.set())
181 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
182 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
184 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
186 # set initial message
187 self.console.showMessage(self.network.banner)
190 self.payment_request = None
192 def update_account_selector(self):
194 accounts = self.wallet.get_account_names()
195 self.account_selector.clear()
196 if len(accounts) > 1:
197 self.account_selector.addItems([_("All accounts")] + accounts.values())
198 self.account_selector.setCurrentIndex(0)
199 self.account_selector.show()
201 self.account_selector.hide()
204 def load_wallet(self, wallet):
208 self.update_wallet_format()
210 self.invoices = self.wallet.storage.get('invoices', {})
211 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
212 self.current_account = self.wallet.storage.get("current_account", None)
213 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
214 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
215 self.setWindowTitle( title )
217 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
218 self.notify_transactions()
219 self.update_account_selector()
221 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
222 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
223 self.password_menu.setEnabled(not self.wallet.is_watching_only())
224 self.seed_menu.setEnabled(self.wallet.has_seed())
225 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
226 self.import_menu.setEnabled(self.wallet.can_import())
228 self.update_lock_icon()
229 self.update_buttons_on_seed()
230 self.update_console()
232 self.clear_receive_tab()
233 self.update_receive_tab()
234 run_hook('load_wallet', wallet)
237 def update_wallet_format(self):
238 # convert old-format imported keys
239 if self.wallet.imported_keys:
240 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
242 self.wallet.convert_imported_keys(password)
244 self.show_message("error")
247 def open_wallet(self):
248 wallet_folder = self.wallet.storage.path
249 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
253 storage = WalletStorage({'wallet_path': filename})
254 if not storage.file_exists:
255 self.show_message("file not found "+ filename)
258 self.wallet.stop_threads()
261 wallet = Wallet(storage)
262 wallet.start_threads(self.network)
264 self.load_wallet(wallet)
268 def backup_wallet(self):
270 path = self.wallet.storage.path
271 wallet_folder = os.path.dirname(path)
272 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
276 new_path = os.path.join(wallet_folder, filename)
279 shutil.copy2(path, new_path)
280 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
281 except (IOError, os.error), reason:
282 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
285 def new_wallet(self):
288 wallet_folder = os.path.dirname(self.wallet.storage.path)
291 filename = "wallet_%d"%i
292 if filename in os.listdir(wallet_folder):
297 filename = line_dialog(self, _('New Wallet'), _('Enter file name') + ':', _('OK'), filename)
301 full_path = os.path.join(wallet_folder, filename)
302 storage = WalletStorage({'wallet_path': full_path})
303 if storage.file_exists:
304 QMessageBox.critical(None, "Error", _("File exists"))
307 wizard = installwizard.InstallWizard(self.config, self.network, storage)
308 wallet = wizard.run('new')
310 self.load_wallet(wallet)
314 def init_menubar(self):
317 file_menu = menubar.addMenu(_("&File"))
318 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
319 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
320 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
321 file_menu.addAction(_("&Quit"), self.close)
323 wallet_menu = menubar.addMenu(_("&Wallet"))
324 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
325 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
327 wallet_menu.addSeparator()
329 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
330 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
331 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
333 wallet_menu.addSeparator()
334 labels_menu = wallet_menu.addMenu(_("&Labels"))
335 labels_menu.addAction(_("&Import"), self.do_import_labels)
336 labels_menu.addAction(_("&Export"), self.do_export_labels)
338 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
339 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
340 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
341 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
342 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
344 tools_menu = menubar.addMenu(_("&Tools"))
346 # Settings / Preferences are all reserved keywords in OSX using this as work around
347 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
348 tools_menu.addAction(_("&Network"), self.run_network_dialog)
349 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
350 tools_menu.addSeparator()
351 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
352 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
353 tools_menu.addSeparator()
355 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
356 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
357 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
359 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
360 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
361 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
362 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
363 raw_transaction_menu.addAction(_("&From QR code"), self.read_tx_from_qrcode)
364 self.raw_transaction_menu = raw_transaction_menu
366 help_menu = menubar.addMenu(_("&Help"))
367 help_menu.addAction(_("&About"), self.show_about)
368 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
369 help_menu.addSeparator()
370 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
371 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
373 self.setMenuBar(menubar)
375 def show_about(self):
376 QMessageBox.about(self, "Electrum",
377 _("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."))
379 def show_report_bug(self):
380 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
381 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
384 def notify_transactions(self):
385 if not self.network or not self.network.is_connected():
388 print_error("Notifying GUI")
389 if len(self.network.pending_transactions_for_notifications) > 0:
390 # Combine the transactions if there are more then three
391 tx_amount = len(self.network.pending_transactions_for_notifications)
394 for tx in self.network.pending_transactions_for_notifications:
395 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
399 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
400 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
402 self.network.pending_transactions_for_notifications = []
404 for tx in self.network.pending_transactions_for_notifications:
406 self.network.pending_transactions_for_notifications.remove(tx)
407 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
409 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
411 def notify(self, message):
412 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
416 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
417 def getOpenFileName(self, title, filter = ""):
418 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
419 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
420 if fileName and directory != os.path.dirname(fileName):
421 self.config.set_key('io_dir', os.path.dirname(fileName), True)
424 def getSaveFileName(self, title, filename, filter = ""):
425 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
426 path = os.path.join( directory, filename )
427 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
428 if fileName and directory != os.path.dirname(fileName):
429 self.config.set_key('io_dir', os.path.dirname(fileName), True)
433 QMainWindow.close(self)
434 run_hook('close_main_window')
436 def connect_slots(self, sender):
437 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
438 self.previous_payto_e=''
440 def timer_actions(self):
441 if self.need_update.is_set():
443 self.need_update.clear()
445 run_hook('timer_actions')
447 def format_amount(self, x, is_diff=False, whitespaces=False):
448 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
451 def get_decimal_point(self):
452 return self.decimal_point
456 assert self.decimal_point in [2, 5, 8]
457 if self.decimal_point == 2:
459 if self.decimal_point == 5:
461 if self.decimal_point == 8:
463 raise Exception('Unknown base unit')
465 def update_status(self):
466 if self.network is None or not self.network.is_running():
468 icon = QIcon(":icons/status_disconnected.png")
470 elif self.network.is_connected():
471 if not self.wallet.up_to_date:
472 text = _("Synchronizing...")
473 icon = QIcon(":icons/status_waiting.png")
474 elif self.network.server_lag > 1:
475 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
476 icon = QIcon(":icons/status_lagging.png")
478 c, u = self.wallet.get_account_balance(self.current_account)
479 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
480 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
482 # append fiat balance and price from exchange rate plugin
484 run_hook('get_fiat_status_text', c+u, r)
489 self.tray.setToolTip(text)
490 icon = QIcon(":icons/status_connected.png")
492 text = _("Not connected")
493 icon = QIcon(":icons/status_disconnected.png")
495 self.balance_label.setText(text)
496 self.status_button.setIcon( icon )
499 def update_wallet(self):
501 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
502 self.update_history_tab()
503 self.update_receive_tab()
504 self.update_address_tab()
505 self.update_contacts_tab()
506 self.update_completions()
507 self.update_invoices_tab()
510 def create_history_tab(self):
511 self.history_list = l = MyTreeWidget(self)
513 for i,width in enumerate(self.column_widths['history']):
514 l.setColumnWidth(i, width)
515 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
516 l.itemDoubleClicked.connect(self.tx_label_clicked)
517 l.itemChanged.connect(self.tx_label_changed)
518 l.customContextMenuRequested.connect(self.create_history_menu)
522 def create_history_menu(self, position):
523 self.history_list.selectedIndexes()
524 item = self.history_list.currentItem()
525 be = self.config.get('block_explorer', 'Blockchain.info')
526 if be == 'Blockchain.info':
527 block_explorer = 'https://blockchain.info/tx/'
528 elif be == 'Blockr.io':
529 block_explorer = 'https://blockr.io/tx/info/'
530 elif be == 'Insight.is':
531 block_explorer = 'http://live.insight.is/tx/'
533 tx_hash = str(item.data(0, Qt.UserRole).toString())
534 if not tx_hash: return
536 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
537 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
538 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
539 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
540 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
543 def show_transaction(self, tx):
544 import transaction_dialog
545 d = transaction_dialog.TxDialog(tx, self)
548 def tx_label_clicked(self, item, column):
549 if column==2 and item.isSelected():
551 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
552 self.history_list.editItem( item, column )
553 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
556 def tx_label_changed(self, item, column):
560 tx_hash = str(item.data(0, Qt.UserRole).toString())
561 tx = self.wallet.transactions.get(tx_hash)
562 text = unicode( item.text(2) )
563 self.wallet.set_label(tx_hash, text)
565 item.setForeground(2, QBrush(QColor('black')))
567 text = self.wallet.get_default_label(tx_hash)
568 item.setText(2, text)
569 item.setForeground(2, QBrush(QColor('gray')))
573 def edit_label(self, is_recv):
574 l = self.address_list if is_recv else self.contacts_list
575 item = l.currentItem()
576 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
577 l.editItem( item, 1 )
578 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
582 def address_label_clicked(self, item, column, l, column_addr, column_label):
583 if column == column_label and item.isSelected():
584 is_editable = item.data(0, 32).toBool()
587 addr = unicode( item.text(column_addr) )
588 label = unicode( item.text(column_label) )
589 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
590 l.editItem( item, column )
591 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
594 def address_label_changed(self, item, column, l, column_addr, column_label):
595 if column == column_label:
596 addr = unicode( item.text(column_addr) )
597 text = unicode( item.text(column_label) )
598 is_editable = item.data(0, 32).toBool()
602 changed = self.wallet.set_label(addr, text)
604 self.update_history_tab()
605 self.update_completions()
607 self.current_item_changed(item)
609 run_hook('item_changed', item, column)
612 def current_item_changed(self, a):
613 run_hook('current_item_changed', a)
617 def update_history_tab(self):
619 self.history_list.clear()
620 for item in self.wallet.get_tx_history(self.current_account):
621 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
622 time_str = _("unknown")
625 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
627 time_str = _("error")
630 time_str = 'unverified'
631 icon = QIcon(":icons/unconfirmed.png")
634 icon = QIcon(":icons/unconfirmed.png")
636 icon = QIcon(":icons/clock%d.png"%conf)
638 icon = QIcon(":icons/confirmed.png")
640 if value is not None:
641 v_str = self.format_amount(value, True, whitespaces=True)
645 balance_str = self.format_amount(balance, whitespaces=True)
648 label, is_default_label = self.wallet.get_label(tx_hash)
650 label = _('Pruned transaction outputs')
651 is_default_label = False
653 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
654 item.setFont(2, QFont(MONOSPACE_FONT))
655 item.setFont(3, QFont(MONOSPACE_FONT))
656 item.setFont(4, QFont(MONOSPACE_FONT))
658 item.setForeground(3, QBrush(QColor("#BC1E1E")))
660 item.setData(0, Qt.UserRole, tx_hash)
661 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
663 item.setForeground(2, QBrush(QColor('grey')))
665 item.setIcon(0, icon)
666 self.history_list.insertTopLevelItem(0,item)
669 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
670 run_hook('history_tab_update')
673 def create_receive_tab(self):
675 grid = QGridLayout(w)
676 grid.setColumnMinimumWidth(3, 300)
677 grid.setColumnStretch(5, 1)
679 self.receive_address_e = QLineEdit()
680 self.receive_address_e.setReadOnly(True)
681 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
682 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
683 self.receive_address_e.textChanged.connect(self.update_receive_qr)
685 self.receive_message_e = QLineEdit()
686 grid.addWidget(QLabel(_('Message')), 1, 0)
687 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
688 self.receive_message_e.textChanged.connect(self.update_receive_qr)
690 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
691 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
692 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
693 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
695 self.save_request_button = QPushButton(_('Save'))
696 self.save_request_button.clicked.connect(self.save_payment_request)
697 grid.addWidget(self.save_request_button, 3, 1)
698 clear_button = QPushButton(_('New'))
699 clear_button.clicked.connect(self.new_receive_address)
700 grid.addWidget(clear_button, 3, 2)
701 grid.setRowStretch(4, 1)
703 self.receive_qr = QRCodeWidget(fixedSize=200)
704 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
706 grid.setRowStretch(5, 1)
708 self.receive_requests_label = QLabel(_('Saved Requests'))
709 self.receive_list = MyTreeWidget(self)
710 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
711 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
712 self.receive_list.itemClicked.connect(self.receive_item_changed)
713 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
714 self.receive_list.setColumnWidth(0, 340)
715 h = self.receive_list.header()
716 h.setStretchLastSection(False)
717 h.setResizeMode(1, QHeaderView.Stretch)
719 grid.addWidget(self.receive_requests_label, 6, 0)
720 grid.addWidget(self.receive_list, 7, 0, 1, 6)
723 def receive_item_changed(self, item):
726 addr = str(item.text(0))
727 amount, message = self.receive_requests[addr]
728 self.receive_address_e.setText(addr)
729 self.receive_message_e.setText(message)
730 self.receive_amount_e.setAmount(amount)
733 def receive_list_delete(self, item):
734 addr = str(item.text(0))
735 self.receive_requests.pop(addr)
736 self.update_receive_tab()
737 self.clear_receive_tab()
739 def receive_list_menu(self, position):
740 item = self.receive_list.itemAt(position)
742 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
743 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
745 def save_payment_request(self):
746 addr = str(self.receive_address_e.text())
747 amount = self.receive_amount_e.get_amount()
748 message = str(self.receive_message_e.text())
749 if not message and not amount:
750 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
752 self.receive_requests = self.wallet.storage.get('receive_requests',{})
753 self.receive_requests[addr] = (amount, message)
754 self.wallet.storage.put('receive_requests', self.receive_requests)
755 self.update_receive_tab()
757 def new_receive_address(self):
758 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
760 if not self.wallet.history.get(addr) and addr not in self.receive_requests.keys():
763 if isinstance(self.wallet, Imported_Wallet):
764 self.show_message(_('No more addresses in your wallet.'))
766 if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
768 addr = self.wallet.create_new_address(self.current_account, False)
769 self.receive_address_e.setText(addr)
770 self.receive_message_e.setText('')
771 self.receive_amount_e.setAmount(None)
773 def clear_receive_tab(self):
774 self.receive_requests = self.wallet.storage.get('receive_requests',{})
775 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
777 if not self.wallet.history.get(addr) and addr not in self.receive_requests.keys():
781 self.receive_address_e.setText(addr)
782 self.receive_message_e.setText('')
783 self.receive_amount_e.setAmount(None)
785 def receive_at(self, addr):
786 if not bitcoin.is_address(addr):
788 self.tabs.setCurrentIndex(2)
789 self.receive_address_e.setText(addr)
791 def update_receive_tab(self):
792 self.receive_requests = self.wallet.storage.get('receive_requests',{})
793 b = len(self.receive_requests) > 0
794 self.receive_list.setVisible(b)
795 self.receive_requests_label.setVisible(b)
797 self.receive_list.clear()
798 for address, v in self.receive_requests.items():
800 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
801 item.setFont(0, QFont(MONOSPACE_FONT))
802 self.receive_list.addTopLevelItem(item)
805 def update_receive_qr(self):
806 import urlparse, urllib
807 addr = str(self.receive_address_e.text())
808 amount = self.receive_amount_e.get_amount()
809 message = unicode(self.receive_message_e.text()).encode('utf8')
810 self.save_request_button.setEnabled((amount is not None) or (message != ""))
814 query.append('amount=%s'%format_satoshis(amount))
816 query.append('message=%s'%urllib.quote(message))
817 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
818 url = urlparse.urlunparse(p)
821 self.receive_qr.setData(url)
822 run_hook('update_receive_qr', addr, amount, message, url)
825 def create_send_tab(self):
828 self.send_grid = grid = QGridLayout(w)
830 grid.setColumnMinimumWidth(3,300)
831 grid.setColumnStretch(5,1)
832 grid.setRowStretch(8, 1)
834 from paytoedit import PayToEdit
835 self.amount_e = BTCAmountEdit(self.get_decimal_point)
836 self.payto_e = PayToEdit(self)
837 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)'))
838 grid.addWidget(QLabel(_('Pay to')), 1, 0)
839 grid.addWidget(self.payto_e, 1, 1, 1, 3)
840 grid.addWidget(self.payto_help, 1, 4)
842 completer = QCompleter()
843 completer.setCaseSensitivity(False)
844 self.payto_e.setCompleter(completer)
845 completer.setModel(self.completions)
847 self.message_e = MyLineEdit()
848 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.'))
849 grid.addWidget(QLabel(_('Description')), 2, 0)
850 grid.addWidget(self.message_e, 2, 1, 1, 3)
851 grid.addWidget(self.message_help, 2, 4)
853 self.from_label = QLabel(_('From'))
854 grid.addWidget(self.from_label, 3, 0)
855 self.from_list = MyTreeWidget(self)
856 self.from_list.setColumnCount(2)
857 self.from_list.setColumnWidth(0, 350)
858 self.from_list.setColumnWidth(1, 50)
859 self.from_list.setHeaderHidden(True)
860 self.from_list.setMaximumHeight(80)
861 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
862 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
863 grid.addWidget(self.from_list, 3, 1, 1, 3)
864 self.set_pay_from([])
866 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
867 + _('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.') \
868 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
869 grid.addWidget(QLabel(_('Amount')), 4, 0)
870 grid.addWidget(self.amount_e, 4, 1, 1, 2)
871 grid.addWidget(self.amount_help, 4, 3)
873 self.fee_e = BTCAmountEdit(self.get_decimal_point)
874 grid.addWidget(QLabel(_('Fee')), 5, 0)
875 grid.addWidget(self.fee_e, 5, 1, 1, 2)
876 grid.addWidget(HelpButton(
877 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
878 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
879 + _('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)
881 self.send_button = EnterButton(_("Send"), self.do_send)
882 grid.addWidget(self.send_button, 6, 1)
884 b = EnterButton(_("Clear"), self.do_clear)
885 grid.addWidget(b, 6, 2)
887 self.payto_sig = QLabel('')
888 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
890 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
891 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
894 def entry_changed( is_fee ):
896 if self.amount_e.is_shortcut:
897 self.amount_e.is_shortcut = False
898 sendable = self.get_sendable_balance()
899 # there is only one output because we are completely spending inputs
900 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
901 fee = self.wallet.estimated_fee(inputs, 1)
903 self.amount_e.setAmount(amount)
904 self.amount_e.textEdited.emit("")
905 self.fee_e.setAmount(fee)
908 amount = self.amount_e.get_amount()
909 fee = self.fee_e.get_amount()
910 outputs = self.payto_e.get_outputs()
916 self.fee_e.setAmount(None)
917 not_enough_funds = False
919 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, len(outputs), coins = self.get_coins())
920 not_enough_funds = len(inputs) == 0
922 self.fee_e.setAmount(fee)
924 if not not_enough_funds:
926 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
930 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
931 text = _( "Not enough funds" )
932 c, u = self.wallet.get_frozen_balance()
933 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
935 self.statusBar().showMessage(text)
936 self.amount_e.setPalette(palette)
937 self.fee_e.setPalette(palette)
939 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
940 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
942 run_hook('create_send_tab', grid)
945 def from_list_delete(self, item):
946 i = self.from_list.indexOfTopLevelItem(item)
948 self.redraw_from_list()
950 def from_list_menu(self, position):
951 item = self.from_list.itemAt(position)
953 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
954 menu.exec_(self.from_list.viewport().mapToGlobal(position))
956 def set_pay_from(self, domain = None):
957 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
958 self.redraw_from_list()
960 def redraw_from_list(self):
961 self.from_list.clear()
962 self.from_label.setHidden(len(self.pay_from) == 0)
963 self.from_list.setHidden(len(self.pay_from) == 0)
966 h = x.get('prevout_hash')
967 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
969 for item in self.pay_from:
970 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
972 def update_completions(self):
974 for addr,label in self.wallet.labels.items():
975 if addr in self.wallet.addressbook:
976 l.append( label + ' <' + addr + '>')
978 run_hook('update_completions', l)
979 self.completions.setStringList(l)
983 return lambda s, *args: s.do_protect(func, args)
986 def read_send_tab(self):
988 if self.payment_request and self.payment_request.has_expired():
989 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
992 label = unicode( self.message_e.text() )
994 if self.payment_request:
995 outputs = self.payment_request.get_outputs()
997 outputs = self.payto_e.get_outputs()
1000 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
1003 for type, addr, amount in outputs:
1005 QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK'))
1007 if type == 'op_return':
1009 if type == 'address' and not bitcoin.is_address(addr):
1010 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
1013 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
1016 amount = sum(map(lambda x:x[2], outputs))
1018 fee = self.fee_e.get_amount()
1020 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
1023 confirm_amount = self.config.get('confirm_amount', 100000000)
1024 if amount >= confirm_amount:
1025 o = '\n'.join(map(lambda x:x[1], outputs))
1026 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
1029 confirm_fee = self.config.get('confirm_fee', 100000)
1030 if fee >= confirm_fee:
1031 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()}):
1034 coins = self.get_coins()
1035 return outputs, fee, label, coins
1039 r = self.read_send_tab()
1042 outputs, fee, label, coins = r
1043 self.send_tx(outputs, fee, label, coins)
1047 def send_tx(self, outputs, fee, label, coins, password):
1048 self.send_button.setDisabled(True)
1050 # first, create an unsigned tx
1052 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1054 except Exception as e:
1055 traceback.print_exc(file=sys.stdout)
1056 self.show_message(str(e))
1057 self.send_button.setDisabled(False)
1060 # call hook to see if plugin needs gui interaction
1061 run_hook('send_tx', tx)
1065 if self.wallet.is_watching_only():
1069 self.wallet.add_keypairs(tx, keypairs, password)
1070 self.wallet.sign_transaction(tx, keypairs, password)
1071 except Exception as e:
1072 traceback.print_exc(file=sys.stdout)
1078 self.show_message(tx.error)
1079 self.send_button.setDisabled(False)
1081 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1082 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1083 self.send_button.setDisabled(False)
1086 self.wallet.set_label(tx.hash(), label)
1088 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1089 self.show_transaction(tx)
1091 self.send_button.setDisabled(False)
1094 self.broadcast_transaction(tx)
1096 # keep a reference to WaitingDialog or the gui might crash
1097 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1098 self.waiting_dialog.start()
1102 def broadcast_transaction(self, tx):
1104 def broadcast_thread():
1105 pr = self.payment_request
1107 return self.wallet.sendtx(tx)
1109 if pr.has_expired():
1110 self.payment_request = None
1111 return False, _("Payment request has expired")
1113 status, msg = self.wallet.sendtx(tx)
1117 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1118 self.wallet.storage.put('invoices', self.invoices)
1119 self.update_invoices_tab()
1120 self.payment_request = None
1121 refund_address = self.wallet.addresses()[0]
1122 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1128 def broadcast_done(status, msg):
1130 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1133 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1134 self.send_button.setDisabled(False)
1136 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1137 self.waiting_dialog.start()
1141 def prepare_for_payment_request(self):
1142 self.tabs.setCurrentIndex(1)
1143 self.payto_e.is_pr = True
1144 for e in [self.payto_e, self.amount_e, self.message_e]:
1146 for h in [self.payto_help, self.amount_help, self.message_help]:
1148 self.payto_e.setText(_("please wait..."))
1151 def payment_request_ok(self):
1152 pr = self.payment_request
1154 if pr_id not in self.invoices:
1155 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1156 self.wallet.storage.put('invoices', self.invoices)
1157 self.update_invoices_tab()
1159 print_error('invoice already in list')
1161 status = self.invoices[pr_id][4]
1162 if status == PR_PAID:
1164 self.show_message("invoice already paid")
1165 self.payment_request = None
1168 self.payto_help.show()
1169 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1171 if not pr.has_expired():
1172 self.payto_e.setGreen()
1174 self.payto_e.setExpired()
1176 self.payto_e.setText(pr.domain)
1177 self.amount_e.setText(self.format_amount(pr.get_amount()))
1178 self.message_e.setText(pr.get_memo())
1180 def payment_request_error(self):
1182 self.show_message(self.payment_request.error)
1183 self.payment_request = None
1185 def pay_from_URI(self,URI):
1188 address, amount, label, message, request_url = util.parse_URI(URI)
1190 address, amount, label, message, request_url = util.parse_URI(URI)
1191 except Exception as e:
1192 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1195 self.tabs.setCurrentIndex(1)
1199 if self.wallet.labels.get(address) != label:
1200 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1201 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1202 self.wallet.addressbook.append(address)
1203 self.wallet.set_label(address, label)
1205 label = self.wallet.labels.get(address)
1207 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1209 self.message_e.setText(message)
1211 self.amount_e.setAmount(amount)
1214 from electrum import paymentrequest
1215 def payment_request():
1216 self.payment_request = paymentrequest.PaymentRequest(self.config)
1217 self.payment_request.read(request_url)
1218 if self.payment_request.verify():
1219 self.emit(SIGNAL('payment_request_ok'))
1221 self.emit(SIGNAL('payment_request_error'))
1223 self.pr_thread = threading.Thread(target=payment_request).start()
1224 self.prepare_for_payment_request()
1229 self.payto_e.is_pr = False
1230 self.payto_sig.setVisible(False)
1231 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1235 for h in [self.payto_help, self.amount_help, self.message_help]:
1238 self.payto_help.set_alt(None)
1239 self.set_pay_from([])
1240 self.update_status()
1244 def set_addrs_frozen(self,addrs,freeze):
1246 if not addr: continue
1247 if addr in self.wallet.frozen_addresses and not freeze:
1248 self.wallet.unfreeze(addr)
1249 elif addr not in self.wallet.frozen_addresses and freeze:
1250 self.wallet.freeze(addr)
1251 self.update_address_tab()
1255 def create_list_tab(self, headers):
1256 "generic tab creation method"
1257 l = MyTreeWidget(self)
1258 l.setColumnCount( len(headers) )
1259 l.setHeaderLabels( headers )
1262 vbox = QVBoxLayout()
1269 vbox.addWidget(buttons)
1274 def create_addresses_tab(self):
1275 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1276 for i,width in enumerate(self.column_widths['receive']):
1277 l.setColumnWidth(i, width)
1278 l.setContextMenuPolicy(Qt.CustomContextMenu)
1279 l.customContextMenuRequested.connect(self.create_receive_menu)
1280 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1281 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1282 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1283 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1284 self.address_list = l
1290 def save_column_widths(self):
1291 self.column_widths["receive"] = []
1292 for i in range(self.address_list.columnCount() -1):
1293 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1295 self.column_widths["history"] = []
1296 for i in range(self.history_list.columnCount() - 1):
1297 self.column_widths["history"].append(self.history_list.columnWidth(i))
1299 self.column_widths["contacts"] = []
1300 for i in range(self.contacts_list.columnCount() - 1):
1301 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1303 self.config.set_key("column_widths_2", self.column_widths, True)
1306 def create_contacts_tab(self):
1307 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1308 l.setContextMenuPolicy(Qt.CustomContextMenu)
1309 l.customContextMenuRequested.connect(self.create_contact_menu)
1310 for i,width in enumerate(self.column_widths['contacts']):
1311 l.setColumnWidth(i, width)
1312 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1313 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1314 self.contacts_list = l
1318 def create_invoices_tab(self):
1319 l, w = self.create_list_tab([_('Requestor'), _('Memo'), _('Date'), _('Amount'), _('Status')])
1320 l.setColumnWidth(0, 150)
1321 l.setColumnWidth(2, 150)
1322 l.setColumnWidth(3, 150)
1324 h.setStretchLastSection(False)
1325 h.setResizeMode(1, QHeaderView.Stretch)
1326 l.setContextMenuPolicy(Qt.CustomContextMenu)
1327 l.customContextMenuRequested.connect(self.create_invoice_menu)
1328 self.invoices_list = l
1331 def update_invoices_tab(self):
1332 invoices = self.wallet.storage.get('invoices', {})
1333 l = self.invoices_list
1335 for value in sorted(invoices.values(), key=lambda x: -x[3]):
1336 domain, memo, amount, expiration_date, status, tx_hash = value
1337 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1339 date_str = datetime.datetime.fromtimestamp(expiration_date).isoformat(' ')[:-3]
1340 item = QTreeWidgetItem( [ domain, memo, date_str, self.format_amount(amount, whitespaces=True), format_status(status)] )
1341 item.setFont(0, QFont(MONOSPACE_FONT))
1342 item.setFont(3, QFont(MONOSPACE_FONT))
1343 l.addTopLevelItem(item)
1344 l.setCurrentItem(l.topLevelItem(0))
1346 def delete_imported_key(self, addr):
1347 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1348 self.wallet.delete_imported_key(addr)
1349 self.update_address_tab()
1350 self.update_history_tab()
1352 def edit_account_label(self, k):
1353 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1355 label = unicode(text)
1356 self.wallet.set_label(k,label)
1357 self.update_address_tab()
1359 def account_set_expanded(self, item, k, b):
1361 self.accounts_expanded[k] = b
1363 def create_account_menu(self, position, k, item):
1365 if item.isExpanded():
1366 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1368 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1369 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1370 if self.wallet.seed_version > 4:
1371 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1372 if self.wallet.account_is_pending(k):
1373 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1374 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1376 def delete_pending_account(self, k):
1377 self.wallet.delete_pending_account(k)
1378 self.update_address_tab()
1380 def create_receive_menu(self, position):
1381 # fixme: this function apparently has a side effect.
1382 # if it is not called the menu pops up several times
1383 #self.address_list.selectedIndexes()
1385 selected = self.address_list.selectedItems()
1386 multi_select = len(selected) > 1
1387 addrs = [unicode(item.text(0)) for item in selected]
1388 if not multi_select:
1389 item = self.address_list.itemAt(position)
1393 if not is_valid(addr):
1394 k = str(item.data(0,32).toString())
1396 self.create_account_menu(position, k, item)
1398 item.setExpanded(not item.isExpanded())
1402 if not multi_select:
1403 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1404 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1405 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1406 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1407 if not self.wallet.is_watching_only():
1408 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1409 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1410 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1411 if self.wallet.is_imported(addr):
1412 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1414 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1415 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1416 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1417 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1420 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1421 if any(can_send(addr) for addr in addrs):
1422 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1424 run_hook('receive_menu', menu, addrs)
1425 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1428 def get_sendable_balance(self):
1429 return sum(map(lambda x:x['value'], self.get_coins()))
1432 def get_coins(self):
1434 return self.pay_from
1436 domain = self.wallet.get_account_addresses(self.current_account)
1437 for i in self.wallet.frozen_addresses:
1438 if i in domain: domain.remove(i)
1439 return self.wallet.get_unspent_coins(domain)
1442 def send_from_addresses(self, addrs):
1443 self.set_pay_from( addrs )
1444 self.tabs.setCurrentIndex(1)
1447 def payto(self, addr):
1449 label = self.wallet.labels.get(addr)
1450 m_addr = label + ' <' + addr + '>' if label else addr
1451 self.tabs.setCurrentIndex(1)
1452 self.payto_e.setText(m_addr)
1453 self.amount_e.setFocus()
1456 def delete_contact(self, x):
1457 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1458 self.wallet.delete_contact(x)
1459 self.wallet.set_label(x, None)
1460 self.update_history_tab()
1461 self.update_contacts_tab()
1462 self.update_completions()
1465 def create_contact_menu(self, position):
1466 item = self.contacts_list.itemAt(position)
1469 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1471 addr = unicode(item.text(0))
1472 label = unicode(item.text(1))
1473 is_editable = item.data(0,32).toBool()
1474 payto_addr = item.data(0,33).toString()
1475 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1476 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1477 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1479 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1480 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1482 run_hook('create_contact_menu', menu, item)
1483 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1485 def delete_invoice(self, key):
1486 self.invoices.pop(key)
1487 self.wallet.storage.put('invoices', self.invoices)
1488 self.update_invoices_tab()
1490 def show_invoice(self, key):
1491 from electrum.paymentrequest import PaymentRequest
1492 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1493 pr = PaymentRequest(self.config)
1497 self.show_pr_details(pr)
1499 def show_pr_details(self, pr):
1500 msg = 'Domain: ' + pr.domain
1501 msg += '\nStatus: ' + pr.get_status()
1502 msg += '\nMemo: ' + pr.get_memo()
1503 msg += '\nPayment URL: ' + pr.payment_url
1504 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1505 QMessageBox.information(self, 'Invoice', msg , 'OK')
1507 def do_pay_invoice(self, key):
1508 from electrum.paymentrequest import PaymentRequest
1509 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1510 pr = PaymentRequest(self.config)
1513 self.payment_request = pr
1514 self.prepare_for_payment_request()
1516 self.payment_request_ok()
1518 self.payment_request_error()
1521 def create_invoice_menu(self, position):
1522 item = self.invoices_list.itemAt(position)
1525 k = self.invoices_list.indexOfTopLevelItem(item)
1526 key = self.invoices.keys()[k]
1527 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1529 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1530 if status == PR_UNPAID:
1531 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1532 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1533 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1537 def update_address_tab(self):
1538 l = self.address_list
1539 # extend the syntax for consistency
1540 l.addChild = l.addTopLevelItem
1541 l.insertChild = l.insertTopLevelItem
1545 accounts = self.wallet.get_accounts()
1546 if self.current_account is None:
1547 account_items = sorted(accounts.items())
1549 account_items = [(self.current_account, accounts.get(self.current_account))]
1552 for k, account in account_items:
1554 if len(accounts) > 1:
1555 name = self.wallet.get_account_name(k)
1556 c,u = self.wallet.get_account_balance(k)
1557 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1558 l.addTopLevelItem(account_item)
1559 account_item.setExpanded(self.accounts_expanded.get(k, True))
1560 account_item.setData(0, 32, k)
1564 sequences = [0,1] if account.has_change() else [0]
1565 for is_change in sequences:
1566 if len(sequences) > 1:
1567 name = _("Receiving") if not is_change else _("Change")
1568 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1569 account_item.addChild(seq_item)
1571 seq_item.setExpanded(True)
1573 seq_item = account_item
1575 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1578 addr_list = account.get_addresses(is_change)
1579 for address in addr_list:
1580 num, is_used = self.wallet.is_used(address)
1581 label = self.wallet.labels.get(address,'')
1582 c, u = self.wallet.get_addr_balance(address)
1583 balance = self.format_amount(c + u)
1584 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1585 item.setFont(0, QFont(MONOSPACE_FONT))
1586 item.setData(0, 32, True) # label can be edited
1587 if address in self.wallet.frozen_addresses:
1588 item.setBackgroundColor(0, QColor('lightblue'))
1589 if self.wallet.is_beyond_limit(address, account, is_change):
1590 item.setBackgroundColor(0, QColor('red'))
1593 seq_item.insertChild(0, used_item)
1595 used_item.addChild(item)
1597 seq_item.addChild(item)
1599 # we use column 1 because column 0 may be hidden
1600 l.setCurrentItem(l.topLevelItem(0),1)
1603 def update_contacts_tab(self):
1604 l = self.contacts_list
1607 for address in self.wallet.addressbook:
1608 label = self.wallet.labels.get(address,'')
1609 n = self.wallet.get_num_tx(address)
1610 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1611 item.setFont(0, QFont(MONOSPACE_FONT))
1612 # 32 = label can be edited (bool)
1613 item.setData(0,32, True)
1615 item.setData(0,33, address)
1616 l.addTopLevelItem(item)
1618 run_hook('update_contacts_tab', l)
1619 l.setCurrentItem(l.topLevelItem(0))
1622 def create_console_tab(self):
1623 from console import Console
1624 self.console = console = Console()
1628 def update_console(self):
1629 console = self.console
1630 console.history = self.config.get("console-history",[])
1631 console.history_index = len(console.history)
1633 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1634 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1636 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1638 def mkfunc(f, method):
1639 return lambda *args: apply( f, (method, args, self.password_dialog ))
1641 if m[0]=='_' or m in ['network','wallet']: continue
1642 methods[m] = mkfunc(c._run, m)
1644 console.updateNamespace(methods)
1647 def change_account(self,s):
1648 if s == _("All accounts"):
1649 self.current_account = None
1651 accounts = self.wallet.get_account_names()
1652 for k, v in accounts.items():
1654 self.current_account = k
1655 self.update_history_tab()
1656 self.update_status()
1657 self.update_address_tab()
1658 self.update_receive_tab()
1660 def create_status_bar(self):
1663 sb.setFixedHeight(35)
1664 qtVersion = qVersion()
1666 self.balance_label = QLabel("")
1667 sb.addWidget(self.balance_label)
1669 from version_getter import UpdateLabel
1670 self.updatelabel = UpdateLabel(self.config, sb)
1672 self.account_selector = QComboBox()
1673 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1674 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1675 sb.addPermanentWidget(self.account_selector)
1677 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1678 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1680 self.lock_icon = QIcon()
1681 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1682 sb.addPermanentWidget( self.password_button )
1684 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1685 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1686 sb.addPermanentWidget( self.seed_button )
1687 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1688 sb.addPermanentWidget( self.status_button )
1690 run_hook('create_status_bar', (sb,))
1692 self.setStatusBar(sb)
1695 def update_lock_icon(self):
1696 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1697 self.password_button.setIcon( icon )
1700 def update_buttons_on_seed(self):
1701 if self.wallet.has_seed():
1702 self.seed_button.show()
1704 self.seed_button.hide()
1706 if not self.wallet.is_watching_only():
1707 self.password_button.show()
1708 self.send_button.setText(_("Send"))
1710 self.password_button.hide()
1711 self.send_button.setText(_("Create unsigned transaction"))
1714 def change_password_dialog(self):
1715 from password_dialog import PasswordDialog
1716 d = PasswordDialog(self.wallet, self)
1718 self.update_lock_icon()
1721 def new_contact_dialog(self):
1724 d.setWindowTitle(_("New Contact"))
1725 vbox = QVBoxLayout(d)
1726 vbox.addWidget(QLabel(_('New Contact')+':'))
1728 grid = QGridLayout()
1731 grid.addWidget(QLabel(_("Address")), 1, 0)
1732 grid.addWidget(line1, 1, 1)
1733 grid.addWidget(QLabel(_("Name")), 2, 0)
1734 grid.addWidget(line2, 2, 1)
1736 vbox.addLayout(grid)
1737 vbox.addLayout(ok_cancel_buttons(d))
1742 address = str(line1.text())
1743 label = unicode(line2.text())
1745 if not is_valid(address):
1746 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1749 self.wallet.add_contact(address)
1751 self.wallet.set_label(address, label)
1753 self.update_contacts_tab()
1754 self.update_history_tab()
1755 self.update_completions()
1756 self.tabs.setCurrentIndex(3)
1760 def new_account_dialog(self, password):
1762 dialog = QDialog(self)
1764 dialog.setWindowTitle(_("New Account"))
1766 vbox = QVBoxLayout()
1767 vbox.addWidget(QLabel(_('Account name')+':'))
1770 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1771 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1776 vbox.addLayout(ok_cancel_buttons(dialog))
1777 dialog.setLayout(vbox)
1781 name = str(e.text())
1784 self.wallet.create_pending_account(name, password)
1785 self.update_address_tab()
1786 self.tabs.setCurrentIndex(3)
1791 def show_master_public_keys(self):
1793 dialog = QDialog(self)
1795 dialog.setWindowTitle(_("Master Public Keys"))
1797 main_layout = QGridLayout()
1798 mpk_dict = self.wallet.get_master_public_keys()
1800 for key, value in mpk_dict.items():
1801 main_layout.addWidget(QLabel(key), i, 0)
1802 mpk_text = QTextEdit()
1803 mpk_text.setReadOnly(True)
1804 mpk_text.setMaximumHeight(170)
1805 mpk_text.setText(value)
1806 main_layout.addWidget(mpk_text, i + 1, 0)
1809 vbox = QVBoxLayout()
1810 vbox.addLayout(main_layout)
1811 vbox.addLayout(close_button(dialog))
1813 dialog.setLayout(vbox)
1818 def show_seed_dialog(self, password):
1819 if not self.wallet.has_seed():
1820 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1824 mnemonic = self.wallet.get_mnemonic(password)
1826 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1828 from seed_dialog import SeedDialog
1829 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1834 def show_qrcode(self, data, title = _("QR code")):
1837 d = QRDialog(data, self, title)
1841 def do_protect(self, func, args):
1842 if self.wallet.use_encryption:
1843 password = self.password_dialog()
1849 if args != (False,):
1850 args = (self,) + args + (password,)
1852 args = (self,password)
1856 def show_public_keys(self, address):
1857 if not address: return
1859 pubkey_list = self.wallet.get_public_keys(address)
1860 except Exception as e:
1861 traceback.print_exc(file=sys.stdout)
1862 self.show_message(str(e))
1866 d.setMinimumSize(600, 200)
1868 vbox = QVBoxLayout()
1869 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1870 vbox.addWidget( QLabel(_("Public key") + ':'))
1872 keys.setReadOnly(True)
1873 keys.setText('\n'.join(pubkey_list))
1874 vbox.addWidget(keys)
1875 vbox.addLayout(close_button(d))
1880 def show_private_key(self, address, password):
1881 if not address: return
1883 pk_list = self.wallet.get_private_key(address, password)
1884 except Exception as e:
1885 traceback.print_exc(file=sys.stdout)
1886 self.show_message(str(e))
1890 d.setMinimumSize(600, 200)
1892 vbox = QVBoxLayout()
1893 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1894 vbox.addWidget( QLabel(_("Private key") + ':'))
1896 keys.setReadOnly(True)
1897 keys.setText('\n'.join(pk_list))
1898 vbox.addWidget(keys)
1899 vbox.addLayout(close_button(d))
1905 def do_sign(self, address, message, signature, password):
1906 message = unicode(message.toPlainText())
1907 message = message.encode('utf-8')
1909 sig = self.wallet.sign_message(str(address.text()), message, password)
1910 signature.setText(sig)
1911 except Exception as e:
1912 self.show_message(str(e))
1914 def do_verify(self, address, message, signature):
1915 message = unicode(message.toPlainText())
1916 message = message.encode('utf-8')
1917 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1918 self.show_message(_("Signature verified"))
1920 self.show_message(_("Error: wrong signature"))
1923 def sign_verify_message(self, address=''):
1926 d.setWindowTitle(_('Sign/verify Message'))
1927 d.setMinimumSize(410, 290)
1929 layout = QGridLayout(d)
1931 message_e = QTextEdit()
1932 layout.addWidget(QLabel(_('Message')), 1, 0)
1933 layout.addWidget(message_e, 1, 1)
1934 layout.setRowStretch(2,3)
1936 address_e = QLineEdit()
1937 address_e.setText(address)
1938 layout.addWidget(QLabel(_('Address')), 2, 0)
1939 layout.addWidget(address_e, 2, 1)
1941 signature_e = QTextEdit()
1942 layout.addWidget(QLabel(_('Signature')), 3, 0)
1943 layout.addWidget(signature_e, 3, 1)
1944 layout.setRowStretch(3,1)
1946 hbox = QHBoxLayout()
1948 b = QPushButton(_("Sign"))
1949 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1952 b = QPushButton(_("Verify"))
1953 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1956 b = QPushButton(_("Close"))
1957 b.clicked.connect(d.accept)
1959 layout.addLayout(hbox, 4, 1)
1964 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1966 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1967 message_e.setText(decrypted)
1968 except Exception as e:
1969 self.show_message(str(e))
1972 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1973 message = unicode(message_e.toPlainText())
1974 message = message.encode('utf-8')
1976 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1977 encrypted_e.setText(encrypted)
1978 except Exception as e:
1979 self.show_message(str(e))
1983 def encrypt_message(self, address = ''):
1986 d.setWindowTitle(_('Encrypt/decrypt Message'))
1987 d.setMinimumSize(610, 490)
1989 layout = QGridLayout(d)
1991 message_e = QTextEdit()
1992 layout.addWidget(QLabel(_('Message')), 1, 0)
1993 layout.addWidget(message_e, 1, 1)
1994 layout.setRowStretch(2,3)
1996 pubkey_e = QLineEdit()
1998 pubkey = self.wallet.get_public_keys(address)[0]
1999 pubkey_e.setText(pubkey)
2000 layout.addWidget(QLabel(_('Public key')), 2, 0)
2001 layout.addWidget(pubkey_e, 2, 1)
2003 encrypted_e = QTextEdit()
2004 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
2005 layout.addWidget(encrypted_e, 3, 1)
2006 layout.setRowStretch(3,1)
2008 hbox = QHBoxLayout()
2009 b = QPushButton(_("Encrypt"))
2010 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2013 b = QPushButton(_("Decrypt"))
2014 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2017 b = QPushButton(_("Close"))
2018 b.clicked.connect(d.accept)
2021 layout.addLayout(hbox, 4, 1)
2025 def question(self, msg):
2026 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2028 def show_message(self, msg):
2029 QMessageBox.information(self, _('Message'), msg, _('OK'))
2031 def password_dialog(self, msg=None):
2034 d.setWindowTitle(_("Enter Password"))
2039 vbox = QVBoxLayout()
2041 msg = _('Please enter your password')
2042 vbox.addWidget(QLabel(msg))
2044 grid = QGridLayout()
2046 grid.addWidget(QLabel(_('Password')), 1, 0)
2047 grid.addWidget(pw, 1, 1)
2048 vbox.addLayout(grid)
2050 vbox.addLayout(ok_cancel_buttons(d))
2053 run_hook('password_dialog', pw, grid, 1)
2054 if not d.exec_(): return
2055 return unicode(pw.text())
2064 def tx_from_text(self, txt):
2065 "json or raw hexadecimal"
2074 return Transaction.deserialize(txt)
2076 traceback.print_exc(file=sys.stdout)
2077 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2081 tx_dict = json.loads(str(txt))
2082 assert "hex" in tx_dict.keys()
2083 tx = Transaction.deserialize(tx_dict["hex"])
2084 #if tx_dict.has_key("input_info"):
2085 # input_info = json.loads(tx_dict['input_info'])
2086 # tx.add_input_info(input_info)
2089 traceback.print_exc(file=sys.stdout)
2090 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2093 def read_tx_from_qrcode(self):
2094 data = run_hook('scan_qr_hook')
2097 # transactions are binary, but qrcode seems to return utf8...
2098 z = data.decode('utf8')
2102 data = s.encode('hex')
2103 tx = self.tx_from_text(data)
2106 self.show_transaction(tx)
2109 def read_tx_from_file(self):
2110 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2114 with open(fileName, "r") as f:
2115 file_content = f.read()
2116 except (ValueError, IOError, os.error), reason:
2117 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2119 return self.tx_from_text(file_content)
2123 def sign_raw_transaction(self, tx, password):
2125 self.wallet.signrawtransaction(tx, [], password)
2126 except Exception as e:
2127 traceback.print_exc(file=sys.stdout)
2128 QMessageBox.warning(self, _("Error"), str(e))
2130 def do_process_from_text(self):
2131 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2134 tx = self.tx_from_text(text)
2136 self.show_transaction(tx)
2138 def do_process_from_file(self):
2139 tx = self.read_tx_from_file()
2141 self.show_transaction(tx)
2143 def do_process_from_txid(self):
2144 from electrum import transaction
2145 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2147 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2149 tx = transaction.Transaction.deserialize(r)
2151 self.show_transaction(tx)
2153 self.show_message("unknown transaction")
2155 def do_process_from_csvReader(self, csvReader):
2160 for position, row in enumerate(csvReader):
2162 if not is_address(address):
2163 errors.append((position, address))
2165 amount = Decimal(row[1])
2166 amount = int(100000000*amount)
2167 outputs.append(('address', address, amount))
2168 except (ValueError, IOError, os.error), reason:
2169 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2173 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2174 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2178 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2179 except Exception as e:
2180 self.show_message(str(e))
2183 self.show_transaction(tx)
2185 def do_process_from_csv_file(self):
2186 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2190 with open(fileName, "r") as f:
2191 csvReader = csv.reader(f)
2192 self.do_process_from_csvReader(csvReader)
2193 except (ValueError, IOError, os.error), reason:
2194 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2197 def do_process_from_csv_text(self):
2198 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2199 + _("Format: address, amount. One output per line"), _("Load CSV"))
2202 f = StringIO.StringIO(text)
2203 csvReader = csv.reader(f)
2204 self.do_process_from_csvReader(csvReader)
2209 def export_privkeys_dialog(self, password):
2210 if self.wallet.is_watching_only():
2211 self.show_message(_("This is a watching-only wallet"))
2215 d.setWindowTitle(_('Private keys'))
2216 d.setMinimumSize(850, 300)
2217 vbox = QVBoxLayout(d)
2219 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2220 _("Exposing a single private key can compromise your entire wallet!"),
2221 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2222 vbox.addWidget(QLabel(msg))
2228 defaultname = 'electrum-private-keys.csv'
2229 select_msg = _('Select file to export your private keys to')
2230 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2231 vbox.addLayout(hbox)
2233 h, b = ok_cancel_buttons2(d, _('Export'))
2238 addresses = self.wallet.addresses(True)
2240 def privkeys_thread():
2241 for addr in addresses:
2245 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2246 d.emit(SIGNAL('computing_privkeys'))
2247 d.emit(SIGNAL('show_privkeys'))
2249 def show_privkeys():
2250 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2254 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2255 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2256 threading.Thread(target=privkeys_thread).start()
2262 filename = filename_e.text()
2267 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2268 except (IOError, os.error), reason:
2269 export_error_label = _("Electrum was unable to produce a private key-export.")
2270 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2272 except Exception as e:
2273 self.show_message(str(e))
2276 self.show_message(_("Private keys exported."))
2279 def do_export_privkeys(self, fileName, pklist, is_csv):
2280 with open(fileName, "w+") as f:
2282 transaction = csv.writer(f)
2283 transaction.writerow(["address", "private_key"])
2284 for addr, pk in pklist.items():
2285 transaction.writerow(["%34s"%addr,pk])
2288 f.write(json.dumps(pklist, indent = 4))
2291 def do_import_labels(self):
2292 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2293 if not labelsFile: return
2295 f = open(labelsFile, 'r')
2298 for key, value in json.loads(data).items():
2299 self.wallet.set_label(key, value)
2300 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2301 except (IOError, os.error), reason:
2302 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2305 def do_export_labels(self):
2306 labels = self.wallet.labels
2308 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2310 with open(fileName, 'w+') as f:
2311 json.dump(labels, f)
2312 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2313 except (IOError, os.error), reason:
2314 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2317 def export_history_dialog(self):
2320 d.setWindowTitle(_('Export History'))
2321 d.setMinimumSize(400, 200)
2322 vbox = QVBoxLayout(d)
2324 defaultname = os.path.expanduser('~/electrum-history.csv')
2325 select_msg = _('Select file to export your wallet transactions to')
2327 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2328 vbox.addLayout(hbox)
2332 h, b = ok_cancel_buttons2(d, _('Export'))
2337 filename = filename_e.text()
2342 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2343 except (IOError, os.error), reason:
2344 export_error_label = _("Electrum was unable to produce a transaction export.")
2345 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2348 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2351 def do_export_history(self, wallet, fileName, is_csv):
2352 history = wallet.get_tx_history()
2354 for item in history:
2355 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2357 if timestamp is not None:
2359 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2360 except [RuntimeError, TypeError, NameError] as reason:
2361 time_string = "unknown"
2364 time_string = "unknown"
2366 time_string = "pending"
2368 if value is not None:
2369 value_string = format_satoshis(value, True)
2374 fee_string = format_satoshis(fee, True)
2379 label, is_default_label = wallet.get_label(tx_hash)
2380 label = label.encode('utf-8')
2384 balance_string = format_satoshis(balance, False)
2386 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2388 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2390 with open(fileName, "w+") as f:
2392 transaction = csv.writer(f)
2393 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2395 transaction.writerow(line)
2398 f.write(json.dumps(lines, indent = 4))
2401 def sweep_key_dialog(self):
2403 d.setWindowTitle(_('Sweep private keys'))
2404 d.setMinimumSize(600, 300)
2406 vbox = QVBoxLayout(d)
2407 vbox.addWidget(QLabel(_("Enter private keys")))
2409 keys_e = QTextEdit()
2410 keys_e.setTabChangesFocus(True)
2411 vbox.addWidget(keys_e)
2413 h, address_e = address_field(self.wallet.addresses())
2417 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2418 vbox.addLayout(hbox)
2419 button.setEnabled(False)
2422 addr = str(address_e.text())
2423 if bitcoin.is_address(addr):
2427 pk = str(keys_e.toPlainText()).strip()
2428 if Wallet.is_private_key(pk):
2431 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2432 keys_e.textChanged.connect(f)
2433 address_e.textChanged.connect(f)
2437 fee = self.wallet.fee
2438 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2439 self.show_transaction(tx)
2443 def do_import_privkey(self, password):
2444 if not self.wallet.has_imported_keys():
2445 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2446 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2447 + _('Are you sure you understand what you are doing?'), 3, 4)
2450 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2453 text = str(text).split()
2458 addr = self.wallet.import_key(key, password)
2459 except Exception as e:
2465 addrlist.append(addr)
2467 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2469 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2470 self.update_address_tab()
2471 self.update_history_tab()
2474 def settings_dialog(self):
2476 d.setWindowTitle(_('Electrum Settings'))
2478 vbox = QVBoxLayout()
2479 grid = QGridLayout()
2480 grid.setColumnStretch(0,1)
2482 nz_label = QLabel(_('Display zeros') + ':')
2483 grid.addWidget(nz_label, 0, 0)
2484 nz_e = AmountEdit(None,True)
2485 nz_e.setText("%d"% self.num_zeros)
2486 grid.addWidget(nz_e, 0, 1)
2487 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2488 grid.addWidget(HelpButton(msg), 0, 2)
2489 if not self.config.is_modifiable('num_zeros'):
2490 for w in [nz_e, nz_label]: w.setEnabled(False)
2492 lang_label=QLabel(_('Language') + ':')
2493 grid.addWidget(lang_label, 1, 0)
2494 lang_combo = QComboBox()
2495 from electrum.i18n import languages
2496 lang_combo.addItems(languages.values())
2498 index = languages.keys().index(self.config.get("language",''))
2501 lang_combo.setCurrentIndex(index)
2502 grid.addWidget(lang_combo, 1, 1)
2503 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2504 if not self.config.is_modifiable('language'):
2505 for w in [lang_combo, lang_label]: w.setEnabled(False)
2508 fee_label = QLabel(_('Transaction fee') + ':')
2509 grid.addWidget(fee_label, 2, 0)
2510 fee_e = BTCAmountEdit(self.get_decimal_point)
2511 fee_e.setAmount(self.wallet.fee)
2512 grid.addWidget(fee_e, 2, 1)
2513 msg = _('Fee per kilobyte of transaction.') + '\n' \
2514 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2515 grid.addWidget(HelpButton(msg), 2, 2)
2516 if not self.config.is_modifiable('fee_per_kb'):
2517 for w in [fee_e, fee_label]: w.setEnabled(False)
2519 units = ['BTC', 'mBTC', 'bits']
2520 unit_label = QLabel(_('Base unit') + ':')
2521 grid.addWidget(unit_label, 3, 0)
2522 unit_combo = QComboBox()
2523 unit_combo.addItems(units)
2524 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2525 grid.addWidget(unit_combo, 3, 1)
2526 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2527 + '\n1BTC=1000mBTC.\n' \
2528 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2530 usechange_cb = QCheckBox(_('Use change addresses'))
2531 usechange_cb.setChecked(self.wallet.use_change)
2532 grid.addWidget(usechange_cb, 4, 0)
2533 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2534 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2536 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2537 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2538 grid.addWidget(block_ex_label, 5, 0)
2539 block_ex_combo = QComboBox()
2540 block_ex_combo.addItems(block_explorers)
2541 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2542 grid.addWidget(block_ex_combo, 5, 1)
2543 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2545 show_tx = self.config.get('show_before_broadcast', False)
2546 showtx_cb = QCheckBox(_('Show before broadcast'))
2547 showtx_cb.setChecked(show_tx)
2548 grid.addWidget(showtx_cb, 6, 0)
2549 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2551 vbox.addLayout(grid)
2553 vbox.addLayout(ok_cancel_buttons(d))
2557 if not d.exec_(): return
2559 fee = fee_e.get_amount()
2561 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2564 self.wallet.set_fee(fee)
2566 nz = unicode(nz_e.text())
2571 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2574 if self.num_zeros != nz:
2576 self.config.set_key('num_zeros', nz, True)
2577 self.update_history_tab()
2578 self.update_address_tab()
2580 usechange_result = usechange_cb.isChecked()
2581 if self.wallet.use_change != usechange_result:
2582 self.wallet.use_change = usechange_result
2583 self.wallet.storage.put('use_change', self.wallet.use_change)
2585 if showtx_cb.isChecked() != show_tx:
2586 self.config.set_key('show_before_broadcast', not show_tx)
2588 unit_result = units[unit_combo.currentIndex()]
2589 if self.base_unit() != unit_result:
2590 if unit_result == 'BTC':
2591 self.decimal_point = 8
2592 elif unit_result == 'mBTC':
2593 self.decimal_point = 5
2594 elif unit_result == 'bits':
2595 self.decimal_point = 2
2597 raise Exception('Unknown base unit')
2598 self.config.set_key('decimal_point', self.decimal_point, True)
2599 self.update_history_tab()
2600 self.update_status()
2602 need_restart = False
2604 lang_request = languages.keys()[lang_combo.currentIndex()]
2605 if lang_request != self.config.get('language'):
2606 self.config.set_key("language", lang_request, True)
2609 be_result = block_explorers[block_ex_combo.currentIndex()]
2610 self.config.set_key('block_explorer', be_result, True)
2612 run_hook('close_settings_dialog')
2615 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2618 def run_network_dialog(self):
2619 if not self.network:
2621 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2623 def closeEvent(self, event):
2625 self.config.set_key("is_maximized", self.isMaximized())
2626 if not self.isMaximized():
2628 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2629 self.save_column_widths()
2630 self.config.set_key("console-history", self.console.history[-50:], True)
2631 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2635 def plugins_dialog(self):
2636 from electrum.plugins import plugins
2639 d.setWindowTitle(_('Electrum Plugins'))
2642 vbox = QVBoxLayout(d)
2645 scroll = QScrollArea()
2646 scroll.setEnabled(True)
2647 scroll.setWidgetResizable(True)
2648 scroll.setMinimumSize(400,250)
2649 vbox.addWidget(scroll)
2653 w.setMinimumHeight(len(plugins)*35)
2655 grid = QGridLayout()
2656 grid.setColumnStretch(0,1)
2659 def do_toggle(cb, p, w):
2662 if w: w.setEnabled(r)
2664 def mk_toggle(cb, p, w):
2665 return lambda: do_toggle(cb,p,w)
2667 for i, p in enumerate(plugins):
2669 cb = QCheckBox(p.fullname())
2670 cb.setDisabled(not p.is_available())
2671 cb.setChecked(p.is_enabled())
2672 grid.addWidget(cb, i, 0)
2673 if p.requires_settings():
2674 w = p.settings_widget(self)
2675 w.setEnabled( p.is_enabled() )
2676 grid.addWidget(w, i, 1)
2679 cb.clicked.connect(mk_toggle(cb,p,w))
2680 grid.addWidget(HelpButton(p.description()), i, 2)
2682 print_msg(_("Error: cannot display plugin"), p)
2683 traceback.print_exc(file=sys.stdout)
2684 grid.setRowStretch(i+1,1)
2686 vbox.addLayout(close_button(d))
2691 def show_account_details(self, k):
2692 account = self.wallet.accounts[k]
2695 d.setWindowTitle(_('Account Details'))
2698 vbox = QVBoxLayout(d)
2699 name = self.wallet.get_account_name(k)
2700 label = QLabel('Name: ' + name)
2701 vbox.addWidget(label)
2703 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2705 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2707 vbox.addWidget(QLabel(_('Master Public Key:')))
2710 text.setReadOnly(True)
2711 text.setMaximumHeight(170)
2712 vbox.addWidget(text)
2714 mpk_text = '\n'.join( account.get_master_pubkeys() )
2715 text.setText(mpk_text)
2717 vbox.addLayout(close_button(d))