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'),_('Amount'), _('Status')])
1320 l.setColumnWidth(0, 150)
1322 h.setStretchLastSection(False)
1323 h.setResizeMode(1, QHeaderView.Stretch)
1324 l.setContextMenuPolicy(Qt.CustomContextMenu)
1325 l.customContextMenuRequested.connect(self.create_invoice_menu)
1326 self.invoices_list = l
1329 def update_invoices_tab(self):
1330 invoices = self.wallet.storage.get('invoices', {})
1331 l = self.invoices_list
1333 for key, value in invoices.items():
1335 domain, memo, amount, expiration_date, status, tx_hash = value
1339 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1341 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1342 l.addTopLevelItem(item)
1344 l.setCurrentItem(l.topLevelItem(0))
1348 def delete_imported_key(self, addr):
1349 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1350 self.wallet.delete_imported_key(addr)
1351 self.update_address_tab()
1352 self.update_history_tab()
1354 def edit_account_label(self, k):
1355 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1357 label = unicode(text)
1358 self.wallet.set_label(k,label)
1359 self.update_address_tab()
1361 def account_set_expanded(self, item, k, b):
1363 self.accounts_expanded[k] = b
1365 def create_account_menu(self, position, k, item):
1367 if item.isExpanded():
1368 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1370 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1371 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1372 if self.wallet.seed_version > 4:
1373 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1374 if self.wallet.account_is_pending(k):
1375 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1376 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1378 def delete_pending_account(self, k):
1379 self.wallet.delete_pending_account(k)
1380 self.update_address_tab()
1382 def create_receive_menu(self, position):
1383 # fixme: this function apparently has a side effect.
1384 # if it is not called the menu pops up several times
1385 #self.address_list.selectedIndexes()
1387 selected = self.address_list.selectedItems()
1388 multi_select = len(selected) > 1
1389 addrs = [unicode(item.text(0)) for item in selected]
1390 if not multi_select:
1391 item = self.address_list.itemAt(position)
1395 if not is_valid(addr):
1396 k = str(item.data(0,32).toString())
1398 self.create_account_menu(position, k, item)
1400 item.setExpanded(not item.isExpanded())
1404 if not multi_select:
1405 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1406 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1407 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1408 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1409 if not self.wallet.is_watching_only():
1410 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1411 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1412 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1413 if self.wallet.is_imported(addr):
1414 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1416 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1417 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1418 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1419 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1422 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1423 if any(can_send(addr) for addr in addrs):
1424 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1426 run_hook('receive_menu', menu, addrs)
1427 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1430 def get_sendable_balance(self):
1431 return sum(map(lambda x:x['value'], self.get_coins()))
1434 def get_coins(self):
1436 return self.pay_from
1438 domain = self.wallet.get_account_addresses(self.current_account)
1439 for i in self.wallet.frozen_addresses:
1440 if i in domain: domain.remove(i)
1441 return self.wallet.get_unspent_coins(domain)
1444 def send_from_addresses(self, addrs):
1445 self.set_pay_from( addrs )
1446 self.tabs.setCurrentIndex(1)
1449 def payto(self, addr):
1451 label = self.wallet.labels.get(addr)
1452 m_addr = label + ' <' + addr + '>' if label else addr
1453 self.tabs.setCurrentIndex(1)
1454 self.payto_e.setText(m_addr)
1455 self.amount_e.setFocus()
1458 def delete_contact(self, x):
1459 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1460 self.wallet.delete_contact(x)
1461 self.wallet.set_label(x, None)
1462 self.update_history_tab()
1463 self.update_contacts_tab()
1464 self.update_completions()
1467 def create_contact_menu(self, position):
1468 item = self.contacts_list.itemAt(position)
1471 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1473 addr = unicode(item.text(0))
1474 label = unicode(item.text(1))
1475 is_editable = item.data(0,32).toBool()
1476 payto_addr = item.data(0,33).toString()
1477 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1478 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1479 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1481 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1482 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1484 run_hook('create_contact_menu', menu, item)
1485 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1487 def delete_invoice(self, key):
1488 self.invoices.pop(key)
1489 self.wallet.storage.put('invoices', self.invoices)
1490 self.update_invoices_tab()
1492 def show_invoice(self, key):
1493 from electrum.paymentrequest import PaymentRequest
1494 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1495 pr = PaymentRequest(self.config)
1499 self.show_pr_details(pr)
1501 def show_pr_details(self, pr):
1502 msg = 'Domain: ' + pr.domain
1503 msg += '\nStatus: ' + pr.get_status()
1504 msg += '\nMemo: ' + pr.get_memo()
1505 msg += '\nPayment URL: ' + pr.payment_url
1506 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1507 QMessageBox.information(self, 'Invoice', msg , 'OK')
1509 def do_pay_invoice(self, key):
1510 from electrum.paymentrequest import PaymentRequest
1511 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1512 pr = PaymentRequest(self.config)
1515 self.payment_request = pr
1516 self.prepare_for_payment_request()
1518 self.payment_request_ok()
1520 self.payment_request_error()
1523 def create_invoice_menu(self, position):
1524 item = self.invoices_list.itemAt(position)
1527 k = self.invoices_list.indexOfTopLevelItem(item)
1528 key = self.invoices.keys()[k]
1529 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1531 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1532 if status == PR_UNPAID:
1533 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1534 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1535 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1539 def update_address_tab(self):
1540 l = self.address_list
1541 # extend the syntax for consistency
1542 l.addChild = l.addTopLevelItem
1543 l.insertChild = l.insertTopLevelItem
1547 accounts = self.wallet.get_accounts()
1548 if self.current_account is None:
1549 account_items = sorted(accounts.items())
1551 account_items = [(self.current_account, accounts.get(self.current_account))]
1554 for k, account in account_items:
1556 if len(accounts) > 1:
1557 name = self.wallet.get_account_name(k)
1558 c,u = self.wallet.get_account_balance(k)
1559 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1560 l.addTopLevelItem(account_item)
1561 account_item.setExpanded(self.accounts_expanded.get(k, True))
1562 account_item.setData(0, 32, k)
1566 sequences = [0,1] if account.has_change() else [0]
1567 for is_change in sequences:
1568 if len(sequences) > 1:
1569 name = _("Receiving") if not is_change else _("Change")
1570 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1571 account_item.addChild(seq_item)
1573 seq_item.setExpanded(True)
1575 seq_item = account_item
1577 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1580 addr_list = account.get_addresses(is_change)
1581 for address in addr_list:
1582 num, is_used = self.wallet.is_used(address)
1583 label = self.wallet.labels.get(address,'')
1584 c, u = self.wallet.get_addr_balance(address)
1585 balance = self.format_amount(c + u)
1586 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1587 item.setFont(0, QFont(MONOSPACE_FONT))
1588 item.setData(0, 32, True) # label can be edited
1589 if address in self.wallet.frozen_addresses:
1590 item.setBackgroundColor(0, QColor('lightblue'))
1591 if self.wallet.is_beyond_limit(address, account, is_change):
1592 item.setBackgroundColor(0, QColor('red'))
1595 seq_item.insertChild(0, used_item)
1597 used_item.addChild(item)
1599 seq_item.addChild(item)
1601 # we use column 1 because column 0 may be hidden
1602 l.setCurrentItem(l.topLevelItem(0),1)
1605 def update_contacts_tab(self):
1606 l = self.contacts_list
1609 for address in self.wallet.addressbook:
1610 label = self.wallet.labels.get(address,'')
1611 n = self.wallet.get_num_tx(address)
1612 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1613 item.setFont(0, QFont(MONOSPACE_FONT))
1614 # 32 = label can be edited (bool)
1615 item.setData(0,32, True)
1617 item.setData(0,33, address)
1618 l.addTopLevelItem(item)
1620 run_hook('update_contacts_tab', l)
1621 l.setCurrentItem(l.topLevelItem(0))
1624 def create_console_tab(self):
1625 from console import Console
1626 self.console = console = Console()
1630 def update_console(self):
1631 console = self.console
1632 console.history = self.config.get("console-history",[])
1633 console.history_index = len(console.history)
1635 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1636 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1638 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1640 def mkfunc(f, method):
1641 return lambda *args: apply( f, (method, args, self.password_dialog ))
1643 if m[0]=='_' or m in ['network','wallet']: continue
1644 methods[m] = mkfunc(c._run, m)
1646 console.updateNamespace(methods)
1649 def change_account(self,s):
1650 if s == _("All accounts"):
1651 self.current_account = None
1653 accounts = self.wallet.get_account_names()
1654 for k, v in accounts.items():
1656 self.current_account = k
1657 self.update_history_tab()
1658 self.update_status()
1659 self.update_address_tab()
1660 self.update_receive_tab()
1662 def create_status_bar(self):
1665 sb.setFixedHeight(35)
1666 qtVersion = qVersion()
1668 self.balance_label = QLabel("")
1669 sb.addWidget(self.balance_label)
1671 from version_getter import UpdateLabel
1672 self.updatelabel = UpdateLabel(self.config, sb)
1674 self.account_selector = QComboBox()
1675 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1676 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1677 sb.addPermanentWidget(self.account_selector)
1679 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1680 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1682 self.lock_icon = QIcon()
1683 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1684 sb.addPermanentWidget( self.password_button )
1686 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1687 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1688 sb.addPermanentWidget( self.seed_button )
1689 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1690 sb.addPermanentWidget( self.status_button )
1692 run_hook('create_status_bar', (sb,))
1694 self.setStatusBar(sb)
1697 def update_lock_icon(self):
1698 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1699 self.password_button.setIcon( icon )
1702 def update_buttons_on_seed(self):
1703 if self.wallet.has_seed():
1704 self.seed_button.show()
1706 self.seed_button.hide()
1708 if not self.wallet.is_watching_only():
1709 self.password_button.show()
1710 self.send_button.setText(_("Send"))
1712 self.password_button.hide()
1713 self.send_button.setText(_("Create unsigned transaction"))
1716 def change_password_dialog(self):
1717 from password_dialog import PasswordDialog
1718 d = PasswordDialog(self.wallet, self)
1720 self.update_lock_icon()
1723 def new_contact_dialog(self):
1726 d.setWindowTitle(_("New Contact"))
1727 vbox = QVBoxLayout(d)
1728 vbox.addWidget(QLabel(_('New Contact')+':'))
1730 grid = QGridLayout()
1733 grid.addWidget(QLabel(_("Address")), 1, 0)
1734 grid.addWidget(line1, 1, 1)
1735 grid.addWidget(QLabel(_("Name")), 2, 0)
1736 grid.addWidget(line2, 2, 1)
1738 vbox.addLayout(grid)
1739 vbox.addLayout(ok_cancel_buttons(d))
1744 address = str(line1.text())
1745 label = unicode(line2.text())
1747 if not is_valid(address):
1748 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1751 self.wallet.add_contact(address)
1753 self.wallet.set_label(address, label)
1755 self.update_contacts_tab()
1756 self.update_history_tab()
1757 self.update_completions()
1758 self.tabs.setCurrentIndex(3)
1762 def new_account_dialog(self, password):
1764 dialog = QDialog(self)
1766 dialog.setWindowTitle(_("New Account"))
1768 vbox = QVBoxLayout()
1769 vbox.addWidget(QLabel(_('Account name')+':'))
1772 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1773 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1778 vbox.addLayout(ok_cancel_buttons(dialog))
1779 dialog.setLayout(vbox)
1783 name = str(e.text())
1786 self.wallet.create_pending_account(name, password)
1787 self.update_address_tab()
1788 self.tabs.setCurrentIndex(3)
1793 def show_master_public_keys(self):
1795 dialog = QDialog(self)
1797 dialog.setWindowTitle(_("Master Public Keys"))
1799 main_layout = QGridLayout()
1800 mpk_dict = self.wallet.get_master_public_keys()
1802 for key, value in mpk_dict.items():
1803 main_layout.addWidget(QLabel(key), i, 0)
1804 mpk_text = QTextEdit()
1805 mpk_text.setReadOnly(True)
1806 mpk_text.setMaximumHeight(170)
1807 mpk_text.setText(value)
1808 main_layout.addWidget(mpk_text, i + 1, 0)
1811 vbox = QVBoxLayout()
1812 vbox.addLayout(main_layout)
1813 vbox.addLayout(close_button(dialog))
1815 dialog.setLayout(vbox)
1820 def show_seed_dialog(self, password):
1821 if not self.wallet.has_seed():
1822 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1826 mnemonic = self.wallet.get_mnemonic(password)
1828 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1830 from seed_dialog import SeedDialog
1831 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1836 def show_qrcode(self, data, title = _("QR code")):
1839 d = QRDialog(data, self, title)
1843 def do_protect(self, func, args):
1844 if self.wallet.use_encryption:
1845 password = self.password_dialog()
1851 if args != (False,):
1852 args = (self,) + args + (password,)
1854 args = (self,password)
1858 def show_public_keys(self, address):
1859 if not address: return
1861 pubkey_list = self.wallet.get_public_keys(address)
1862 except Exception as e:
1863 traceback.print_exc(file=sys.stdout)
1864 self.show_message(str(e))
1868 d.setMinimumSize(600, 200)
1870 vbox = QVBoxLayout()
1871 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1872 vbox.addWidget( QLabel(_("Public key") + ':'))
1874 keys.setReadOnly(True)
1875 keys.setText('\n'.join(pubkey_list))
1876 vbox.addWidget(keys)
1877 vbox.addLayout(close_button(d))
1882 def show_private_key(self, address, password):
1883 if not address: return
1885 pk_list = self.wallet.get_private_key(address, password)
1886 except Exception as e:
1887 traceback.print_exc(file=sys.stdout)
1888 self.show_message(str(e))
1892 d.setMinimumSize(600, 200)
1894 vbox = QVBoxLayout()
1895 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1896 vbox.addWidget( QLabel(_("Private key") + ':'))
1898 keys.setReadOnly(True)
1899 keys.setText('\n'.join(pk_list))
1900 vbox.addWidget(keys)
1901 vbox.addLayout(close_button(d))
1907 def do_sign(self, address, message, signature, password):
1908 message = unicode(message.toPlainText())
1909 message = message.encode('utf-8')
1911 sig = self.wallet.sign_message(str(address.text()), message, password)
1912 signature.setText(sig)
1913 except Exception as e:
1914 self.show_message(str(e))
1916 def do_verify(self, address, message, signature):
1917 message = unicode(message.toPlainText())
1918 message = message.encode('utf-8')
1919 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1920 self.show_message(_("Signature verified"))
1922 self.show_message(_("Error: wrong signature"))
1925 def sign_verify_message(self, address=''):
1928 d.setWindowTitle(_('Sign/verify Message'))
1929 d.setMinimumSize(410, 290)
1931 layout = QGridLayout(d)
1933 message_e = QTextEdit()
1934 layout.addWidget(QLabel(_('Message')), 1, 0)
1935 layout.addWidget(message_e, 1, 1)
1936 layout.setRowStretch(2,3)
1938 address_e = QLineEdit()
1939 address_e.setText(address)
1940 layout.addWidget(QLabel(_('Address')), 2, 0)
1941 layout.addWidget(address_e, 2, 1)
1943 signature_e = QTextEdit()
1944 layout.addWidget(QLabel(_('Signature')), 3, 0)
1945 layout.addWidget(signature_e, 3, 1)
1946 layout.setRowStretch(3,1)
1948 hbox = QHBoxLayout()
1950 b = QPushButton(_("Sign"))
1951 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1954 b = QPushButton(_("Verify"))
1955 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1958 b = QPushButton(_("Close"))
1959 b.clicked.connect(d.accept)
1961 layout.addLayout(hbox, 4, 1)
1966 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1968 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1969 message_e.setText(decrypted)
1970 except Exception as e:
1971 self.show_message(str(e))
1974 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1975 message = unicode(message_e.toPlainText())
1976 message = message.encode('utf-8')
1978 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1979 encrypted_e.setText(encrypted)
1980 except Exception as e:
1981 self.show_message(str(e))
1985 def encrypt_message(self, address = ''):
1988 d.setWindowTitle(_('Encrypt/decrypt Message'))
1989 d.setMinimumSize(610, 490)
1991 layout = QGridLayout(d)
1993 message_e = QTextEdit()
1994 layout.addWidget(QLabel(_('Message')), 1, 0)
1995 layout.addWidget(message_e, 1, 1)
1996 layout.setRowStretch(2,3)
1998 pubkey_e = QLineEdit()
2000 pubkey = self.wallet.get_public_keys(address)[0]
2001 pubkey_e.setText(pubkey)
2002 layout.addWidget(QLabel(_('Public key')), 2, 0)
2003 layout.addWidget(pubkey_e, 2, 1)
2005 encrypted_e = QTextEdit()
2006 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
2007 layout.addWidget(encrypted_e, 3, 1)
2008 layout.setRowStretch(3,1)
2010 hbox = QHBoxLayout()
2011 b = QPushButton(_("Encrypt"))
2012 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2015 b = QPushButton(_("Decrypt"))
2016 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2019 b = QPushButton(_("Close"))
2020 b.clicked.connect(d.accept)
2023 layout.addLayout(hbox, 4, 1)
2027 def question(self, msg):
2028 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2030 def show_message(self, msg):
2031 QMessageBox.information(self, _('Message'), msg, _('OK'))
2033 def password_dialog(self, msg=None):
2036 d.setWindowTitle(_("Enter Password"))
2041 vbox = QVBoxLayout()
2043 msg = _('Please enter your password')
2044 vbox.addWidget(QLabel(msg))
2046 grid = QGridLayout()
2048 grid.addWidget(QLabel(_('Password')), 1, 0)
2049 grid.addWidget(pw, 1, 1)
2050 vbox.addLayout(grid)
2052 vbox.addLayout(ok_cancel_buttons(d))
2055 run_hook('password_dialog', pw, grid, 1)
2056 if not d.exec_(): return
2057 return unicode(pw.text())
2066 def tx_from_text(self, txt):
2067 "json or raw hexadecimal"
2076 return Transaction.deserialize(txt)
2078 traceback.print_exc(file=sys.stdout)
2079 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2083 tx_dict = json.loads(str(txt))
2084 assert "hex" in tx_dict.keys()
2085 tx = Transaction.deserialize(tx_dict["hex"])
2086 #if tx_dict.has_key("input_info"):
2087 # input_info = json.loads(tx_dict['input_info'])
2088 # tx.add_input_info(input_info)
2091 traceback.print_exc(file=sys.stdout)
2092 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2095 def read_tx_from_qrcode(self):
2096 data = run_hook('scan_qr_hook')
2099 # transactions are binary, but qrcode seems to return utf8...
2100 z = data.decode('utf8')
2104 data = s.encode('hex')
2105 tx = self.tx_from_text(data)
2108 self.show_transaction(tx)
2111 def read_tx_from_file(self):
2112 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2116 with open(fileName, "r") as f:
2117 file_content = f.read()
2118 except (ValueError, IOError, os.error), reason:
2119 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2121 return self.tx_from_text(file_content)
2125 def sign_raw_transaction(self, tx, password):
2127 self.wallet.signrawtransaction(tx, [], password)
2128 except Exception as e:
2129 traceback.print_exc(file=sys.stdout)
2130 QMessageBox.warning(self, _("Error"), str(e))
2132 def do_process_from_text(self):
2133 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2136 tx = self.tx_from_text(text)
2138 self.show_transaction(tx)
2140 def do_process_from_file(self):
2141 tx = self.read_tx_from_file()
2143 self.show_transaction(tx)
2145 def do_process_from_txid(self):
2146 from electrum import transaction
2147 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2149 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2151 tx = transaction.Transaction.deserialize(r)
2153 self.show_transaction(tx)
2155 self.show_message("unknown transaction")
2157 def do_process_from_csvReader(self, csvReader):
2162 for position, row in enumerate(csvReader):
2164 if not is_address(address):
2165 errors.append((position, address))
2167 amount = Decimal(row[1])
2168 amount = int(100000000*amount)
2169 outputs.append(('address', address, amount))
2170 except (ValueError, IOError, os.error), reason:
2171 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2175 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2176 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2180 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2181 except Exception as e:
2182 self.show_message(str(e))
2185 self.show_transaction(tx)
2187 def do_process_from_csv_file(self):
2188 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2192 with open(fileName, "r") as f:
2193 csvReader = csv.reader(f)
2194 self.do_process_from_csvReader(csvReader)
2195 except (ValueError, IOError, os.error), reason:
2196 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2199 def do_process_from_csv_text(self):
2200 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2201 + _("Format: address, amount. One output per line"), _("Load CSV"))
2204 f = StringIO.StringIO(text)
2205 csvReader = csv.reader(f)
2206 self.do_process_from_csvReader(csvReader)
2211 def export_privkeys_dialog(self, password):
2212 if self.wallet.is_watching_only():
2213 self.show_message(_("This is a watching-only wallet"))
2217 d.setWindowTitle(_('Private keys'))
2218 d.setMinimumSize(850, 300)
2219 vbox = QVBoxLayout(d)
2221 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2222 _("Exposing a single private key can compromise your entire wallet!"),
2223 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2224 vbox.addWidget(QLabel(msg))
2230 defaultname = 'electrum-private-keys.csv'
2231 select_msg = _('Select file to export your private keys to')
2232 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2233 vbox.addLayout(hbox)
2235 h, b = ok_cancel_buttons2(d, _('Export'))
2240 addresses = self.wallet.addresses(True)
2242 def privkeys_thread():
2243 for addr in addresses:
2247 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2248 d.emit(SIGNAL('computing_privkeys'))
2249 d.emit(SIGNAL('show_privkeys'))
2251 def show_privkeys():
2252 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2256 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2257 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2258 threading.Thread(target=privkeys_thread).start()
2264 filename = filename_e.text()
2269 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2270 except (IOError, os.error), reason:
2271 export_error_label = _("Electrum was unable to produce a private key-export.")
2272 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2274 except Exception as e:
2275 self.show_message(str(e))
2278 self.show_message(_("Private keys exported."))
2281 def do_export_privkeys(self, fileName, pklist, is_csv):
2282 with open(fileName, "w+") as f:
2284 transaction = csv.writer(f)
2285 transaction.writerow(["address", "private_key"])
2286 for addr, pk in pklist.items():
2287 transaction.writerow(["%34s"%addr,pk])
2290 f.write(json.dumps(pklist, indent = 4))
2293 def do_import_labels(self):
2294 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2295 if not labelsFile: return
2297 f = open(labelsFile, 'r')
2300 for key, value in json.loads(data).items():
2301 self.wallet.set_label(key, value)
2302 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2303 except (IOError, os.error), reason:
2304 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2307 def do_export_labels(self):
2308 labels = self.wallet.labels
2310 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2312 with open(fileName, 'w+') as f:
2313 json.dump(labels, f)
2314 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2315 except (IOError, os.error), reason:
2316 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2319 def export_history_dialog(self):
2322 d.setWindowTitle(_('Export History'))
2323 d.setMinimumSize(400, 200)
2324 vbox = QVBoxLayout(d)
2326 defaultname = os.path.expanduser('~/electrum-history.csv')
2327 select_msg = _('Select file to export your wallet transactions to')
2329 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2330 vbox.addLayout(hbox)
2334 h, b = ok_cancel_buttons2(d, _('Export'))
2339 filename = filename_e.text()
2344 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2345 except (IOError, os.error), reason:
2346 export_error_label = _("Electrum was unable to produce a transaction export.")
2347 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2350 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2353 def do_export_history(self, wallet, fileName, is_csv):
2354 history = wallet.get_tx_history()
2356 for item in history:
2357 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2359 if timestamp is not None:
2361 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2362 except [RuntimeError, TypeError, NameError] as reason:
2363 time_string = "unknown"
2366 time_string = "unknown"
2368 time_string = "pending"
2370 if value is not None:
2371 value_string = format_satoshis(value, True)
2376 fee_string = format_satoshis(fee, True)
2381 label, is_default_label = wallet.get_label(tx_hash)
2382 label = label.encode('utf-8')
2386 balance_string = format_satoshis(balance, False)
2388 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2390 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2392 with open(fileName, "w+") as f:
2394 transaction = csv.writer(f)
2395 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2397 transaction.writerow(line)
2400 f.write(json.dumps(lines, indent = 4))
2403 def sweep_key_dialog(self):
2405 d.setWindowTitle(_('Sweep private keys'))
2406 d.setMinimumSize(600, 300)
2408 vbox = QVBoxLayout(d)
2409 vbox.addWidget(QLabel(_("Enter private keys")))
2411 keys_e = QTextEdit()
2412 keys_e.setTabChangesFocus(True)
2413 vbox.addWidget(keys_e)
2415 h, address_e = address_field(self.wallet.addresses())
2419 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2420 vbox.addLayout(hbox)
2421 button.setEnabled(False)
2424 addr = str(address_e.text())
2425 if bitcoin.is_address(addr):
2429 pk = str(keys_e.toPlainText()).strip()
2430 if Wallet.is_private_key(pk):
2433 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2434 keys_e.textChanged.connect(f)
2435 address_e.textChanged.connect(f)
2439 fee = self.wallet.fee
2440 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2441 self.show_transaction(tx)
2445 def do_import_privkey(self, password):
2446 if not self.wallet.has_imported_keys():
2447 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2448 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2449 + _('Are you sure you understand what you are doing?'), 3, 4)
2452 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2455 text = str(text).split()
2460 addr = self.wallet.import_key(key, password)
2461 except Exception as e:
2467 addrlist.append(addr)
2469 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2471 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2472 self.update_address_tab()
2473 self.update_history_tab()
2476 def settings_dialog(self):
2478 d.setWindowTitle(_('Electrum Settings'))
2480 vbox = QVBoxLayout()
2481 grid = QGridLayout()
2482 grid.setColumnStretch(0,1)
2484 nz_label = QLabel(_('Display zeros') + ':')
2485 grid.addWidget(nz_label, 0, 0)
2486 nz_e = AmountEdit(None,True)
2487 nz_e.setText("%d"% self.num_zeros)
2488 grid.addWidget(nz_e, 0, 1)
2489 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2490 grid.addWidget(HelpButton(msg), 0, 2)
2491 if not self.config.is_modifiable('num_zeros'):
2492 for w in [nz_e, nz_label]: w.setEnabled(False)
2494 lang_label=QLabel(_('Language') + ':')
2495 grid.addWidget(lang_label, 1, 0)
2496 lang_combo = QComboBox()
2497 from electrum.i18n import languages
2498 lang_combo.addItems(languages.values())
2500 index = languages.keys().index(self.config.get("language",''))
2503 lang_combo.setCurrentIndex(index)
2504 grid.addWidget(lang_combo, 1, 1)
2505 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2506 if not self.config.is_modifiable('language'):
2507 for w in [lang_combo, lang_label]: w.setEnabled(False)
2510 fee_label = QLabel(_('Transaction fee') + ':')
2511 grid.addWidget(fee_label, 2, 0)
2512 fee_e = BTCAmountEdit(self.get_decimal_point)
2513 fee_e.setAmount(self.wallet.fee)
2514 grid.addWidget(fee_e, 2, 1)
2515 msg = _('Fee per kilobyte of transaction.') + '\n' \
2516 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2517 grid.addWidget(HelpButton(msg), 2, 2)
2518 if not self.config.is_modifiable('fee_per_kb'):
2519 for w in [fee_e, fee_label]: w.setEnabled(False)
2521 units = ['BTC', 'mBTC', 'bits']
2522 unit_label = QLabel(_('Base unit') + ':')
2523 grid.addWidget(unit_label, 3, 0)
2524 unit_combo = QComboBox()
2525 unit_combo.addItems(units)
2526 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2527 grid.addWidget(unit_combo, 3, 1)
2528 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2529 + '\n1BTC=1000mBTC.\n' \
2530 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2532 usechange_cb = QCheckBox(_('Use change addresses'))
2533 usechange_cb.setChecked(self.wallet.use_change)
2534 grid.addWidget(usechange_cb, 4, 0)
2535 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2536 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2538 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2539 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2540 grid.addWidget(block_ex_label, 5, 0)
2541 block_ex_combo = QComboBox()
2542 block_ex_combo.addItems(block_explorers)
2543 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2544 grid.addWidget(block_ex_combo, 5, 1)
2545 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2547 show_tx = self.config.get('show_before_broadcast', False)
2548 showtx_cb = QCheckBox(_('Show before broadcast'))
2549 showtx_cb.setChecked(show_tx)
2550 grid.addWidget(showtx_cb, 6, 0)
2551 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2553 vbox.addLayout(grid)
2555 vbox.addLayout(ok_cancel_buttons(d))
2559 if not d.exec_(): return
2561 fee = fee_e.get_amount()
2563 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2566 self.wallet.set_fee(fee)
2568 nz = unicode(nz_e.text())
2573 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2576 if self.num_zeros != nz:
2578 self.config.set_key('num_zeros', nz, True)
2579 self.update_history_tab()
2580 self.update_address_tab()
2582 usechange_result = usechange_cb.isChecked()
2583 if self.wallet.use_change != usechange_result:
2584 self.wallet.use_change = usechange_result
2585 self.wallet.storage.put('use_change', self.wallet.use_change)
2587 if showtx_cb.isChecked() != show_tx:
2588 self.config.set_key('show_before_broadcast', not show_tx)
2590 unit_result = units[unit_combo.currentIndex()]
2591 if self.base_unit() != unit_result:
2592 if unit_result == 'BTC':
2593 self.decimal_point = 8
2594 elif unit_result == 'mBTC':
2595 self.decimal_point = 5
2596 elif unit_result == 'bits':
2597 self.decimal_point = 2
2599 raise Exception('Unknown base unit')
2600 self.config.set_key('decimal_point', self.decimal_point, True)
2601 self.update_history_tab()
2602 self.update_status()
2604 need_restart = False
2606 lang_request = languages.keys()[lang_combo.currentIndex()]
2607 if lang_request != self.config.get('language'):
2608 self.config.set_key("language", lang_request, True)
2611 be_result = block_explorers[block_ex_combo.currentIndex()]
2612 self.config.set_key('block_explorer', be_result, True)
2614 run_hook('close_settings_dialog')
2617 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2620 def run_network_dialog(self):
2621 if not self.network:
2623 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2625 def closeEvent(self, event):
2627 self.config.set_key("is_maximized", self.isMaximized())
2628 if not self.isMaximized():
2630 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2631 self.save_column_widths()
2632 self.config.set_key("console-history", self.console.history[-50:], True)
2633 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2637 def plugins_dialog(self):
2638 from electrum.plugins import plugins
2641 d.setWindowTitle(_('Electrum Plugins'))
2644 vbox = QVBoxLayout(d)
2647 scroll = QScrollArea()
2648 scroll.setEnabled(True)
2649 scroll.setWidgetResizable(True)
2650 scroll.setMinimumSize(400,250)
2651 vbox.addWidget(scroll)
2655 w.setMinimumHeight(len(plugins)*35)
2657 grid = QGridLayout()
2658 grid.setColumnStretch(0,1)
2661 def do_toggle(cb, p, w):
2664 if w: w.setEnabled(r)
2666 def mk_toggle(cb, p, w):
2667 return lambda: do_toggle(cb,p,w)
2669 for i, p in enumerate(plugins):
2671 cb = QCheckBox(p.fullname())
2672 cb.setDisabled(not p.is_available())
2673 cb.setChecked(p.is_enabled())
2674 grid.addWidget(cb, i, 0)
2675 if p.requires_settings():
2676 w = p.settings_widget(self)
2677 w.setEnabled( p.is_enabled() )
2678 grid.addWidget(w, i, 1)
2681 cb.clicked.connect(mk_toggle(cb,p,w))
2682 grid.addWidget(HelpButton(p.description()), i, 2)
2684 print_msg(_("Error: cannot display plugin"), p)
2685 traceback.print_exc(file=sys.stdout)
2686 grid.setRowStretch(i+1,1)
2688 vbox.addLayout(close_button(d))
2693 def show_account_details(self, k):
2694 account = self.wallet.accounts[k]
2697 d.setWindowTitle(_('Account Details'))
2700 vbox = QVBoxLayout(d)
2701 name = self.wallet.get_account_name(k)
2702 label = QLabel('Name: ' + name)
2703 vbox.addWidget(label)
2705 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2707 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2709 vbox.addWidget(QLabel(_('Master Public Key:')))
2712 text.setReadOnly(True)
2713 text.setMaximumHeight(170)
2714 vbox.addWidget(text)
2716 mpk_text = '\n'.join( account.get_master_pubkeys() )
2717 text.setText(mpk_text)
2719 vbox.addLayout(close_button(d))