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
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
32 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
33 from electrum.plugins import run_hook
37 from electrum.wallet import format_satoshis
38 from electrum import Transaction
39 from electrum import mnemonic
40 from electrum import util, bitcoin, commands, Interface, Wallet
41 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget
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'
65 from electrum import ELECTRUM_VERSION
75 class StatusBarButton(QPushButton):
76 def __init__(self, icon, tooltip, func):
77 QPushButton.__init__(self, icon, '')
78 self.setToolTip(tooltip)
80 self.setMaximumWidth(25)
81 self.clicked.connect(func)
83 self.setIconSize(QSize(25,25))
85 def keyPressEvent(self, e):
86 if e.key() == QtCore.Qt.Key_Return:
98 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
100 class ElectrumWindow(QMainWindow):
101 def changeEvent(self, event):
102 flags = self.windowFlags();
103 if event and event.type() == QtCore.QEvent.WindowStateChange:
104 if self.windowState() & QtCore.Qt.WindowMinimized:
105 self.build_menu(True)
106 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
107 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
108 # Electrum from closing.
109 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
110 # self.setWindowFlags(flags & ~Qt.ToolTip)
111 elif event.oldState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(False)
113 #self.setWindowFlags(flags | Qt.ToolTip)
115 def build_menu(self, is_hidden = False):
117 if self.isMinimized():
118 m.addAction(_("Show"), self.showNormal)
120 m.addAction(_("Hide"), self.showMinimized)
123 m.addAction(_("Exit Electrum"), self.close)
124 self.tray.setContextMenu(m)
126 def tray_activated(self, reason):
127 if reason == QSystemTrayIcon.DoubleClick:
130 def showNormal(self):
131 self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
133 def __init__(self, config, network):
134 QMainWindow.__init__(self)
137 self.network = network
139 self._close_electrum = False
142 self.icon = QIcon(':icons/electrum_light_icon.png')
143 self.tray = QSystemTrayIcon(self.icon, self)
144 self.tray.setToolTip('Electrum')
145 self.tray.activated.connect(self.tray_activated)
149 self.create_status_bar()
151 self.need_update = threading.Event()
153 self.decimal_point = config.get('decimal_point', 8)
154 self.num_zeros = int(config.get('num_zeros',0))
156 set_language(config.get('language'))
158 self.funds_error = False
159 self.completions = QStringListModel()
161 self.tabs = tabs = QTabWidget(self)
162 self.column_widths = self.config.get("column_widths_2", default_column_widths )
163 tabs.addTab(self.create_history_tab(), _('History') )
164 tabs.addTab(self.create_send_tab(), _('Send') )
165 tabs.addTab(self.create_receive_tab(), _('Receive') )
166 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
167 tabs.addTab(self.create_console_tab(), _('Console') )
168 tabs.setMinimumSize(600, 400)
169 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
170 self.setCentralWidget(tabs)
172 g = self.config.get("winpos-qt",[100, 100, 840, 400])
173 self.setGeometry(g[0], g[1], g[2], g[3])
177 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
178 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
179 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
180 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
181 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
183 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
184 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
185 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
187 self.history_list.setFocus(True)
190 self.network.register_callback('updated', lambda: self.need_update.set())
191 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
192 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
193 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
194 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
195 # set initial message
196 self.console.showMessage(self.network.banner)
198 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
199 if platform.system() == 'Windows':
200 n = 3 if self.wallet.seed else 2
201 tabs.setCurrentIndex (n)
202 tabs.setCurrentIndex (0)
209 self.config.set_key('lite_mode', False, True)
214 self.config.set_key('lite_mode', True, True)
221 if not self.check_qt_version():
222 if self.config.get('lite_mode') is True:
223 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
224 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
225 self.config.set_key('lite_mode', False, True)
230 actuator = lite_window.MiniActuator(self)
232 # Should probably not modify the current path but instead
233 # change the behaviour of rsrc(...)
234 old_path = QDir.currentPath()
235 actuator.load_theme()
237 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
239 driver = lite_window.MiniDriver(self, self.mini)
241 # Reset path back to original value now that loading the GUI
243 QDir.setCurrent(old_path)
245 if self.config.get('lite_mode') is True:
251 def check_qt_version(self):
252 qtVersion = qVersion()
253 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
256 def update_account_selector(self):
258 accounts = self.wallet.get_account_names()
259 self.account_selector.clear()
260 if len(accounts) > 1:
261 self.account_selector.addItems([_("All accounts")] + accounts.values())
262 self.account_selector.setCurrentIndex(0)
263 self.account_selector.show()
265 self.account_selector.hide()
268 def load_wallet(self, wallet):
271 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
272 self.current_account = self.wallet.storage.get("current_account", None)
273 self.pending_accounts = self.wallet.storage.get('pending_accounts',{})
275 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
276 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
277 self.setWindowTitle( title )
279 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
280 self.notify_transactions()
281 self.update_account_selector()
282 self.new_account.setEnabled(self.wallet.seed_version>4)
283 self.update_lock_icon()
284 self.update_buttons_on_seed()
285 self.update_console()
287 run_hook('load_wallet', wallet)
290 def select_wallet_file(self):
291 wallet_folder = self.wallet.storage.path
292 re.sub("(\/\w*.dat)$", "", wallet_folder)
293 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
297 def open_wallet(self):
299 filename = self.select_wallet_file()
303 storage = WalletStorage({'wallet_path': filename})
304 if not storage.file_exists:
305 self.show_message("file not found "+ filename)
308 self.wallet.stop_threads()
311 wallet = Wallet(storage)
312 wallet.start_threads(self.network)
314 self.load_wallet(wallet)
318 def backup_wallet(self):
320 path = self.wallet.storage.path
321 wallet_folder = os.path.dirname(path)
322 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
323 new_filename = unicode(new_filename)
324 if not ok or not new_filename:
327 new_path = os.path.join(wallet_folder, new_filename)
330 shutil.copy2(path, new_path)
331 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
332 except (IOError, os.error), reason:
333 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
336 def new_wallet(self):
339 wallet_folder = os.path.dirname(self.wallet.storage.path)
340 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
341 filename = unicode(filename)
342 if not ok or not filename:
344 filename = os.path.join(wallet_folder, filename)
346 storage = WalletStorage({'wallet_path': filename})
347 assert not storage.file_exists
349 wizard = installwizard.InstallWizard(self.config, self.network, storage)
350 wallet = wizard.run()
352 self.load_wallet(wallet)
356 def init_menubar(self):
359 file_menu = menubar.addMenu(_("&File"))
360 open_wallet_action = file_menu.addAction(_("&Open"))
361 open_wallet_action.triggered.connect(self.open_wallet)
363 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
364 new_wallet_action.triggered.connect(self.new_wallet)
366 wallet_backup = file_menu.addAction(_("&Copy"))
367 wallet_backup.triggered.connect(self.backup_wallet)
369 quit_item = file_menu.addAction(_("&Close"))
370 quit_item.triggered.connect(self.close)
372 wallet_menu = menubar.addMenu(_("&Wallet"))
374 new_contact = wallet_menu.addAction(_("&New contact"))
375 new_contact.triggered.connect(self.new_contact_dialog)
377 self.new_account = wallet_menu.addAction(_("&New account"))
378 self.new_account.triggered.connect(self.new_account_dialog)
380 wallet_menu.addSeparator()
382 pw = wallet_menu.addAction(_("&Password"))
383 pw.triggered.connect(self.change_password_dialog)
385 show_seed = wallet_menu.addAction(_("&Seed"))
386 show_seed.triggered.connect(self.show_seed_dialog)
388 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
389 show_mpk.triggered.connect(self.show_master_public_key)
391 wallet_menu.addSeparator()
393 labels_menu = wallet_menu.addMenu(_("&Labels"))
394 import_labels = labels_menu.addAction(_("&Import"))
395 import_labels.triggered.connect(self.do_import_labels)
396 export_labels = labels_menu.addAction(_("&Export"))
397 export_labels.triggered.connect(self.do_export_labels)
399 keys_menu = wallet_menu.addMenu(_("&Private keys"))
400 import_keys = keys_menu.addAction(_("&Import"))
401 import_keys.triggered.connect(self.do_import_privkey)
402 export_keys = keys_menu.addAction(_("&Export"))
403 export_keys.triggered.connect(self.do_export_privkeys)
405 ex_history = wallet_menu.addAction(_("&Export History"))
406 ex_history.triggered.connect(self.do_export_history)
410 tools_menu = menubar.addMenu(_("&Tools"))
412 # Settings / Preferences are all reserved keywords in OSX using this as work around
413 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
414 preferences_menu = tools_menu.addAction(preferences_name)
415 preferences_menu.triggered.connect(self.settings_dialog)
417 network = tools_menu.addAction(_("&Network"))
418 network.triggered.connect(self.run_network_dialog)
420 plugins_labels = tools_menu.addAction(_("&Plugins"))
421 plugins_labels.triggered.connect(self.plugins_dialog)
423 tools_menu.addSeparator()
425 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
427 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
428 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
430 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
431 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
433 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
435 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
436 raw_transaction_file.triggered.connect(self.do_process_from_file)
438 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
439 raw_transaction_text.triggered.connect(self.do_process_from_text)
442 help_menu = menubar.addMenu(_("&Help"))
443 show_about = help_menu.addAction(_("&About"))
444 show_about.triggered.connect(self.show_about)
445 web_open = help_menu.addAction(_("&Official website"))
446 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
448 help_menu.addSeparator()
449 doc_open = help_menu.addAction(_("&Documentation"))
450 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
451 report_bug = help_menu.addAction(_("&Report Bug"))
452 report_bug.triggered.connect(self.show_report_bug)
454 self.setMenuBar(menubar)
456 def show_about(self):
457 QMessageBox.about(self, "Electrum",
458 _("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."))
460 def show_report_bug(self):
461 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
462 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
465 def notify_transactions(self):
466 if not self.network.is_connected():
469 print_error("Notifying GUI")
470 if len(self.network.interface.pending_transactions_for_notifications) > 0:
471 # Combine the transactions if there are more then three
472 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
475 for tx in self.network.interface.pending_transactions_for_notifications:
476 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
480 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
481 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
483 self.network.interface.pending_transactions_for_notifications = []
485 for tx in self.network.interface.pending_transactions_for_notifications:
487 self.network.interface.pending_transactions_for_notifications.remove(tx)
488 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
490 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
492 def notify(self, message):
493 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
497 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
498 def getOpenFileName(self, title, filter = ""):
499 directory = self.config.get('io_dir', os.path.expanduser('~'))
500 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
501 if fileName and directory != os.path.dirname(fileName):
502 self.config.set_key('io_dir', os.path.dirname(fileName), True)
505 def getSaveFileName(self, title, filename, filter = ""):
506 directory = self.config.get('io_dir', os.path.expanduser('~'))
507 path = os.path.join( directory, filename )
508 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
509 if fileName and directory != os.path.dirname(fileName):
510 self.config.set_key('io_dir', os.path.dirname(fileName), True)
514 QMainWindow.close(self)
515 run_hook('close_main_window')
517 def connect_slots(self, sender):
518 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
519 self.previous_payto_e=''
521 def timer_actions(self):
522 if self.need_update.is_set():
524 self.need_update.clear()
525 run_hook('timer_actions')
527 def format_amount(self, x, is_diff=False, whitespaces=False):
528 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
530 def read_amount(self, x):
531 if x in['.', '']: return None
532 p = pow(10, self.decimal_point)
533 return int( p * Decimal(x) )
536 assert self.decimal_point in [5,8]
537 return "BTC" if self.decimal_point == 8 else "mBTC"
540 def update_status(self):
541 if self.network.is_connected():
542 if not self.wallet.up_to_date:
543 text = _("Synchronizing...")
544 icon = QIcon(":icons/status_waiting.png")
545 elif self.network.server_lag > 1:
546 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
547 icon = QIcon(":icons/status_lagging.png")
549 c, u = self.wallet.get_account_balance(self.current_account)
550 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
551 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
554 run_hook('set_quote_text', c+u, r)
557 text += " (%s)"%quote
559 self.tray.setToolTip(text)
560 icon = QIcon(":icons/status_connected.png")
562 text = _("Not connected")
563 icon = QIcon(":icons/status_disconnected.png")
565 self.balance_label.setText(text)
566 self.status_button.setIcon( icon )
569 def update_wallet(self):
571 if self.wallet.up_to_date or not self.network.is_connected():
572 self.update_history_tab()
573 self.update_receive_tab()
574 self.update_contacts_tab()
575 self.update_completions()
578 def create_history_tab(self):
579 self.history_list = l = MyTreeWidget(self)
581 for i,width in enumerate(self.column_widths['history']):
582 l.setColumnWidth(i, width)
583 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
584 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
585 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
587 l.customContextMenuRequested.connect(self.create_history_menu)
591 def create_history_menu(self, position):
592 self.history_list.selectedIndexes()
593 item = self.history_list.currentItem()
595 tx_hash = str(item.data(0, Qt.UserRole).toString())
596 if not tx_hash: return
598 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
599 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
600 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
601 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
604 def show_transaction(self, tx):
605 import transaction_dialog
606 d = transaction_dialog.TxDialog(tx, self)
609 def tx_label_clicked(self, item, column):
610 if column==2 and item.isSelected():
612 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
613 self.history_list.editItem( item, column )
614 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
617 def tx_label_changed(self, item, column):
621 tx_hash = str(item.data(0, Qt.UserRole).toString())
622 tx = self.wallet.transactions.get(tx_hash)
623 text = unicode( item.text(2) )
624 self.wallet.set_label(tx_hash, text)
626 item.setForeground(2, QBrush(QColor('black')))
628 text = self.wallet.get_default_label(tx_hash)
629 item.setText(2, text)
630 item.setForeground(2, QBrush(QColor('gray')))
634 def edit_label(self, is_recv):
635 l = self.receive_list if is_recv else self.contacts_list
636 item = l.currentItem()
637 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
638 l.editItem( item, 1 )
639 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
643 def address_label_clicked(self, item, column, l, column_addr, column_label):
644 if column == column_label and item.isSelected():
645 is_editable = item.data(0, 32).toBool()
648 addr = unicode( item.text(column_addr) )
649 label = unicode( item.text(column_label) )
650 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
651 l.editItem( item, column )
652 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
655 def address_label_changed(self, item, column, l, column_addr, column_label):
656 if column == column_label:
657 addr = unicode( item.text(column_addr) )
658 text = unicode( item.text(column_label) )
659 is_editable = item.data(0, 32).toBool()
663 changed = self.wallet.set_label(addr, text)
665 self.update_history_tab()
666 self.update_completions()
668 self.current_item_changed(item)
670 run_hook('item_changed', item, column)
673 def current_item_changed(self, a):
674 run_hook('current_item_changed', a)
678 def update_history_tab(self):
680 self.history_list.clear()
681 for item in self.wallet.get_tx_history(self.current_account):
682 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
685 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
687 time_str = _("unknown")
690 time_str = 'unverified'
691 icon = QIcon(":icons/unconfirmed.png")
694 icon = QIcon(":icons/unconfirmed.png")
696 icon = QIcon(":icons/clock%d.png"%conf)
698 icon = QIcon(":icons/confirmed.png")
700 if value is not None:
701 v_str = self.format_amount(value, True, whitespaces=True)
705 balance_str = self.format_amount(balance, whitespaces=True)
708 label, is_default_label = self.wallet.get_label(tx_hash)
710 label = _('Pruned transaction outputs')
711 is_default_label = False
713 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
714 item.setFont(2, QFont(MONOSPACE_FONT))
715 item.setFont(3, QFont(MONOSPACE_FONT))
716 item.setFont(4, QFont(MONOSPACE_FONT))
718 item.setForeground(3, QBrush(QColor("#BC1E1E")))
720 item.setData(0, Qt.UserRole, tx_hash)
721 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
723 item.setForeground(2, QBrush(QColor('grey')))
725 item.setIcon(0, icon)
726 self.history_list.insertTopLevelItem(0,item)
729 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
732 def create_send_tab(self):
737 grid.setColumnMinimumWidth(3,300)
738 grid.setColumnStretch(5,1)
741 self.payto_e = QLineEdit()
742 grid.addWidget(QLabel(_('Pay to')), 1, 0)
743 grid.addWidget(self.payto_e, 1, 1, 1, 3)
745 grid.addWidget(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)')), 1, 4)
747 completer = QCompleter()
748 completer.setCaseSensitivity(False)
749 self.payto_e.setCompleter(completer)
750 completer.setModel(self.completions)
752 self.message_e = QLineEdit()
753 grid.addWidget(QLabel(_('Description')), 2, 0)
754 grid.addWidget(self.message_e, 2, 1, 1, 3)
755 grid.addWidget(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.')), 2, 4)
757 self.amount_e = AmountEdit(self.base_unit)
758 grid.addWidget(QLabel(_('Amount')), 3, 0)
759 grid.addWidget(self.amount_e, 3, 1, 1, 2)
760 grid.addWidget(HelpButton(
761 _('Amount to be sent.') + '\n\n' \
762 + _('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.') \
763 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
765 self.fee_e = AmountEdit(self.base_unit)
766 grid.addWidget(QLabel(_('Fee')), 4, 0)
767 grid.addWidget(self.fee_e, 4, 1, 1, 2)
768 grid.addWidget(HelpButton(
769 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
770 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
771 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
774 self.send_button = EnterButton(_("Send"), self.do_send)
775 grid.addWidget(self.send_button, 6, 1)
777 b = EnterButton(_("Clear"),self.do_clear)
778 grid.addWidget(b, 6, 2)
780 self.payto_sig = QLabel('')
781 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
783 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
784 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
793 def entry_changed( is_fee ):
794 self.funds_error = False
796 if self.amount_e.is_shortcut:
797 self.amount_e.is_shortcut = False
798 c, u = self.wallet.get_account_balance(self.current_account)
799 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
800 fee = self.wallet.estimated_fee(inputs)
802 self.amount_e.setText( self.format_amount(amount) )
803 self.fee_e.setText( self.format_amount( fee ) )
806 amount = self.read_amount(str(self.amount_e.text()))
807 fee = self.read_amount(str(self.fee_e.text()))
809 if not is_fee: fee = None
812 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
814 self.fee_e.setText( self.format_amount( fee ) )
817 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
821 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
822 self.funds_error = True
823 text = _( "Not enough funds" )
824 c, u = self.wallet.get_frozen_balance()
825 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
827 self.statusBar().showMessage(text)
828 self.amount_e.setPalette(palette)
829 self.fee_e.setPalette(palette)
831 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
832 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
834 run_hook('create_send_tab', grid)
838 def update_completions(self):
840 for addr,label in self.wallet.labels.items():
841 if addr in self.wallet.addressbook:
842 l.append( label + ' <' + addr + '>')
844 run_hook('update_completions', l)
845 self.completions.setStringList(l)
849 return lambda s, *args: s.do_protect(func, args)
854 label = unicode( self.message_e.text() )
855 r = unicode( self.payto_e.text() )
858 # label or alias, with address in brackets
859 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
860 to_address = m.group(2) if m else r
862 if not is_valid(to_address):
863 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
867 amount = self.read_amount(unicode( self.amount_e.text()))
869 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
872 fee = self.read_amount(unicode( self.fee_e.text()))
874 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
877 confirm_amount = self.config.get('confirm_amount', 100000000)
878 if amount >= confirm_amount:
879 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
882 self.send_tx(to_address, amount, fee, label)
886 def send_tx(self, to_address, amount, fee, label, password):
889 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
890 except BaseException, e:
891 traceback.print_exc(file=sys.stdout)
892 self.show_message(str(e))
895 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
896 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
900 self.wallet.set_label(tx.hash(), label)
903 h = self.wallet.send_tx(tx)
904 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
905 status, msg = self.wallet.receive_tx( h )
907 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
909 self.update_contacts_tab()
911 QMessageBox.warning(self, _('Error'), msg, _('OK'))
913 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
915 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
916 with open(fileName,'w') as f:
917 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
918 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
920 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
922 # add recipient to addressbook
923 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
924 self.wallet.addressbook.append(to_address)
929 def set_url(self, url):
930 address, amount, label, message, signature, identity, url = util.parse_url(url)
932 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
935 self.mini.set_payment_fields(address, amount)
937 if label and self.wallet.labels.get(address) != label:
938 if self.question('Give label "%s" to address %s ?'%(label,address)):
939 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
940 self.wallet.addressbook.append(address)
941 self.wallet.set_label(address, label)
943 run_hook('set_url', url, self.show_message, self.question)
945 self.tabs.setCurrentIndex(1)
946 label = self.wallet.labels.get(address)
947 m_addr = label + ' <'+ address +'>' if label else address
948 self.payto_e.setText(m_addr)
950 self.message_e.setText(message)
952 self.amount_e.setText(amount)
955 self.set_frozen(self.payto_e,True)
956 self.set_frozen(self.amount_e,True)
957 self.set_frozen(self.message_e,True)
958 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
960 self.payto_sig.setVisible(False)
963 self.payto_sig.setVisible(False)
964 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
966 self.set_frozen(e,False)
969 def set_frozen(self,entry,frozen):
971 entry.setReadOnly(True)
972 entry.setFrame(False)
974 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
975 entry.setPalette(palette)
977 entry.setReadOnly(False)
980 palette.setColor(entry.backgroundRole(), QColor('white'))
981 entry.setPalette(palette)
984 def toggle_freeze(self,addr):
986 if addr in self.wallet.frozen_addresses:
987 self.wallet.unfreeze(addr)
989 self.wallet.freeze(addr)
990 self.update_receive_tab()
992 def toggle_priority(self,addr):
994 if addr in self.wallet.prioritized_addresses:
995 self.wallet.unprioritize(addr)
997 self.wallet.prioritize(addr)
998 self.update_receive_tab()
1001 def create_list_tab(self, headers):
1002 "generic tab creation method"
1003 l = MyTreeWidget(self)
1004 l.setColumnCount( len(headers) )
1005 l.setHeaderLabels( headers )
1008 vbox = QVBoxLayout()
1015 vbox.addWidget(buttons)
1017 hbox = QHBoxLayout()
1020 buttons.setLayout(hbox)
1025 def create_receive_tab(self):
1026 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1027 l.setContextMenuPolicy(Qt.CustomContextMenu)
1028 l.customContextMenuRequested.connect(self.create_receive_menu)
1029 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1030 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1031 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1032 self.receive_list = l
1033 self.receive_buttons_hbox = hbox
1040 def save_column_widths(self):
1041 self.column_widths["receive"] = []
1042 for i in range(self.receive_list.columnCount() -1):
1043 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1045 self.column_widths["history"] = []
1046 for i in range(self.history_list.columnCount() - 1):
1047 self.column_widths["history"].append(self.history_list.columnWidth(i))
1049 self.column_widths["contacts"] = []
1050 for i in range(self.contacts_list.columnCount() - 1):
1051 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1053 self.config.set_key("column_widths_2", self.column_widths, True)
1056 def create_contacts_tab(self):
1057 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1058 l.setContextMenuPolicy(Qt.CustomContextMenu)
1059 l.customContextMenuRequested.connect(self.create_contact_menu)
1060 for i,width in enumerate(self.column_widths['contacts']):
1061 l.setColumnWidth(i, width)
1063 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1064 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1065 self.contacts_list = l
1066 self.contacts_buttons_hbox = hbox
1071 def delete_imported_key(self, addr):
1072 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1073 self.wallet.delete_imported_key(addr)
1074 self.update_receive_tab()
1075 self.update_history_tab()
1077 def edit_account_label(self, k):
1078 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1080 label = unicode(text)
1081 self.wallet.set_label(k,label)
1082 self.update_receive_tab()
1084 def account_set_expanded(self, item, k, b):
1086 self.accounts_expanded[k] = b
1088 def create_account_menu(self, position, k, item):
1090 if item.isExpanded():
1091 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1093 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1094 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1095 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1096 if k in self.pending_accounts:
1097 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1098 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1100 def delete_pending_account(self, k):
1101 self.pending_accounts.pop(k)
1102 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1103 self.update_receive_tab()
1105 def create_receive_menu(self, position):
1106 # fixme: this function apparently has a side effect.
1107 # if it is not called the menu pops up several times
1108 #self.receive_list.selectedIndexes()
1110 item = self.receive_list.itemAt(position)
1113 addr = unicode(item.text(0))
1114 if not is_valid(addr):
1115 k = str(item.data(0,32).toString())
1117 self.create_account_menu(position, k, item)
1119 item.setExpanded(not item.isExpanded())
1123 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1124 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1125 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1126 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1127 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1128 if addr in self.wallet.imported_keys:
1129 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1131 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1132 menu.addAction(t, lambda: self.toggle_freeze(addr))
1133 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1134 menu.addAction(t, lambda: self.toggle_priority(addr))
1136 run_hook('receive_menu', menu)
1137 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1140 def payto(self, addr):
1142 label = self.wallet.labels.get(addr)
1143 m_addr = label + ' <' + addr + '>' if label else addr
1144 self.tabs.setCurrentIndex(1)
1145 self.payto_e.setText(m_addr)
1146 self.amount_e.setFocus()
1149 def delete_contact(self, x):
1150 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1151 self.wallet.delete_contact(x)
1152 self.wallet.set_label(x, None)
1153 self.update_history_tab()
1154 self.update_contacts_tab()
1155 self.update_completions()
1158 def create_contact_menu(self, position):
1159 item = self.contacts_list.itemAt(position)
1161 addr = unicode(item.text(0))
1162 label = unicode(item.text(1))
1163 is_editable = item.data(0,32).toBool()
1164 payto_addr = item.data(0,33).toString()
1166 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1167 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1168 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1170 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1171 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1173 run_hook('create_contact_menu', menu, item)
1174 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1177 def update_receive_item(self, item):
1178 item.setFont(0, QFont(MONOSPACE_FONT))
1179 address = str(item.data(0,0).toString())
1180 label = self.wallet.labels.get(address,'')
1181 item.setData(1,0,label)
1182 item.setData(0,32, True) # is editable
1184 run_hook('update_receive_item', address, item)
1186 if not self.wallet.is_mine(address): return
1188 c, u = self.wallet.get_addr_balance(address)
1189 balance = self.format_amount(c + u)
1190 item.setData(2,0,balance)
1192 if address in self.wallet.frozen_addresses:
1193 item.setBackgroundColor(0, QColor('lightblue'))
1194 elif address in self.wallet.prioritized_addresses:
1195 item.setBackgroundColor(0, QColor('lightgreen'))
1198 def update_receive_tab(self):
1199 l = self.receive_list
1202 l.setColumnHidden(2, False)
1203 l.setColumnHidden(3, False)
1204 for i,width in enumerate(self.column_widths['receive']):
1205 l.setColumnWidth(i, width)
1207 if self.current_account is None:
1208 account_items = self.wallet.accounts.items()
1209 elif self.current_account != -1:
1210 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1214 for k, account in account_items:
1215 name = self.wallet.get_account_name(k)
1216 c,u = self.wallet.get_account_balance(k)
1217 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1218 l.addTopLevelItem(account_item)
1219 account_item.setExpanded(self.accounts_expanded.get(k, True))
1220 account_item.setData(0, 32, k)
1222 if not self.wallet.is_seeded(k):
1223 icon = QIcon(":icons/key.png")
1224 account_item.setIcon(0, icon)
1226 for is_change in ([0,1]):
1227 name = _("Receiving") if not is_change else _("Change")
1228 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1229 account_item.addChild(seq_item)
1230 if not is_change: seq_item.setExpanded(True)
1235 for address in account.get_addresses(is_change):
1236 h = self.wallet.history.get(address,[])
1240 if gap > self.wallet.gap_limit:
1245 num_tx = '*' if h == ['*'] else "%d"%len(h)
1246 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1247 self.update_receive_item(item)
1249 item.setBackgroundColor(1, QColor('red'))
1250 seq_item.addChild(item)
1253 for k, addr in self.pending_accounts.items():
1254 if k in self.wallet.accounts:
1255 self.pending_accounts.pop(k)
1256 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1257 name = self.wallet.labels.get(k,'')
1258 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1259 self.update_receive_item(item)
1260 l.addTopLevelItem(account_item)
1261 account_item.setExpanded(True)
1262 account_item.setData(0, 32, k)
1263 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1264 account_item.addChild(item)
1265 self.update_receive_item(item)
1268 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1269 c,u = self.wallet.get_imported_balance()
1270 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1271 l.addTopLevelItem(account_item)
1272 account_item.setExpanded(True)
1273 for address in self.wallet.imported_keys.keys():
1274 item = QTreeWidgetItem( [ address, '', '', ''] )
1275 self.update_receive_item(item)
1276 account_item.addChild(item)
1279 # we use column 1 because column 0 may be hidden
1280 l.setCurrentItem(l.topLevelItem(0),1)
1283 def update_contacts_tab(self):
1284 l = self.contacts_list
1287 for address in self.wallet.addressbook:
1288 label = self.wallet.labels.get(address,'')
1289 n = self.wallet.get_num_tx(address)
1290 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1291 item.setFont(0, QFont(MONOSPACE_FONT))
1292 # 32 = label can be edited (bool)
1293 item.setData(0,32, True)
1295 item.setData(0,33, address)
1296 l.addTopLevelItem(item)
1298 run_hook('update_contacts_tab', l)
1299 l.setCurrentItem(l.topLevelItem(0))
1303 def create_console_tab(self):
1304 from console import Console
1305 self.console = console = Console()
1309 def update_console(self):
1310 console = self.console
1311 console.history = self.config.get("console-history",[])
1312 console.history_index = len(console.history)
1314 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1315 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1317 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1319 def mkfunc(f, method):
1320 return lambda *args: apply( f, (method, args, self.password_dialog ))
1322 if m[0]=='_' or m in ['network','wallet']: continue
1323 methods[m] = mkfunc(c._run, m)
1325 console.updateNamespace(methods)
1328 def change_account(self,s):
1329 if s == _("All accounts"):
1330 self.current_account = None
1332 accounts = self.wallet.get_account_names()
1333 for k, v in accounts.items():
1335 self.current_account = k
1336 self.update_history_tab()
1337 self.update_status()
1338 self.update_receive_tab()
1340 def create_status_bar(self):
1343 sb.setFixedHeight(35)
1344 qtVersion = qVersion()
1346 self.balance_label = QLabel("")
1347 sb.addWidget(self.balance_label)
1349 from version_getter import UpdateLabel
1350 self.updatelabel = UpdateLabel(self.config, sb)
1352 self.account_selector = QComboBox()
1353 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1354 sb.addPermanentWidget(self.account_selector)
1356 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1357 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1359 self.lock_icon = QIcon()
1360 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1361 sb.addPermanentWidget( self.password_button )
1363 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1364 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1365 sb.addPermanentWidget( self.seed_button )
1366 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1367 sb.addPermanentWidget( self.status_button )
1369 run_hook('create_status_bar', (sb,))
1371 self.setStatusBar(sb)
1374 def update_lock_icon(self):
1375 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1376 self.password_button.setIcon( icon )
1379 def update_buttons_on_seed(self):
1380 if not self.wallet.is_watching_only():
1381 self.seed_button.show()
1382 self.password_button.show()
1383 self.send_button.setText(_("Send"))
1385 self.password_button.hide()
1386 self.seed_button.hide()
1387 self.send_button.setText(_("Create unsigned transaction"))
1390 def change_password_dialog(self):
1391 from password_dialog import PasswordDialog
1392 d = PasswordDialog(self.wallet, self)
1394 self.update_lock_icon()
1397 def new_contact_dialog(self):
1400 vbox = QVBoxLayout(d)
1401 vbox.addWidget(QLabel(_('New Contact')+':'))
1403 grid = QGridLayout()
1406 grid.addWidget(QLabel(_("Address")), 1, 0)
1407 grid.addWidget(line1, 1, 1)
1408 grid.addWidget(QLabel(_("Name")), 2, 0)
1409 grid.addWidget(line2, 2, 1)
1411 vbox.addLayout(grid)
1412 vbox.addLayout(ok_cancel_buttons(d))
1417 address = str(line1.text())
1418 label = unicode(line2.text())
1420 if not is_valid(address):
1421 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1424 self.wallet.add_contact(address)
1426 self.wallet.set_label(address, label)
1428 self.update_contacts_tab()
1429 self.update_history_tab()
1430 self.update_completions()
1431 self.tabs.setCurrentIndex(3)
1434 def new_account_dialog(self):
1436 dialog = QDialog(self)
1438 dialog.setWindowTitle(_("New Account"))
1440 vbox = QVBoxLayout()
1441 vbox.addWidget(QLabel(_('Account name')+':'))
1444 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1445 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1450 vbox.addLayout(ok_cancel_buttons(dialog))
1451 dialog.setLayout(vbox)
1455 name = str(e.text())
1458 k, addr = self.wallet.new_account_address()
1459 self.wallet.set_label(k, name)
1460 self.pending_accounts[k] = addr
1461 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1462 self.update_receive_tab()
1463 self.tabs.setCurrentIndex(2)
1467 def show_master_public_key_old(self):
1468 dialog = QDialog(self)
1470 dialog.setWindowTitle(_("Master Public Key"))
1472 main_text = QTextEdit()
1473 main_text.setText(self.wallet.get_master_public_key())
1474 main_text.setReadOnly(True)
1475 main_text.setMaximumHeight(170)
1476 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1478 ok_button = QPushButton(_("OK"))
1479 ok_button.setDefault(True)
1480 ok_button.clicked.connect(dialog.accept)
1482 main_layout = QGridLayout()
1483 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1485 main_layout.addWidget(main_text, 1, 0)
1486 main_layout.addWidget(qrw, 1, 1 )
1488 vbox.addLayout(close_button(dialog))
1489 dialog.setLayout(vbox)
1493 def show_master_public_key(self):
1495 if self.wallet.seed_version == 4:
1496 self.show_master_public_keys_old()
1499 dialog = QDialog(self)
1501 dialog.setWindowTitle(_("Master Public Keys"))
1503 chain_text = QTextEdit()
1504 chain_text.setReadOnly(True)
1505 chain_text.setMaximumHeight(170)
1506 chain_qrw = QRCodeWidget()
1508 mpk_text = QTextEdit()
1509 mpk_text.setReadOnly(True)
1510 mpk_text.setMaximumHeight(170)
1511 mpk_qrw = QRCodeWidget()
1513 main_layout = QGridLayout()
1515 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1516 main_layout.addWidget(mpk_text, 1, 1)
1517 main_layout.addWidget(mpk_qrw, 1, 2)
1519 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1520 main_layout.addWidget(chain_text, 2, 1)
1521 main_layout.addWidget(chain_qrw, 2, 2)
1524 c, K, cK = self.wallet.master_public_keys[str(key)]
1525 chain_text.setText(c)
1526 chain_qrw.set_addr(c)
1527 chain_qrw.update_qr()
1532 key_selector = QComboBox()
1533 keys = sorted(self.wallet.master_public_keys.keys())
1534 key_selector.addItems(keys)
1536 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1537 main_layout.addWidget(key_selector, 0, 1)
1538 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1542 vbox = QVBoxLayout()
1543 vbox.addLayout(main_layout)
1544 vbox.addLayout(close_button(dialog))
1546 dialog.setLayout(vbox)
1551 def show_seed_dialog(self, password):
1552 if self.wallet.is_watching_only():
1553 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1556 if self.wallet.seed:
1558 seed = self.wallet.decode_seed(password)
1560 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1562 from seed_dialog import SeedDialog
1563 d = SeedDialog(self, seed, self.wallet.imported_keys)
1567 for k in self.wallet.master_private_keys.keys():
1568 pk = self.wallet.get_master_private_key(k, password)
1570 from seed_dialog import PrivateKeysDialog
1571 d = PrivateKeysDialog(self,l)
1578 def show_qrcode(self, data, title = _("QR code")):
1582 d.setWindowTitle(title)
1583 d.setMinimumSize(270, 300)
1584 vbox = QVBoxLayout()
1585 qrw = QRCodeWidget(data)
1586 vbox.addWidget(qrw, 1)
1587 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1588 hbox = QHBoxLayout()
1592 filename = "qrcode.bmp"
1593 bmp.save_qrcode(qrw.qr, filename)
1594 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1596 b = QPushButton(_("Save"))
1598 b.clicked.connect(print_qr)
1600 b = QPushButton(_("Close"))
1602 b.clicked.connect(d.accept)
1605 vbox.addLayout(hbox)
1610 def do_protect(self, func, args):
1611 if self.wallet.use_encryption:
1612 password = self.password_dialog()
1618 if args != (False,):
1619 args = (self,) + args + (password,)
1621 args = (self,password)
1626 def show_private_key(self, address, password):
1627 if not address: return
1629 pk_list = self.wallet.get_private_key(address, password)
1630 except BaseException, e:
1631 self.show_message(str(e))
1633 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1637 def do_sign(self, address, message, signature, password):
1638 message = unicode(message.toPlainText())
1639 message = message.encode('utf-8')
1641 sig = self.wallet.sign_message(str(address.text()), message, password)
1642 signature.setText(sig)
1643 except BaseException, e:
1644 self.show_message(str(e))
1646 def sign_message(self, address):
1647 if not address: return
1650 d.setWindowTitle(_('Sign Message'))
1651 d.setMinimumSize(410, 290)
1653 tab_widget = QTabWidget()
1655 layout = QGridLayout(tab)
1657 sign_address = QLineEdit()
1659 sign_address.setText(address)
1660 layout.addWidget(QLabel(_('Address')), 1, 0)
1661 layout.addWidget(sign_address, 1, 1)
1663 sign_message = QTextEdit()
1664 layout.addWidget(QLabel(_('Message')), 2, 0)
1665 layout.addWidget(sign_message, 2, 1)
1666 layout.setRowStretch(2,3)
1668 sign_signature = QTextEdit()
1669 layout.addWidget(QLabel(_('Signature')), 3, 0)
1670 layout.addWidget(sign_signature, 3, 1)
1671 layout.setRowStretch(3,1)
1674 hbox = QHBoxLayout()
1675 b = QPushButton(_("Sign"))
1677 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1678 b = QPushButton(_("Close"))
1679 b.clicked.connect(d.accept)
1681 layout.addLayout(hbox, 4, 1)
1682 tab_widget.addTab(tab, _("Sign"))
1686 layout = QGridLayout(tab)
1688 verify_address = QLineEdit()
1689 layout.addWidget(QLabel(_('Address')), 1, 0)
1690 layout.addWidget(verify_address, 1, 1)
1692 verify_message = QTextEdit()
1693 layout.addWidget(QLabel(_('Message')), 2, 0)
1694 layout.addWidget(verify_message, 2, 1)
1695 layout.setRowStretch(2,3)
1697 verify_signature = QTextEdit()
1698 layout.addWidget(QLabel(_('Signature')), 3, 0)
1699 layout.addWidget(verify_signature, 3, 1)
1700 layout.setRowStretch(3,1)
1703 message = unicode(verify_message.toPlainText())
1704 message = message.encode('utf-8')
1705 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1706 self.show_message(_("Signature verified"))
1708 self.show_message(_("Error: wrong signature"))
1710 hbox = QHBoxLayout()
1711 b = QPushButton(_("Verify"))
1712 b.clicked.connect(do_verify)
1714 b = QPushButton(_("Close"))
1715 b.clicked.connect(d.accept)
1717 layout.addLayout(hbox, 4, 1)
1718 tab_widget.addTab(tab, _("Verify"))
1720 vbox = QVBoxLayout()
1721 vbox.addWidget(tab_widget)
1728 def question(self, msg):
1729 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1731 def show_message(self, msg):
1732 QMessageBox.information(self, _('Message'), msg, _('OK'))
1734 def password_dialog(self ):
1741 vbox = QVBoxLayout()
1742 msg = _('Please enter your password')
1743 vbox.addWidget(QLabel(msg))
1745 grid = QGridLayout()
1747 grid.addWidget(QLabel(_('Password')), 1, 0)
1748 grid.addWidget(pw, 1, 1)
1749 vbox.addLayout(grid)
1751 vbox.addLayout(ok_cancel_buttons(d))
1754 run_hook('password_dialog', pw, grid, 1)
1755 if not d.exec_(): return
1756 return unicode(pw.text())
1765 def tx_from_text(self, txt):
1766 "json or raw hexadecimal"
1769 tx = Transaction(txt)
1775 tx_dict = json.loads(str(txt))
1776 assert "hex" in tx_dict.keys()
1777 assert "complete" in tx_dict.keys()
1778 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1779 if not tx_dict["complete"]:
1780 assert "input_info" in tx_dict.keys()
1781 input_info = json.loads(tx_dict['input_info'])
1782 tx.add_input_info(input_info)
1787 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1791 def read_tx_from_file(self):
1792 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1796 with open(fileName, "r") as f:
1797 file_content = f.read()
1798 except (ValueError, IOError, os.error), reason:
1799 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1801 return self.tx_from_text(file_content)
1805 def sign_raw_transaction(self, tx, input_info, password):
1806 self.wallet.signrawtransaction(tx, input_info, [], password)
1808 def do_process_from_text(self):
1809 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1812 tx = self.tx_from_text(text)
1814 self.show_transaction(tx)
1816 def do_process_from_file(self):
1817 tx = self.read_tx_from_file()
1819 self.show_transaction(tx)
1821 def do_process_from_csvReader(self, csvReader):
1824 for row in csvReader:
1826 amount = float(row[1])
1827 amount = int(100000000*amount)
1828 outputs.append((address, amount))
1829 except (ValueError, IOError, os.error), reason:
1830 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1834 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1835 except BaseException, e:
1836 self.show_message(str(e))
1839 self.show_transaction(tx)
1841 def do_process_from_csv_file(self):
1842 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1846 with open(fileName, "r") as f:
1847 csvReader = csv.reader(f)
1848 self.do_process_from_csvReader(csvReader)
1849 except (ValueError, IOError, os.error), reason:
1850 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1853 def do_process_from_csv_text(self):
1854 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1855 + _("Format: address, amount. One output per line"), _("Load CSV"))
1858 f = StringIO.StringIO(text)
1859 csvReader = csv.reader(f)
1860 self.do_process_from_csvReader(csvReader)
1865 def do_export_privkeys(self, password):
1866 self.show_message("%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), _("Exposing a single private key can compromise your entire wallet!"), _("In particular, DO NOT use 'redeem private key' services proposed by third parties.")))
1869 select_export = _('Select file to export your private keys to')
1870 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1872 with open(fileName, "w+") as csvfile:
1873 transaction = csv.writer(csvfile)
1874 transaction.writerow(["address", "private_key"])
1876 addresses = self.wallet.addresses(True)
1878 for addr in addresses:
1879 pk = "".join(self.wallet.get_private_key(addr, password))
1880 transaction.writerow(["%34s"%addr,pk])
1882 self.show_message(_("Private keys exported."))
1884 except (IOError, os.error), reason:
1885 export_error_label = _("Electrum was unable to produce a private key-export.")
1886 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1888 except BaseException, e:
1889 self.show_message(str(e))
1893 def do_import_labels(self):
1894 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1895 if not labelsFile: return
1897 f = open(labelsFile, 'r')
1900 for key, value in json.loads(data).items():
1901 self.wallet.set_label(key, value)
1902 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1903 except (IOError, os.error), reason:
1904 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1907 def do_export_labels(self):
1908 labels = self.wallet.labels
1910 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1912 with open(fileName, 'w+') as f:
1913 json.dump(labels, f)
1914 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1915 except (IOError, os.error), reason:
1916 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1919 def do_export_history(self):
1920 from lite_window import csv_transaction
1921 csv_transaction(self.wallet)
1925 def do_import_privkey(self, password):
1926 if not self.wallet.imported_keys:
1927 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1928 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1929 + _('Are you sure you understand what you are doing?'), 3, 4)
1932 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1935 text = str(text).split()
1940 addr = self.wallet.import_key(key, password)
1941 except BaseException as e:
1947 addrlist.append(addr)
1949 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1951 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1952 self.update_receive_tab()
1953 self.update_history_tab()
1956 def settings_dialog(self):
1958 d.setWindowTitle(_('Electrum Settings'))
1960 vbox = QVBoxLayout()
1961 grid = QGridLayout()
1962 grid.setColumnStretch(0,1)
1964 nz_label = QLabel(_('Display zeros') + ':')
1965 grid.addWidget(nz_label, 0, 0)
1966 nz_e = AmountEdit(None,True)
1967 nz_e.setText("%d"% self.num_zeros)
1968 grid.addWidget(nz_e, 0, 1)
1969 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1970 grid.addWidget(HelpButton(msg), 0, 2)
1971 if not self.config.is_modifiable('num_zeros'):
1972 for w in [nz_e, nz_label]: w.setEnabled(False)
1974 lang_label=QLabel(_('Language') + ':')
1975 grid.addWidget(lang_label, 1, 0)
1976 lang_combo = QComboBox()
1977 from electrum.i18n import languages
1978 lang_combo.addItems(languages.values())
1980 index = languages.keys().index(self.config.get("language",''))
1983 lang_combo.setCurrentIndex(index)
1984 grid.addWidget(lang_combo, 1, 1)
1985 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1986 if not self.config.is_modifiable('language'):
1987 for w in [lang_combo, lang_label]: w.setEnabled(False)
1990 fee_label = QLabel(_('Transaction fee') + ':')
1991 grid.addWidget(fee_label, 2, 0)
1992 fee_e = AmountEdit(self.base_unit)
1993 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1994 grid.addWidget(fee_e, 2, 1)
1995 msg = _('Fee per kilobyte of transaction.') + ' ' \
1996 + _('Recommended value') + ': ' + self.format_amount(50000)
1997 grid.addWidget(HelpButton(msg), 2, 2)
1998 if not self.config.is_modifiable('fee_per_kb'):
1999 for w in [fee_e, fee_label]: w.setEnabled(False)
2001 units = ['BTC', 'mBTC']
2002 unit_label = QLabel(_('Base unit') + ':')
2003 grid.addWidget(unit_label, 3, 0)
2004 unit_combo = QComboBox()
2005 unit_combo.addItems(units)
2006 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2007 grid.addWidget(unit_combo, 3, 1)
2008 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2009 + '\n1BTC=1000mBTC.\n' \
2010 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2012 usechange_cb = QCheckBox(_('Use change addresses'))
2013 usechange_cb.setChecked(self.wallet.use_change)
2014 grid.addWidget(usechange_cb, 4, 0)
2015 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2016 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2018 grid.setRowStretch(5,1)
2020 vbox.addLayout(grid)
2021 vbox.addLayout(ok_cancel_buttons(d))
2025 if not d.exec_(): return
2027 fee = unicode(fee_e.text())
2029 fee = self.read_amount(fee)
2031 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2034 self.wallet.set_fee(fee)
2036 nz = unicode(nz_e.text())
2041 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2044 if self.num_zeros != nz:
2046 self.config.set_key('num_zeros', nz, True)
2047 self.update_history_tab()
2048 self.update_receive_tab()
2050 usechange_result = usechange_cb.isChecked()
2051 if self.wallet.use_change != usechange_result:
2052 self.wallet.use_change = usechange_result
2053 self.config.set_key('use_change', self.wallet.use_change, True)
2055 unit_result = units[unit_combo.currentIndex()]
2056 if self.base_unit() != unit_result:
2057 self.decimal_point = 8 if unit_result == 'BTC' else 5
2058 self.config.set_key('decimal_point', self.decimal_point, True)
2059 self.update_history_tab()
2060 self.update_status()
2062 need_restart = False
2064 lang_request = languages.keys()[lang_combo.currentIndex()]
2065 if lang_request != self.config.get('language'):
2066 self.config.set_key("language", lang_request, True)
2069 run_hook('close_settings_dialog')
2072 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2075 def run_network_dialog(self):
2076 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2078 def closeEvent(self, event):
2080 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2081 self.save_column_widths()
2082 self.config.set_key("console-history", self.console.history[-50:], True)
2083 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2088 def plugins_dialog(self):
2089 from electrum.plugins import plugins
2092 d.setWindowTitle(_('Electrum Plugins'))
2095 vbox = QVBoxLayout(d)
2098 scroll = QScrollArea()
2099 scroll.setEnabled(True)
2100 scroll.setWidgetResizable(True)
2101 scroll.setMinimumSize(400,250)
2102 vbox.addWidget(scroll)
2106 w.setMinimumHeight(len(plugins)*35)
2108 grid = QGridLayout()
2109 grid.setColumnStretch(0,1)
2112 def do_toggle(cb, p, w):
2115 if w: w.setEnabled(r)
2117 def mk_toggle(cb, p, w):
2118 return lambda: do_toggle(cb,p,w)
2120 for i, p in enumerate(plugins):
2122 cb = QCheckBox(p.fullname())
2123 cb.setDisabled(not p.is_available())
2124 cb.setChecked(p.is_enabled())
2125 grid.addWidget(cb, i, 0)
2126 if p.requires_settings():
2127 w = p.settings_widget(self)
2128 w.setEnabled( p.is_enabled() )
2129 grid.addWidget(w, i, 1)
2132 cb.clicked.connect(mk_toggle(cb,p,w))
2133 grid.addWidget(HelpButton(p.description()), i, 2)
2135 print_msg(_("Error: cannot display plugin"), p)
2136 traceback.print_exc(file=sys.stdout)
2137 grid.setRowStretch(i+1,1)
2139 vbox.addLayout(close_button(d))
2144 def show_account_details(self, k):
2146 d.setWindowTitle(_('Account Details'))
2149 vbox = QVBoxLayout(d)
2150 roots = self.wallet.get_roots(k)
2152 name = self.wallet.get_account_name(k)
2153 label = QLabel('Name: ' + name)
2154 vbox.addWidget(label)
2156 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2157 vbox.addWidget(QLabel('Type: ' + acctype))
2159 label = QLabel('Derivation: ' + k)
2160 vbox.addWidget(label)
2163 # mpk = self.wallet.master_public_keys[root]
2164 # text = QTextEdit()
2165 # text.setReadOnly(True)
2166 # text.setMaximumHeight(120)
2167 # text.setText(repr(mpk))
2168 # vbox.addWidget(text)
2170 vbox.addLayout(close_button(d))