3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
38 from electrum.wallet import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget, QRDialog
49 from qrtextedit import QRTextEdit
51 from decimal import Decimal
59 if platform.system() == 'Windows':
60 MONOSPACE_FONT = 'Lucida Console'
61 elif platform.system() == 'Darwin':
62 MONOSPACE_FONT = 'Monaco'
64 MONOSPACE_FONT = 'monospace'
68 # status of payment requests
71 PR_SENT = 2 # sent but not propagated
72 PR_PAID = 3 # send and propagated
73 PR_ERROR = 4 # could not parse
76 from electrum import ELECTRUM_VERSION
91 class StatusBarButton(QPushButton):
92 def __init__(self, icon, tooltip, func):
93 QPushButton.__init__(self, icon, '')
94 self.setToolTip(tooltip)
96 self.setMaximumWidth(25)
97 self.clicked.connect(func)
99 self.setIconSize(QSize(25,25))
101 def keyPressEvent(self, e):
102 if e.key() == QtCore.Qt.Key_Return:
114 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
116 class ElectrumWindow(QMainWindow):
120 def __init__(self, config, network, gui_object):
121 QMainWindow.__init__(self)
124 self.network = network
125 self.gui_object = gui_object
126 self.tray = gui_object.tray
127 self.go_lite = gui_object.go_lite
130 self.create_status_bar()
131 self.need_update = threading.Event()
133 self.decimal_point = config.get('decimal_point', 5)
134 self.num_zeros = int(config.get('num_zeros',0))
137 set_language(config.get('language'))
139 self.funds_error = False
140 self.completions = QStringListModel()
142 self.tabs = tabs = QTabWidget(self)
143 self.column_widths = self.config.get("column_widths_2", default_column_widths )
144 tabs.addTab(self.create_history_tab(), _('History') )
145 tabs.addTab(self.create_send_tab(), _('Send') )
146 tabs.addTab(self.create_receive_tab(), _('Receive') )
147 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
148 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
149 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
150 tabs.addTab(self.create_console_tab(), _('Console') )
151 tabs.setMinimumSize(600, 400)
152 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
153 self.setCentralWidget(tabs)
155 g = self.config.get("winpos-qt",[100, 100, 840, 400])
156 self.setGeometry(g[0], g[1], g[2], g[3])
157 if self.config.get("is_maximized"):
160 self.setWindowIcon(QIcon(":icons/electrum.png"))
163 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
164 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
165 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
166 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
167 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
169 for i in range(tabs.count()):
170 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
172 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
173 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
174 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
175 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
176 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
178 self.history_list.setFocus(True)
182 self.network.register_callback('updated', lambda: self.need_update.set())
183 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
184 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
185 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
186 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
188 # set initial message
189 self.console.showMessage(self.network.banner)
192 self.payment_request = None
194 def update_account_selector(self):
196 accounts = self.wallet.get_account_names()
197 self.account_selector.clear()
198 if len(accounts) > 1:
199 self.account_selector.addItems([_("All accounts")] + accounts.values())
200 self.account_selector.setCurrentIndex(0)
201 self.account_selector.show()
203 self.account_selector.hide()
206 def load_wallet(self, wallet):
210 self.update_wallet_format()
212 self.invoices = self.wallet.storage.get('invoices', {})
213 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
214 self.current_account = self.wallet.storage.get("current_account", None)
215 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
216 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
217 self.setWindowTitle( title )
219 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
220 self.notify_transactions()
221 self.update_account_selector()
223 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
224 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
225 self.password_menu.setEnabled(not self.wallet.is_watching_only())
226 self.seed_menu.setEnabled(self.wallet.has_seed())
227 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
228 self.import_menu.setEnabled(self.wallet.can_import())
230 self.update_lock_icon()
231 self.update_buttons_on_seed()
232 self.update_console()
234 self.clear_receive_tab()
235 self.update_receive_tab()
236 run_hook('load_wallet', wallet)
239 def update_wallet_format(self):
240 # convert old-format imported keys
241 if self.wallet.imported_keys:
242 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
244 self.wallet.convert_imported_keys(password)
246 self.show_message("error")
249 def open_wallet(self):
250 wallet_folder = self.wallet.storage.path
251 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
255 storage = WalletStorage({'wallet_path': filename})
256 if not storage.file_exists:
257 self.show_message("file not found "+ filename)
260 self.wallet.stop_threads()
263 wallet = Wallet(storage)
264 wallet.start_threads(self.network)
266 self.load_wallet(wallet)
270 def backup_wallet(self):
272 path = self.wallet.storage.path
273 wallet_folder = os.path.dirname(path)
274 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
278 new_path = os.path.join(wallet_folder, filename)
281 shutil.copy2(path, new_path)
282 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
283 except (IOError, os.error), reason:
284 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
287 def new_wallet(self):
290 wallet_folder = os.path.dirname(self.wallet.storage.path)
291 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
294 filename = os.path.join(wallet_folder, filename)
296 storage = WalletStorage({'wallet_path': filename})
297 if storage.file_exists:
298 QMessageBox.critical(None, "Error", _("File exists"))
301 wizard = installwizard.InstallWizard(self.config, self.network, storage)
302 wallet = wizard.run('new')
304 self.load_wallet(wallet)
308 def init_menubar(self):
311 file_menu = menubar.addMenu(_("&File"))
312 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
313 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
314 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
315 file_menu.addAction(_("&Quit"), self.close)
317 wallet_menu = menubar.addMenu(_("&Wallet"))
318 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
319 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
321 wallet_menu.addSeparator()
323 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
324 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
325 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
327 wallet_menu.addSeparator()
328 labels_menu = wallet_menu.addMenu(_("&Labels"))
329 labels_menu.addAction(_("&Import"), self.do_import_labels)
330 labels_menu.addAction(_("&Export"), self.do_export_labels)
332 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
333 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
334 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
335 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
336 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
338 tools_menu = menubar.addMenu(_("&Tools"))
340 # Settings / Preferences are all reserved keywords in OSX using this as work around
341 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
342 tools_menu.addAction(_("&Network"), self.run_network_dialog)
343 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
344 tools_menu.addSeparator()
345 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
346 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
347 tools_menu.addSeparator()
349 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
350 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
351 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
353 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
354 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
355 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
356 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
357 self.raw_transaction_menu = raw_transaction_menu
359 help_menu = menubar.addMenu(_("&Help"))
360 help_menu.addAction(_("&About"), self.show_about)
361 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
362 help_menu.addSeparator()
363 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
364 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
366 self.setMenuBar(menubar)
368 def show_about(self):
369 QMessageBox.about(self, "Electrum",
370 _("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."))
372 def show_report_bug(self):
373 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
374 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
377 def notify_transactions(self):
378 if not self.network or not self.network.is_connected():
381 print_error("Notifying GUI")
382 if len(self.network.pending_transactions_for_notifications) > 0:
383 # Combine the transactions if there are more then three
384 tx_amount = len(self.network.pending_transactions_for_notifications)
387 for tx in self.network.pending_transactions_for_notifications:
388 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
392 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
393 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
395 self.network.pending_transactions_for_notifications = []
397 for tx in self.network.pending_transactions_for_notifications:
399 self.network.pending_transactions_for_notifications.remove(tx)
400 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
402 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
404 def notify(self, message):
405 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
409 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
410 def getOpenFileName(self, title, filter = ""):
411 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
412 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
413 if fileName and directory != os.path.dirname(fileName):
414 self.config.set_key('io_dir', os.path.dirname(fileName), True)
417 def getSaveFileName(self, title, filename, filter = ""):
418 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
419 path = os.path.join( directory, filename )
420 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
421 if fileName and directory != os.path.dirname(fileName):
422 self.config.set_key('io_dir', os.path.dirname(fileName), True)
426 QMainWindow.close(self)
427 run_hook('close_main_window')
429 def connect_slots(self, sender):
430 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
431 self.previous_payto_e=''
433 def timer_actions(self):
434 if self.need_update.is_set():
436 self.need_update.clear()
438 self.receive_qr.update_qr()
439 run_hook('timer_actions')
441 def format_amount(self, x, is_diff=False, whitespaces=False):
442 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
445 def get_decimal_point(self):
446 return self.decimal_point
450 assert self.decimal_point in [5,8]
451 return "BTC" if self.decimal_point == 8 else "mBTC"
454 def update_status(self):
455 if self.network is None or not self.network.is_running():
457 icon = QIcon(":icons/status_disconnected.png")
459 elif self.network.is_connected():
460 if not self.wallet.up_to_date:
461 text = _("Synchronizing...")
462 icon = QIcon(":icons/status_waiting.png")
463 elif self.network.server_lag > 1:
464 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
465 icon = QIcon(":icons/status_lagging.png")
467 c, u = self.wallet.get_account_balance(self.current_account)
468 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
469 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
471 # append fiat balance and price from exchange rate plugin
473 run_hook('get_fiat_status_text', c+u, r)
478 self.tray.setToolTip(text)
479 icon = QIcon(":icons/status_connected.png")
481 text = _("Not connected")
482 icon = QIcon(":icons/status_disconnected.png")
484 self.balance_label.setText(text)
485 self.status_button.setIcon( icon )
488 def update_wallet(self):
490 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
491 self.update_history_tab()
492 self.update_receive_tab()
493 self.update_address_tab()
494 self.update_contacts_tab()
495 self.update_completions()
496 self.update_invoices_tab()
499 def create_history_tab(self):
500 self.history_list = l = MyTreeWidget(self)
502 for i,width in enumerate(self.column_widths['history']):
503 l.setColumnWidth(i, width)
504 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
505 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
506 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
508 l.customContextMenuRequested.connect(self.create_history_menu)
512 def create_history_menu(self, position):
513 self.history_list.selectedIndexes()
514 item = self.history_list.currentItem()
515 be = self.config.get('block_explorer', 'Blockchain.info')
516 if be == 'Blockchain.info':
517 block_explorer = 'https://blockchain.info/tx/'
518 elif be == 'Blockr.io':
519 block_explorer = 'https://blockr.io/tx/info/'
520 elif be == 'Insight.is':
521 block_explorer = 'http://live.insight.is/tx/'
523 tx_hash = str(item.data(0, Qt.UserRole).toString())
524 if not tx_hash: return
526 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
527 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
528 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
529 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
530 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
533 def show_transaction(self, tx):
534 import transaction_dialog
535 d = transaction_dialog.TxDialog(tx, self)
538 def tx_label_clicked(self, item, column):
539 if column==2 and item.isSelected():
541 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
542 self.history_list.editItem( item, column )
543 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
546 def tx_label_changed(self, item, column):
550 tx_hash = str(item.data(0, Qt.UserRole).toString())
551 tx = self.wallet.transactions.get(tx_hash)
552 text = unicode( item.text(2) )
553 self.wallet.set_label(tx_hash, text)
555 item.setForeground(2, QBrush(QColor('black')))
557 text = self.wallet.get_default_label(tx_hash)
558 item.setText(2, text)
559 item.setForeground(2, QBrush(QColor('gray')))
563 def edit_label(self, is_recv):
564 l = self.receive_list if is_recv else self.contacts_list
565 item = l.currentItem()
566 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 l.editItem( item, 1 )
568 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
572 def address_label_clicked(self, item, column, l, column_addr, column_label):
573 if column == column_label and item.isSelected():
574 is_editable = item.data(0, 32).toBool()
577 addr = unicode( item.text(column_addr) )
578 label = unicode( item.text(column_label) )
579 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
580 l.editItem( item, column )
581 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
584 def address_label_changed(self, item, column, l, column_addr, column_label):
585 if column == column_label:
586 addr = unicode( item.text(column_addr) )
587 text = unicode( item.text(column_label) )
588 is_editable = item.data(0, 32).toBool()
592 changed = self.wallet.set_label(addr, text)
594 self.update_history_tab()
595 self.update_completions()
597 self.current_item_changed(item)
599 run_hook('item_changed', item, column)
602 def current_item_changed(self, a):
603 run_hook('current_item_changed', a)
607 def update_history_tab(self):
609 self.history_list.clear()
610 for item in self.wallet.get_tx_history(self.current_account):
611 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
612 time_str = _("unknown")
615 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
617 time_str = _("error")
620 time_str = 'unverified'
621 icon = QIcon(":icons/unconfirmed.png")
624 icon = QIcon(":icons/unconfirmed.png")
626 icon = QIcon(":icons/clock%d.png"%conf)
628 icon = QIcon(":icons/confirmed.png")
630 if value is not None:
631 v_str = self.format_amount(value, True, whitespaces=True)
635 balance_str = self.format_amount(balance, whitespaces=True)
638 label, is_default_label = self.wallet.get_label(tx_hash)
640 label = _('Pruned transaction outputs')
641 is_default_label = False
643 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
644 item.setFont(2, QFont(MONOSPACE_FONT))
645 item.setFont(3, QFont(MONOSPACE_FONT))
646 item.setFont(4, QFont(MONOSPACE_FONT))
648 item.setForeground(3, QBrush(QColor("#BC1E1E")))
650 item.setData(0, Qt.UserRole, tx_hash)
651 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
653 item.setForeground(2, QBrush(QColor('grey')))
655 item.setIcon(0, icon)
656 self.history_list.insertTopLevelItem(0,item)
659 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
660 run_hook('history_tab_update')
663 def create_receive_tab(self):
665 grid = QGridLayout(w)
666 grid.setColumnMinimumWidth(3, 300)
667 grid.setColumnStretch(5, 1)
669 self.receive_address_e = QLineEdit()
670 self.receive_address_e.setReadOnly(True)
671 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
672 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
673 self.receive_address_e.textChanged.connect(self.update_receive_qr)
675 self.receive_message_e = QLineEdit()
676 grid.addWidget(QLabel(_('Message')), 1, 0)
677 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
678 self.receive_message_e.textChanged.connect(self.update_receive_qr)
680 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
681 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
682 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
683 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
685 self.save_request_button = QPushButton(_('Save'))
686 self.save_request_button.clicked.connect(self.save_payment_request)
687 grid.addWidget(self.save_request_button, 3, 1)
688 clear_button = QPushButton(_('New'))
689 clear_button.clicked.connect(self.clear_receive_tab)
690 grid.addWidget(clear_button, 3, 2)
691 grid.setRowStretch(4, 1)
693 self.receive_qr = QRCodeWidget()
694 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
696 self.receive_requests_label = QLabel(_('Saved Requests'))
697 self.receive_list = MyTreeWidget(self)
698 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
699 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
700 self.receive_list.itemClicked.connect(self.receive_item_changed)
701 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount'), _('Status')] )
702 self.receive_list.setColumnWidth(0, 320)
703 h = self.receive_list.header()
704 h.setStretchLastSection(False)
705 h.setResizeMode(1, QHeaderView.Stretch)
707 grid.addWidget(self.receive_requests_label, 5, 0)
708 grid.addWidget(self.receive_list, 6, 0, 1, 6)
710 grid.setRowStretch(7, 1)
713 def receive_item_changed(self, item):
716 addr = str(item.text(0))
717 amount, message = self.receive_requests[addr]
718 self.receive_address_e.setText(addr)
719 self.receive_message_e.setText(message)
720 self.receive_amount_e.setAmount(amount)
723 def receive_list_delete(self, item):
724 addr = str(item.text(0))
725 self.receive_requests.pop(addr)
726 self.update_receive_tab()
727 self.clear_receive_tab()
729 def receive_list_menu(self, position):
730 item = self.receive_list.itemAt(position)
732 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
733 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
735 def save_payment_request(self):
736 addr = str(self.receive_address_e.text())
737 amount = self.receive_amount_e.get_amount()
738 message = str(self.receive_message_e.text())
739 if not message and not amount:
740 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
742 self.receive_requests = self.wallet.storage.get('receive_requests',{})
743 self.receive_requests[addr] = (amount, message)
744 self.wallet.storage.put('receive_requests', self.receive_requests)
745 self.update_receive_tab()
747 def clear_receive_tab(self):
748 self.receive_requests = self.wallet.storage.get('receive_requests',{})
749 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
751 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
755 self.receive_address_e.setText(addr)
756 self.receive_message_e.setText('')
757 self.receive_amount_e.setAmount(None)
759 def receive_at(self, addr):
760 if not bitcoin.is_address(addr):
762 self.tabs.setCurrentIndex(2)
763 self.receive_address_e.setText(addr)
765 def update_receive_tab(self):
766 self.receive_requests = self.wallet.storage.get('receive_requests',{})
767 b = len(self.receive_requests) > 0
768 self.receive_list.setVisible(b)
769 self.receive_requests_label.setVisible(b)
771 self.receive_list.clear()
772 for address, v in self.receive_requests.items():
774 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else "", ""] )
775 self.receive_list.addTopLevelItem(item)
778 def update_receive_qr(self):
779 import urlparse, urllib
780 addr = str(self.receive_address_e.text())
781 amount = self.receive_amount_e.get_amount()
782 message = unicode(self.receive_message_e.text()).encode('utf8')
783 self.save_request_button.setEnabled((amount is not None) or (message != ""))
787 query.append('amount=%s'%format_satoshis(amount))
789 query.append('message=%s'%urllib.quote(message))
790 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
791 url = urlparse.urlunparse(p)
794 self.receive_qr.set_addr(url)
797 def create_send_tab(self):
800 self.send_grid = grid = QGridLayout(w)
802 grid.setColumnMinimumWidth(3,300)
803 grid.setColumnStretch(5,1)
804 grid.setRowStretch(8, 1)
806 from paytoedit import PayToEdit
807 self.amount_e = BTCAmountEdit(self.get_decimal_point)
808 self.payto_e = PayToEdit(self)
809 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)'))
810 grid.addWidget(QLabel(_('Pay to')), 1, 0)
811 grid.addWidget(self.payto_e, 1, 1, 1, 3)
812 grid.addWidget(self.payto_help, 1, 4)
814 completer = QCompleter()
815 completer.setCaseSensitivity(False)
816 self.payto_e.setCompleter(completer)
817 completer.setModel(self.completions)
819 self.message_e = MyLineEdit()
820 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.'))
821 grid.addWidget(QLabel(_('Description')), 2, 0)
822 grid.addWidget(self.message_e, 2, 1, 1, 3)
823 grid.addWidget(self.message_help, 2, 4)
825 self.from_label = QLabel(_('From'))
826 grid.addWidget(self.from_label, 3, 0)
827 self.from_list = MyTreeWidget(self)
828 self.from_list.setColumnCount(2)
829 self.from_list.setColumnWidth(0, 350)
830 self.from_list.setColumnWidth(1, 50)
831 self.from_list.setHeaderHidden(True)
832 self.from_list.setMaximumHeight(80)
833 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
834 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
835 grid.addWidget(self.from_list, 3, 1, 1, 3)
836 self.set_pay_from([])
838 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
839 + _('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.') \
840 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
841 grid.addWidget(QLabel(_('Amount')), 4, 0)
842 grid.addWidget(self.amount_e, 4, 1, 1, 2)
843 grid.addWidget(self.amount_help, 4, 3)
845 self.fee_e = BTCAmountEdit(self.get_decimal_point)
846 grid.addWidget(QLabel(_('Fee')), 5, 0)
847 grid.addWidget(self.fee_e, 5, 1, 1, 2)
848 grid.addWidget(HelpButton(
849 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
850 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
851 + _('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)
853 self.send_button = EnterButton(_("Send"), self.do_send)
854 grid.addWidget(self.send_button, 6, 1)
856 b = EnterButton(_("Clear"), self.do_clear)
857 grid.addWidget(b, 6, 2)
859 self.payto_sig = QLabel('')
860 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
862 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
863 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
866 def entry_changed( is_fee ):
867 self.funds_error = False
869 if self.amount_e.is_shortcut:
870 self.amount_e.is_shortcut = False
871 sendable = self.get_sendable_balance()
872 # there is only one output because we are completely spending inputs
873 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
874 fee = self.wallet.estimated_fee(inputs, 1)
876 self.amount_e.setAmount(amount)
877 self.fee_e.setAmount(fee)
880 amount = self.amount_e.get_amount()
881 fee = self.fee_e.get_amount()
883 if not is_fee: fee = None
886 # assume that there will be 2 outputs (one for change)
887 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
889 self.fee_e.setAmount(fee)
892 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
896 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
897 self.funds_error = True
898 text = _( "Not enough funds" )
899 c, u = self.wallet.get_frozen_balance()
900 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
902 self.statusBar().showMessage(text)
903 self.amount_e.setPalette(palette)
904 self.fee_e.setPalette(palette)
906 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
907 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
909 run_hook('create_send_tab', grid)
912 def from_list_delete(self, item):
913 i = self.from_list.indexOfTopLevelItem(item)
915 self.redraw_from_list()
917 def from_list_menu(self, position):
918 item = self.from_list.itemAt(position)
920 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
921 menu.exec_(self.from_list.viewport().mapToGlobal(position))
923 def set_pay_from(self, domain = None):
924 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
925 self.redraw_from_list()
927 def redraw_from_list(self):
928 self.from_list.clear()
929 self.from_label.setHidden(len(self.pay_from) == 0)
930 self.from_list.setHidden(len(self.pay_from) == 0)
933 h = x.get('prevout_hash')
934 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
936 for item in self.pay_from:
937 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
939 def update_completions(self):
941 for addr,label in self.wallet.labels.items():
942 if addr in self.wallet.addressbook:
943 l.append( label + ' <' + addr + '>')
945 run_hook('update_completions', l)
946 self.completions.setStringList(l)
950 return lambda s, *args: s.do_protect(func, args)
953 def read_send_tab(self):
955 if self.payment_request and self.payment_request.has_expired():
956 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
959 label = unicode( self.message_e.text() )
961 if self.payment_request:
962 outputs = self.payment_request.get_outputs()
964 outputs = self.payto_e.get_outputs()
967 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
970 for addr, x in outputs:
971 if addr is None or not bitcoin.is_address(addr):
972 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
975 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
978 amount = sum(map(lambda x:x[1], outputs))
980 fee = self.fee_e.get_amount()
982 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
985 confirm_amount = self.config.get('confirm_amount', 100000000)
986 if amount >= confirm_amount:
987 o = '\n'.join(map(lambda x:x[0], outputs))
988 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
991 confirm_fee = self.config.get('confirm_fee', 100000)
992 if fee >= confirm_fee:
993 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()}):
996 coins = self.get_coins()
997 return outputs, fee, label, coins
1001 r = self.read_send_tab()
1004 outputs, fee, label, coins = r
1005 self.send_tx(outputs, fee, label, coins)
1009 def send_tx(self, outputs, fee, label, coins, password):
1010 self.send_button.setDisabled(True)
1012 # first, create an unsigned tx
1014 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1016 except Exception as e:
1017 traceback.print_exc(file=sys.stdout)
1018 self.show_message(str(e))
1019 self.send_button.setDisabled(False)
1022 # call hook to see if plugin needs gui interaction
1023 run_hook('send_tx', tx)
1029 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
1030 self.wallet.sign_transaction(tx, keypairs, password)
1031 return tx, fee, label
1033 def sign_done(tx, fee, label):
1035 self.show_message(tx.error)
1036 self.send_button.setDisabled(False)
1038 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1039 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1040 self.send_button.setDisabled(False)
1043 self.wallet.set_label(tx.hash(), label)
1045 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1046 self.show_transaction(tx)
1048 self.send_button.setDisabled(False)
1051 self.broadcast_transaction(tx)
1053 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1054 self.waiting_dialog.start()
1058 def broadcast_transaction(self, tx):
1060 def broadcast_thread():
1061 pr = self.payment_request
1063 return self.wallet.sendtx(tx)
1065 if pr.has_expired():
1066 self.payment_request = None
1067 return False, _("Payment request has expired")
1069 status, msg = self.wallet.sendtx(tx)
1073 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1074 self.wallet.storage.put('invoices', self.invoices)
1075 self.update_invoices_tab()
1076 self.payment_request = None
1077 refund_address = self.wallet.addresses()[0]
1078 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1084 def broadcast_done(status, msg):
1086 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1089 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1090 self.send_button.setDisabled(False)
1092 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1093 self.waiting_dialog.start()
1097 def prepare_for_payment_request(self):
1098 self.tabs.setCurrentIndex(1)
1099 self.payto_e.is_pr = True
1100 for e in [self.payto_e, self.amount_e, self.message_e]:
1102 for h in [self.payto_help, self.amount_help, self.message_help]:
1104 self.payto_e.setText(_("please wait..."))
1107 def payment_request_ok(self):
1108 pr = self.payment_request
1110 if pr_id not in self.invoices:
1111 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1112 self.wallet.storage.put('invoices', self.invoices)
1113 self.update_invoices_tab()
1115 print_error('invoice already in list')
1117 status = self.invoices[pr_id][4]
1118 if status == PR_PAID:
1120 self.show_message("invoice already paid")
1121 self.payment_request = None
1124 self.payto_help.show()
1125 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1127 if not pr.has_expired():
1128 self.payto_e.setGreen()
1130 self.payto_e.setExpired()
1132 self.payto_e.setText(pr.domain)
1133 self.amount_e.setText(self.format_amount(pr.get_amount()))
1134 self.message_e.setText(pr.get_memo())
1136 def payment_request_error(self):
1138 self.show_message(self.payment_request.error)
1139 self.payment_request = None
1141 def pay_from_URI(self,URI):
1144 address, amount, label, message, request_url = util.parse_URI(URI)
1146 address, amount, label, message, request_url = util.parse_URI(URI)
1147 except Exception as e:
1148 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1151 self.tabs.setCurrentIndex(1)
1155 if self.wallet.labels.get(address) != label:
1156 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1157 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1158 self.wallet.addressbook.append(address)
1159 self.wallet.set_label(address, label)
1161 label = self.wallet.labels.get(address)
1163 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1165 self.message_e.setText(message)
1167 self.amount_e.setAmount(amount)
1170 from electrum import paymentrequest
1171 def payment_request():
1172 self.payment_request = paymentrequest.PaymentRequest(self.config)
1173 self.payment_request.read(request_url)
1174 if self.payment_request.verify():
1175 self.emit(SIGNAL('payment_request_ok'))
1177 self.emit(SIGNAL('payment_request_error'))
1179 self.pr_thread = threading.Thread(target=payment_request).start()
1180 self.prepare_for_payment_request()
1185 self.payto_e.is_pr = False
1186 self.payto_sig.setVisible(False)
1187 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1191 for h in [self.payto_help, self.amount_help, self.message_help]:
1194 self.payto_help.set_alt(None)
1195 self.set_pay_from([])
1196 self.update_status()
1200 def set_addrs_frozen(self,addrs,freeze):
1202 if not addr: continue
1203 if addr in self.wallet.frozen_addresses and not freeze:
1204 self.wallet.unfreeze(addr)
1205 elif addr not in self.wallet.frozen_addresses and freeze:
1206 self.wallet.freeze(addr)
1207 self.update_address_tab()
1211 def create_list_tab(self, headers):
1212 "generic tab creation method"
1213 l = MyTreeWidget(self)
1214 l.setColumnCount( len(headers) )
1215 l.setHeaderLabels( headers )
1218 vbox = QVBoxLayout()
1225 vbox.addWidget(buttons)
1230 def create_addresses_tab(self):
1231 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1232 for i,width in enumerate(self.column_widths['receive']):
1233 l.setColumnWidth(i, width)
1234 l.setContextMenuPolicy(Qt.CustomContextMenu)
1235 l.customContextMenuRequested.connect(self.create_receive_menu)
1236 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1237 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1238 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1239 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1240 self.address_list = l
1246 def save_column_widths(self):
1247 self.column_widths["receive"] = []
1248 for i in range(self.address_list.columnCount() -1):
1249 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1251 self.column_widths["history"] = []
1252 for i in range(self.history_list.columnCount() - 1):
1253 self.column_widths["history"].append(self.history_list.columnWidth(i))
1255 self.column_widths["contacts"] = []
1256 for i in range(self.contacts_list.columnCount() - 1):
1257 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1259 self.config.set_key("column_widths_2", self.column_widths, True)
1262 def create_contacts_tab(self):
1263 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1264 l.setContextMenuPolicy(Qt.CustomContextMenu)
1265 l.customContextMenuRequested.connect(self.create_contact_menu)
1266 for i,width in enumerate(self.column_widths['contacts']):
1267 l.setColumnWidth(i, width)
1268 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1269 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1270 self.contacts_list = l
1274 def create_invoices_tab(self):
1275 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1277 h.setStretchLastSection(False)
1278 h.setResizeMode(1, QHeaderView.Stretch)
1279 l.setContextMenuPolicy(Qt.CustomContextMenu)
1280 l.customContextMenuRequested.connect(self.create_invoice_menu)
1281 self.invoices_list = l
1284 def update_invoices_tab(self):
1285 invoices = self.wallet.storage.get('invoices', {})
1286 l = self.invoices_list
1288 for key, value in invoices.items():
1290 domain, memo, amount, expiration_date, status, tx_hash = value
1294 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1296 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1297 l.addTopLevelItem(item)
1299 l.setCurrentItem(l.topLevelItem(0))
1303 def delete_imported_key(self, addr):
1304 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1305 self.wallet.delete_imported_key(addr)
1306 self.update_address_tab()
1307 self.update_history_tab()
1309 def edit_account_label(self, k):
1310 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1312 label = unicode(text)
1313 self.wallet.set_label(k,label)
1314 self.update_address_tab()
1316 def account_set_expanded(self, item, k, b):
1318 self.accounts_expanded[k] = b
1320 def create_account_menu(self, position, k, item):
1322 if item.isExpanded():
1323 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1325 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1326 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1327 if self.wallet.seed_version > 4:
1328 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1329 if self.wallet.account_is_pending(k):
1330 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1331 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1333 def delete_pending_account(self, k):
1334 self.wallet.delete_pending_account(k)
1335 self.update_address_tab()
1337 def create_receive_menu(self, position):
1338 # fixme: this function apparently has a side effect.
1339 # if it is not called the menu pops up several times
1340 #self.address_list.selectedIndexes()
1342 selected = self.address_list.selectedItems()
1343 multi_select = len(selected) > 1
1344 addrs = [unicode(item.text(0)) for item in selected]
1345 if not multi_select:
1346 item = self.address_list.itemAt(position)
1350 if not is_valid(addr):
1351 k = str(item.data(0,32).toString())
1353 self.create_account_menu(position, k, item)
1355 item.setExpanded(not item.isExpanded())
1359 if not multi_select:
1360 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1361 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1362 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1363 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1364 if not self.wallet.is_watching_only():
1365 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1366 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1367 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1368 if self.wallet.is_imported(addr):
1369 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1371 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1372 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1373 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1374 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1377 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1378 if any(can_send(addr) for addr in addrs):
1379 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1381 run_hook('receive_menu', menu, addrs)
1382 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1385 def get_sendable_balance(self):
1386 return sum(map(lambda x:x['value'], self.get_coins()))
1389 def get_coins(self):
1391 return self.pay_from
1393 domain = self.wallet.get_account_addresses(self.current_account)
1394 for i in self.wallet.frozen_addresses:
1395 if i in domain: domain.remove(i)
1396 return self.wallet.get_unspent_coins(domain)
1399 def send_from_addresses(self, addrs):
1400 self.set_pay_from( addrs )
1401 self.tabs.setCurrentIndex(1)
1404 def payto(self, addr):
1406 label = self.wallet.labels.get(addr)
1407 m_addr = label + ' <' + addr + '>' if label else addr
1408 self.tabs.setCurrentIndex(1)
1409 self.payto_e.setText(m_addr)
1410 self.amount_e.setFocus()
1413 def delete_contact(self, x):
1414 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1415 self.wallet.delete_contact(x)
1416 self.wallet.set_label(x, None)
1417 self.update_history_tab()
1418 self.update_contacts_tab()
1419 self.update_completions()
1422 def create_contact_menu(self, position):
1423 item = self.contacts_list.itemAt(position)
1426 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1428 addr = unicode(item.text(0))
1429 label = unicode(item.text(1))
1430 is_editable = item.data(0,32).toBool()
1431 payto_addr = item.data(0,33).toString()
1432 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1433 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1434 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1436 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1437 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1439 run_hook('create_contact_menu', menu, item)
1440 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1442 def delete_invoice(self, key):
1443 self.invoices.pop(key)
1444 self.wallet.storage.put('invoices', self.invoices)
1445 self.update_invoices_tab()
1447 def show_invoice(self, key):
1448 from electrum.paymentrequest import PaymentRequest
1449 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1450 pr = PaymentRequest(self.config)
1454 self.show_pr_details(pr)
1456 def show_pr_details(self, pr):
1457 msg = 'Domain: ' + pr.domain
1458 msg += '\nStatus: ' + pr.get_status()
1459 msg += '\nMemo: ' + pr.get_memo()
1460 msg += '\nPayment URL: ' + pr.payment_url
1461 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1462 QMessageBox.information(self, 'Invoice', msg , 'OK')
1464 def do_pay_invoice(self, key):
1465 from electrum.paymentrequest import PaymentRequest
1466 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1467 pr = PaymentRequest(self.config)
1470 self.payment_request = pr
1471 self.prepare_for_payment_request()
1473 self.payment_request_ok()
1475 self.payment_request_error()
1478 def create_invoice_menu(self, position):
1479 item = self.invoices_list.itemAt(position)
1482 k = self.invoices_list.indexOfTopLevelItem(item)
1483 key = self.invoices.keys()[k]
1484 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1486 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1487 if status == PR_UNPAID:
1488 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1489 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1490 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1493 def update_address_item(self, item):
1494 item.setFont(0, QFont(MONOSPACE_FONT))
1495 address = str(item.data(0,0).toString())
1496 label = self.wallet.labels.get(address,'')
1497 item.setData(1,0,label)
1498 item.setData(0,32, True) # is editable
1500 run_hook('update_address_item', address, item)
1502 if not self.wallet.is_mine(address): return
1504 c, u = self.wallet.get_addr_balance(address)
1505 balance = self.format_amount(c + u)
1506 item.setData(2,0,balance)
1508 if address in self.wallet.frozen_addresses:
1509 item.setBackgroundColor(0, QColor('lightblue'))
1512 def update_address_tab(self):
1513 l = self.address_list
1514 # extend the syntax for consistency
1515 l.addChild = l.addTopLevelItem
1516 l.insertChild = l.insertTopLevelItem
1520 accounts = self.wallet.get_accounts()
1521 if self.current_account is None:
1522 account_items = sorted(accounts.items())
1524 account_items = [(self.current_account, accounts.get(self.current_account))]
1527 for k, account in account_items:
1529 if len(accounts) > 1:
1530 name = self.wallet.get_account_name(k)
1531 c,u = self.wallet.get_account_balance(k)
1532 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1533 l.addTopLevelItem(account_item)
1534 account_item.setExpanded(self.accounts_expanded.get(k, True))
1535 account_item.setData(0, 32, k)
1539 sequences = [0,1] if account.has_change() else [0]
1540 for is_change in sequences:
1541 if len(sequences) > 1:
1542 name = _("Receiving") if not is_change else _("Change")
1543 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1544 account_item.addChild(seq_item)
1546 seq_item.setExpanded(True)
1548 seq_item = account_item
1550 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1556 for address in account.get_addresses(is_change):
1558 num, is_used = self.wallet.is_used(address)
1561 if gap > self.wallet.gap_limit:
1566 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1567 self.update_address_item(item)
1569 item.setBackgroundColor(1, QColor('red'))
1573 seq_item.insertChild(0,used_item)
1575 used_item.addChild(item)
1577 seq_item.addChild(item)
1579 # we use column 1 because column 0 may be hidden
1580 l.setCurrentItem(l.topLevelItem(0),1)
1583 def update_contacts_tab(self):
1584 l = self.contacts_list
1587 for address in self.wallet.addressbook:
1588 label = self.wallet.labels.get(address,'')
1589 n = self.wallet.get_num_tx(address)
1590 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1591 item.setFont(0, QFont(MONOSPACE_FONT))
1592 # 32 = label can be edited (bool)
1593 item.setData(0,32, True)
1595 item.setData(0,33, address)
1596 l.addTopLevelItem(item)
1598 run_hook('update_contacts_tab', l)
1599 l.setCurrentItem(l.topLevelItem(0))
1603 def create_console_tab(self):
1604 from console import Console
1605 self.console = console = Console()
1609 def update_console(self):
1610 console = self.console
1611 console.history = self.config.get("console-history",[])
1612 console.history_index = len(console.history)
1614 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1615 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1617 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1619 def mkfunc(f, method):
1620 return lambda *args: apply( f, (method, args, self.password_dialog ))
1622 if m[0]=='_' or m in ['network','wallet']: continue
1623 methods[m] = mkfunc(c._run, m)
1625 console.updateNamespace(methods)
1628 def change_account(self,s):
1629 if s == _("All accounts"):
1630 self.current_account = None
1632 accounts = self.wallet.get_account_names()
1633 for k, v in accounts.items():
1635 self.current_account = k
1636 self.update_history_tab()
1637 self.update_status()
1638 self.update_address_tab()
1639 self.update_receive_tab()
1641 def create_status_bar(self):
1644 sb.setFixedHeight(35)
1645 qtVersion = qVersion()
1647 self.balance_label = QLabel("")
1648 sb.addWidget(self.balance_label)
1650 from version_getter import UpdateLabel
1651 self.updatelabel = UpdateLabel(self.config, sb)
1653 self.account_selector = QComboBox()
1654 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1655 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1656 sb.addPermanentWidget(self.account_selector)
1658 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1659 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1661 self.lock_icon = QIcon()
1662 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1663 sb.addPermanentWidget( self.password_button )
1665 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1666 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1667 sb.addPermanentWidget( self.seed_button )
1668 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1669 sb.addPermanentWidget( self.status_button )
1671 run_hook('create_status_bar', (sb,))
1673 self.setStatusBar(sb)
1676 def update_lock_icon(self):
1677 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1678 self.password_button.setIcon( icon )
1681 def update_buttons_on_seed(self):
1682 if self.wallet.has_seed():
1683 self.seed_button.show()
1685 self.seed_button.hide()
1687 if not self.wallet.is_watching_only():
1688 self.password_button.show()
1689 self.send_button.setText(_("Send"))
1691 self.password_button.hide()
1692 self.send_button.setText(_("Create unsigned transaction"))
1695 def change_password_dialog(self):
1696 from password_dialog import PasswordDialog
1697 d = PasswordDialog(self.wallet, self)
1699 self.update_lock_icon()
1702 def new_contact_dialog(self):
1705 d.setWindowTitle(_("New Contact"))
1706 vbox = QVBoxLayout(d)
1707 vbox.addWidget(QLabel(_('New Contact')+':'))
1709 grid = QGridLayout()
1712 grid.addWidget(QLabel(_("Address")), 1, 0)
1713 grid.addWidget(line1, 1, 1)
1714 grid.addWidget(QLabel(_("Name")), 2, 0)
1715 grid.addWidget(line2, 2, 1)
1717 vbox.addLayout(grid)
1718 vbox.addLayout(ok_cancel_buttons(d))
1723 address = str(line1.text())
1724 label = unicode(line2.text())
1726 if not is_valid(address):
1727 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1730 self.wallet.add_contact(address)
1732 self.wallet.set_label(address, label)
1734 self.update_contacts_tab()
1735 self.update_history_tab()
1736 self.update_completions()
1737 self.tabs.setCurrentIndex(3)
1741 def new_account_dialog(self, password):
1743 dialog = QDialog(self)
1745 dialog.setWindowTitle(_("New Account"))
1747 vbox = QVBoxLayout()
1748 vbox.addWidget(QLabel(_('Account name')+':'))
1751 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1752 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1757 vbox.addLayout(ok_cancel_buttons(dialog))
1758 dialog.setLayout(vbox)
1762 name = str(e.text())
1765 self.wallet.create_pending_account(name, password)
1766 self.update_address_tab()
1767 self.tabs.setCurrentIndex(2)
1772 def show_master_public_keys(self):
1774 dialog = QDialog(self)
1776 dialog.setWindowTitle(_("Master Public Keys"))
1778 main_layout = QGridLayout()
1779 mpk_dict = self.wallet.get_master_public_keys()
1781 for key, value in mpk_dict.items():
1782 main_layout.addWidget(QLabel(key), i, 0)
1783 mpk_text = QTextEdit()
1784 mpk_text.setReadOnly(True)
1785 mpk_text.setMaximumHeight(170)
1786 mpk_text.setText(value)
1787 main_layout.addWidget(mpk_text, i + 1, 0)
1790 vbox = QVBoxLayout()
1791 vbox.addLayout(main_layout)
1792 vbox.addLayout(close_button(dialog))
1794 dialog.setLayout(vbox)
1799 def show_seed_dialog(self, password):
1800 if not self.wallet.has_seed():
1801 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1805 mnemonic = self.wallet.get_mnemonic(password)
1807 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1809 from seed_dialog import SeedDialog
1810 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1815 def show_qrcode(self, data, title = _("QR code")):
1818 print_error("qrcode:", data)
1819 d = QRDialog(data, self, title)
1823 def do_protect(self, func, args):
1824 if self.wallet.use_encryption:
1825 password = self.password_dialog()
1831 if args != (False,):
1832 args = (self,) + args + (password,)
1834 args = (self,password)
1838 def show_public_keys(self, address):
1839 if not address: return
1841 pubkey_list = self.wallet.get_public_keys(address)
1842 except Exception as e:
1843 traceback.print_exc(file=sys.stdout)
1844 self.show_message(str(e))
1848 d.setMinimumSize(600, 200)
1850 vbox = QVBoxLayout()
1851 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1852 vbox.addWidget( QLabel(_("Public key") + ':'))
1854 keys.setReadOnly(True)
1855 keys.setText('\n'.join(pubkey_list))
1856 vbox.addWidget(keys)
1857 vbox.addLayout(close_button(d))
1862 def show_private_key(self, address, password):
1863 if not address: return
1865 pk_list = self.wallet.get_private_key(address, password)
1866 except Exception as e:
1867 traceback.print_exc(file=sys.stdout)
1868 self.show_message(str(e))
1872 d.setMinimumSize(600, 200)
1874 vbox = QVBoxLayout()
1875 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1876 vbox.addWidget( QLabel(_("Private key") + ':'))
1878 keys.setReadOnly(True)
1879 keys.setText('\n'.join(pk_list))
1880 vbox.addWidget(keys)
1881 vbox.addLayout(close_button(d))
1887 def do_sign(self, address, message, signature, password):
1888 message = unicode(message.toPlainText())
1889 message = message.encode('utf-8')
1891 sig = self.wallet.sign_message(str(address.text()), message, password)
1892 signature.setText(sig)
1893 except Exception as e:
1894 self.show_message(str(e))
1896 def do_verify(self, address, message, signature):
1897 message = unicode(message.toPlainText())
1898 message = message.encode('utf-8')
1899 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1900 self.show_message(_("Signature verified"))
1902 self.show_message(_("Error: wrong signature"))
1905 def sign_verify_message(self, address=''):
1908 d.setWindowTitle(_('Sign/verify Message'))
1909 d.setMinimumSize(410, 290)
1911 layout = QGridLayout(d)
1913 message_e = QTextEdit()
1914 layout.addWidget(QLabel(_('Message')), 1, 0)
1915 layout.addWidget(message_e, 1, 1)
1916 layout.setRowStretch(2,3)
1918 address_e = QLineEdit()
1919 address_e.setText(address)
1920 layout.addWidget(QLabel(_('Address')), 2, 0)
1921 layout.addWidget(address_e, 2, 1)
1923 signature_e = QTextEdit()
1924 layout.addWidget(QLabel(_('Signature')), 3, 0)
1925 layout.addWidget(signature_e, 3, 1)
1926 layout.setRowStretch(3,1)
1928 hbox = QHBoxLayout()
1930 b = QPushButton(_("Sign"))
1931 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1934 b = QPushButton(_("Verify"))
1935 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1938 b = QPushButton(_("Close"))
1939 b.clicked.connect(d.accept)
1941 layout.addLayout(hbox, 4, 1)
1946 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1948 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1949 message_e.setText(decrypted)
1950 except Exception as e:
1951 self.show_message(str(e))
1954 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1955 message = unicode(message_e.toPlainText())
1956 message = message.encode('utf-8')
1958 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1959 encrypted_e.setText(encrypted)
1960 except Exception as e:
1961 self.show_message(str(e))
1965 def encrypt_message(self, address = ''):
1968 d.setWindowTitle(_('Encrypt/decrypt Message'))
1969 d.setMinimumSize(610, 490)
1971 layout = QGridLayout(d)
1973 message_e = QTextEdit()
1974 layout.addWidget(QLabel(_('Message')), 1, 0)
1975 layout.addWidget(message_e, 1, 1)
1976 layout.setRowStretch(2,3)
1978 pubkey_e = QLineEdit()
1980 pubkey = self.wallet.getpubkeys(address)[0]
1981 pubkey_e.setText(pubkey)
1982 layout.addWidget(QLabel(_('Public key')), 2, 0)
1983 layout.addWidget(pubkey_e, 2, 1)
1985 encrypted_e = QTextEdit()
1986 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1987 layout.addWidget(encrypted_e, 3, 1)
1988 layout.setRowStretch(3,1)
1990 hbox = QHBoxLayout()
1991 b = QPushButton(_("Encrypt"))
1992 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1995 b = QPushButton(_("Decrypt"))
1996 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1999 b = QPushButton(_("Close"))
2000 b.clicked.connect(d.accept)
2003 layout.addLayout(hbox, 4, 1)
2007 def question(self, msg):
2008 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2010 def show_message(self, msg):
2011 QMessageBox.information(self, _('Message'), msg, _('OK'))
2013 def password_dialog(self, msg=None):
2016 d.setWindowTitle(_("Enter Password"))
2021 vbox = QVBoxLayout()
2023 msg = _('Please enter your password')
2024 vbox.addWidget(QLabel(msg))
2026 grid = QGridLayout()
2028 grid.addWidget(QLabel(_('Password')), 1, 0)
2029 grid.addWidget(pw, 1, 1)
2030 vbox.addLayout(grid)
2032 vbox.addLayout(ok_cancel_buttons(d))
2035 run_hook('password_dialog', pw, grid, 1)
2036 if not d.exec_(): return
2037 return unicode(pw.text())
2046 def tx_from_text(self, txt):
2047 "json or raw hexadecimal"
2050 tx = Transaction(txt)
2056 tx_dict = json.loads(str(txt))
2057 assert "hex" in tx_dict.keys()
2058 tx = Transaction(tx_dict["hex"])
2059 if tx_dict.has_key("input_info"):
2060 input_info = json.loads(tx_dict['input_info'])
2061 tx.add_input_info(input_info)
2064 traceback.print_exc(file=sys.stdout)
2067 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2071 def read_tx_from_file(self):
2072 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2076 with open(fileName, "r") as f:
2077 file_content = f.read()
2078 except (ValueError, IOError, os.error), reason:
2079 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2081 return self.tx_from_text(file_content)
2085 def sign_raw_transaction(self, tx, input_info, password):
2087 self.wallet.signrawtransaction(tx, input_info, [], password)
2088 except Exception as e:
2089 QMessageBox.warning(self, _("Error"), str(e))
2091 def do_process_from_text(self):
2092 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2095 tx = self.tx_from_text(text)
2097 self.show_transaction(tx)
2099 def do_process_from_file(self):
2100 tx = self.read_tx_from_file()
2102 self.show_transaction(tx)
2104 def do_process_from_txid(self):
2105 from electrum import transaction
2106 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2108 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2110 tx = transaction.Transaction(r)
2112 self.show_transaction(tx)
2114 self.show_message("unknown transaction")
2116 def do_process_from_csvReader(self, csvReader):
2121 for position, row in enumerate(csvReader):
2123 if not is_valid(address):
2124 errors.append((position, address))
2126 amount = Decimal(row[1])
2127 amount = int(100000000*amount)
2128 outputs.append((address, amount))
2129 except (ValueError, IOError, os.error), reason:
2130 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2134 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2135 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2139 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2140 except Exception as e:
2141 self.show_message(str(e))
2144 self.show_transaction(tx)
2146 def do_process_from_csv_file(self):
2147 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2151 with open(fileName, "r") as f:
2152 csvReader = csv.reader(f)
2153 self.do_process_from_csvReader(csvReader)
2154 except (ValueError, IOError, os.error), reason:
2155 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2158 def do_process_from_csv_text(self):
2159 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2160 + _("Format: address, amount. One output per line"), _("Load CSV"))
2163 f = StringIO.StringIO(text)
2164 csvReader = csv.reader(f)
2165 self.do_process_from_csvReader(csvReader)
2170 def export_privkeys_dialog(self, password):
2171 if self.wallet.is_watching_only():
2172 self.show_message(_("This is a watching-only wallet"))
2176 d.setWindowTitle(_('Private keys'))
2177 d.setMinimumSize(850, 300)
2178 vbox = QVBoxLayout(d)
2180 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2181 _("Exposing a single private key can compromise your entire wallet!"),
2182 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2183 vbox.addWidget(QLabel(msg))
2189 defaultname = 'electrum-private-keys.csv'
2190 select_msg = _('Select file to export your private keys to')
2191 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2192 vbox.addLayout(hbox)
2194 h, b = ok_cancel_buttons2(d, _('Export'))
2199 addresses = self.wallet.addresses(True)
2201 def privkeys_thread():
2202 for addr in addresses:
2206 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2207 d.emit(SIGNAL('computing_privkeys'))
2208 d.emit(SIGNAL('show_privkeys'))
2210 def show_privkeys():
2211 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2215 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2216 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2217 threading.Thread(target=privkeys_thread).start()
2223 filename = filename_e.text()
2228 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2229 except (IOError, os.error), reason:
2230 export_error_label = _("Electrum was unable to produce a private key-export.")
2231 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2233 except Exception as e:
2234 self.show_message(str(e))
2237 self.show_message(_("Private keys exported."))
2240 def do_export_privkeys(self, fileName, pklist, is_csv):
2241 with open(fileName, "w+") as f:
2243 transaction = csv.writer(f)
2244 transaction.writerow(["address", "private_key"])
2245 for addr, pk in pklist.items():
2246 transaction.writerow(["%34s"%addr,pk])
2249 f.write(json.dumps(pklist, indent = 4))
2252 def do_import_labels(self):
2253 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2254 if not labelsFile: return
2256 f = open(labelsFile, 'r')
2259 for key, value in json.loads(data).items():
2260 self.wallet.set_label(key, value)
2261 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2262 except (IOError, os.error), reason:
2263 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2266 def do_export_labels(self):
2267 labels = self.wallet.labels
2269 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2271 with open(fileName, 'w+') as f:
2272 json.dump(labels, f)
2273 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2274 except (IOError, os.error), reason:
2275 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2278 def export_history_dialog(self):
2281 d.setWindowTitle(_('Export History'))
2282 d.setMinimumSize(400, 200)
2283 vbox = QVBoxLayout(d)
2285 defaultname = os.path.expanduser('~/electrum-history.csv')
2286 select_msg = _('Select file to export your wallet transactions to')
2288 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2289 vbox.addLayout(hbox)
2293 h, b = ok_cancel_buttons2(d, _('Export'))
2298 filename = filename_e.text()
2303 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2304 except (IOError, os.error), reason:
2305 export_error_label = _("Electrum was unable to produce a transaction export.")
2306 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2309 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2312 def do_export_history(self, wallet, fileName, is_csv):
2313 history = wallet.get_tx_history()
2315 for item in history:
2316 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2318 if timestamp is not None:
2320 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2321 except [RuntimeError, TypeError, NameError] as reason:
2322 time_string = "unknown"
2325 time_string = "unknown"
2327 time_string = "pending"
2329 if value is not None:
2330 value_string = format_satoshis(value, True)
2335 fee_string = format_satoshis(fee, True)
2340 label, is_default_label = wallet.get_label(tx_hash)
2341 label = label.encode('utf-8')
2345 balance_string = format_satoshis(balance, False)
2347 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2349 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2351 with open(fileName, "w+") as f:
2353 transaction = csv.writer(f)
2354 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2356 transaction.writerow(line)
2359 f.write(json.dumps(lines, indent = 4))
2362 def sweep_key_dialog(self):
2364 d.setWindowTitle(_('Sweep private keys'))
2365 d.setMinimumSize(600, 300)
2367 vbox = QVBoxLayout(d)
2368 vbox.addWidget(QLabel(_("Enter private keys")))
2370 keys_e = QTextEdit()
2371 keys_e.setTabChangesFocus(True)
2372 vbox.addWidget(keys_e)
2374 h, address_e = address_field(self.wallet.addresses())
2378 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2379 vbox.addLayout(hbox)
2380 button.setEnabled(False)
2383 addr = str(address_e.text())
2384 if bitcoin.is_address(addr):
2388 pk = str(keys_e.toPlainText()).strip()
2389 if Wallet.is_private_key(pk):
2392 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2393 keys_e.textChanged.connect(f)
2394 address_e.textChanged.connect(f)
2398 fee = self.wallet.fee
2399 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2400 self.show_transaction(tx)
2404 def do_import_privkey(self, password):
2405 if not self.wallet.has_imported_keys():
2406 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2407 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2408 + _('Are you sure you understand what you are doing?'), 3, 4)
2411 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2414 text = str(text).split()
2419 addr = self.wallet.import_key(key, password)
2420 except Exception as e:
2426 addrlist.append(addr)
2428 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2430 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2431 self.update_address_tab()
2432 self.update_history_tab()
2435 def settings_dialog(self):
2437 d.setWindowTitle(_('Electrum Settings'))
2439 vbox = QVBoxLayout()
2440 grid = QGridLayout()
2441 grid.setColumnStretch(0,1)
2443 nz_label = QLabel(_('Display zeros') + ':')
2444 grid.addWidget(nz_label, 0, 0)
2445 nz_e = AmountEdit(None,True)
2446 nz_e.setText("%d"% self.num_zeros)
2447 grid.addWidget(nz_e, 0, 1)
2448 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2449 grid.addWidget(HelpButton(msg), 0, 2)
2450 if not self.config.is_modifiable('num_zeros'):
2451 for w in [nz_e, nz_label]: w.setEnabled(False)
2453 lang_label=QLabel(_('Language') + ':')
2454 grid.addWidget(lang_label, 1, 0)
2455 lang_combo = QComboBox()
2456 from electrum.i18n import languages
2457 lang_combo.addItems(languages.values())
2459 index = languages.keys().index(self.config.get("language",''))
2462 lang_combo.setCurrentIndex(index)
2463 grid.addWidget(lang_combo, 1, 1)
2464 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2465 if not self.config.is_modifiable('language'):
2466 for w in [lang_combo, lang_label]: w.setEnabled(False)
2469 fee_label = QLabel(_('Transaction fee') + ':')
2470 grid.addWidget(fee_label, 2, 0)
2471 fee_e = BTCAmountEdit(self.get_decimal_point)
2472 fee_e.setAmount(self.wallet.fee)
2473 grid.addWidget(fee_e, 2, 1)
2474 msg = _('Fee per kilobyte of transaction.') + '\n' \
2475 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2476 grid.addWidget(HelpButton(msg), 2, 2)
2477 if not self.config.is_modifiable('fee_per_kb'):
2478 for w in [fee_e, fee_label]: w.setEnabled(False)
2480 units = ['BTC', 'mBTC']
2481 unit_label = QLabel(_('Base unit') + ':')
2482 grid.addWidget(unit_label, 3, 0)
2483 unit_combo = QComboBox()
2484 unit_combo.addItems(units)
2485 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2486 grid.addWidget(unit_combo, 3, 1)
2487 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2488 + '\n1BTC=1000mBTC.\n' \
2489 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2491 usechange_cb = QCheckBox(_('Use change addresses'))
2492 usechange_cb.setChecked(self.wallet.use_change)
2493 grid.addWidget(usechange_cb, 4, 0)
2494 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2495 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2497 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2498 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2499 grid.addWidget(block_ex_label, 5, 0)
2500 block_ex_combo = QComboBox()
2501 block_ex_combo.addItems(block_explorers)
2502 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2503 grid.addWidget(block_ex_combo, 5, 1)
2504 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2506 show_tx = self.config.get('show_before_broadcast', False)
2507 showtx_cb = QCheckBox(_('Show before broadcast'))
2508 showtx_cb.setChecked(show_tx)
2509 grid.addWidget(showtx_cb, 6, 0)
2510 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2512 vbox.addLayout(grid)
2514 vbox.addLayout(ok_cancel_buttons(d))
2518 if not d.exec_(): return
2520 fee = fee_e.get_amount()
2522 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2525 self.wallet.set_fee(fee)
2527 nz = unicode(nz_e.text())
2532 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2535 if self.num_zeros != nz:
2537 self.config.set_key('num_zeros', nz, True)
2538 self.update_history_tab()
2539 self.update_address_tab()
2541 usechange_result = usechange_cb.isChecked()
2542 if self.wallet.use_change != usechange_result:
2543 self.wallet.use_change = usechange_result
2544 self.wallet.storage.put('use_change', self.wallet.use_change)
2546 if showtx_cb.isChecked() != show_tx:
2547 self.config.set_key('show_before_broadcast', not show_tx)
2549 unit_result = units[unit_combo.currentIndex()]
2550 if self.base_unit() != unit_result:
2551 self.decimal_point = 8 if unit_result == 'BTC' else 5
2552 self.config.set_key('decimal_point', self.decimal_point, True)
2553 self.update_history_tab()
2554 self.update_status()
2556 need_restart = False
2558 lang_request = languages.keys()[lang_combo.currentIndex()]
2559 if lang_request != self.config.get('language'):
2560 self.config.set_key("language", lang_request, True)
2563 be_result = block_explorers[block_ex_combo.currentIndex()]
2564 self.config.set_key('block_explorer', be_result, True)
2566 run_hook('close_settings_dialog')
2569 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2572 def run_network_dialog(self):
2573 if not self.network:
2575 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2577 def closeEvent(self, event):
2579 self.config.set_key("is_maximized", self.isMaximized())
2580 if not self.isMaximized():
2582 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2583 self.save_column_widths()
2584 self.config.set_key("console-history", self.console.history[-50:], True)
2585 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2589 def plugins_dialog(self):
2590 from electrum.plugins import plugins
2593 d.setWindowTitle(_('Electrum Plugins'))
2596 vbox = QVBoxLayout(d)
2599 scroll = QScrollArea()
2600 scroll.setEnabled(True)
2601 scroll.setWidgetResizable(True)
2602 scroll.setMinimumSize(400,250)
2603 vbox.addWidget(scroll)
2607 w.setMinimumHeight(len(plugins)*35)
2609 grid = QGridLayout()
2610 grid.setColumnStretch(0,1)
2613 def do_toggle(cb, p, w):
2616 if w: w.setEnabled(r)
2618 def mk_toggle(cb, p, w):
2619 return lambda: do_toggle(cb,p,w)
2621 for i, p in enumerate(plugins):
2623 cb = QCheckBox(p.fullname())
2624 cb.setDisabled(not p.is_available())
2625 cb.setChecked(p.is_enabled())
2626 grid.addWidget(cb, i, 0)
2627 if p.requires_settings():
2628 w = p.settings_widget(self)
2629 w.setEnabled( p.is_enabled() )
2630 grid.addWidget(w, i, 1)
2633 cb.clicked.connect(mk_toggle(cb,p,w))
2634 grid.addWidget(HelpButton(p.description()), i, 2)
2636 print_msg(_("Error: cannot display plugin"), p)
2637 traceback.print_exc(file=sys.stdout)
2638 grid.setRowStretch(i+1,1)
2640 vbox.addLayout(close_button(d))
2645 def show_account_details(self, k):
2646 account = self.wallet.accounts[k]
2649 d.setWindowTitle(_('Account Details'))
2652 vbox = QVBoxLayout(d)
2653 name = self.wallet.get_account_name(k)
2654 label = QLabel('Name: ' + name)
2655 vbox.addWidget(label)
2657 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2659 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2661 vbox.addWidget(QLabel(_('Master Public Key:')))
2664 text.setReadOnly(True)
2665 text.setMaximumHeight(170)
2666 vbox.addWidget(text)
2668 mpk_text = '\n'.join( account.get_master_pubkeys() )
2669 text.setText(mpk_text)
2671 vbox.addLayout(close_button(d))