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)
203 self.config.set_key('lite_mode', False, True)
208 self.config.set_key('lite_mode', True, True)
215 if not self.check_qt_version():
216 if self.config.get('lite_mode') is True:
217 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
218 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
219 self.config.set_key('lite_mode', False, True)
225 actuator = lite_window.MiniActuator(self)
227 # Should probably not modify the current path but instead
228 # change the behaviour of rsrc(...)
229 old_path = QDir.currentPath()
230 actuator.load_theme()
232 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
234 driver = lite_window.MiniDriver(self, self.mini)
236 # Reset path back to original value now that loading the GUI
238 QDir.setCurrent(old_path)
240 if self.config.get('lite_mode') is True:
246 def check_qt_version(self):
247 qtVersion = qVersion()
248 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
251 def update_account_selector(self):
253 accounts = self.wallet.get_account_names()
254 self.account_selector.clear()
255 if len(accounts) > 1:
256 self.account_selector.addItems([_("All accounts")] + accounts.values())
257 self.account_selector.setCurrentIndex(0)
258 self.account_selector.show()
260 self.account_selector.hide()
263 def load_wallet(self, wallet):
266 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
267 self.current_account = self.wallet.storage.get("current_account", None)
269 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
270 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
271 self.setWindowTitle( title )
273 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
274 self.notify_transactions()
275 self.update_account_selector()
276 self.new_account.setEnabled(self.wallet.seed_version>4)
277 self.update_lock_icon()
278 self.update_buttons_on_seed()
279 self.update_console()
281 run_hook('load_wallet', wallet)
284 def select_wallet_file(self):
285 wallet_folder = self.wallet.storage.path
286 re.sub("(\/\w*.dat)$", "", wallet_folder)
287 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
291 def open_wallet(self):
293 filename = self.select_wallet_file()
297 storage = WalletStorage({'wallet_path': filename})
298 if not storage.file_exists:
299 self.show_message("file not found "+ filename)
302 self.wallet.stop_threads()
305 wallet = Wallet(storage)
306 wallet.start_threads(self.network)
308 self.load_wallet(wallet)
312 def backup_wallet(self):
314 path = self.wallet.storage.path
315 wallet_folder = os.path.dirname(path)
316 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
317 new_filename = unicode(new_filename)
318 if not ok or not new_filename:
321 new_path = os.path.join(wallet_folder, new_filename)
324 shutil.copy2(path, new_path)
325 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
326 except (IOError, os.error), reason:
327 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
330 def new_wallet(self):
333 wallet_folder = os.path.dirname(self.wallet.storage.path)
334 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
335 filename = unicode(filename)
336 if not ok or not filename:
338 filename = os.path.join(wallet_folder, filename)
340 storage = WalletStorage({'wallet_path': filename})
341 assert not storage.file_exists
343 wizard = installwizard.InstallWizard(self.config, self.network, storage)
344 wallet = wizard.run()
346 self.load_wallet(wallet)
350 def init_menubar(self):
353 file_menu = menubar.addMenu(_("&File"))
354 open_wallet_action = file_menu.addAction(_("&Open"))
355 open_wallet_action.triggered.connect(self.open_wallet)
357 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
358 new_wallet_action.triggered.connect(self.new_wallet)
360 wallet_backup = file_menu.addAction(_("&Copy"))
361 wallet_backup.triggered.connect(self.backup_wallet)
363 quit_item = file_menu.addAction(_("&Close"))
364 quit_item.triggered.connect(self.close)
366 wallet_menu = menubar.addMenu(_("&Wallet"))
368 new_contact = wallet_menu.addAction(_("&New contact"))
369 new_contact.triggered.connect(self.new_contact_dialog)
371 self.new_account = wallet_menu.addAction(_("&New account"))
372 self.new_account.triggered.connect(self.new_account_dialog)
374 wallet_menu.addSeparator()
376 pw = wallet_menu.addAction(_("&Password"))
377 pw.triggered.connect(self.change_password_dialog)
379 show_seed = wallet_menu.addAction(_("&Seed"))
380 show_seed.triggered.connect(self.show_seed_dialog)
382 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
383 show_mpk.triggered.connect(self.show_master_public_key)
385 wallet_menu.addSeparator()
387 labels_menu = wallet_menu.addMenu(_("&Labels"))
388 import_labels = labels_menu.addAction(_("&Import"))
389 import_labels.triggered.connect(self.do_import_labels)
390 export_labels = labels_menu.addAction(_("&Export"))
391 export_labels.triggered.connect(self.do_export_labels)
393 keys_menu = wallet_menu.addMenu(_("&Private keys"))
394 import_keys = keys_menu.addAction(_("&Import"))
395 import_keys.triggered.connect(self.do_import_privkey)
396 export_keys = keys_menu.addAction(_("&Export"))
397 export_keys.triggered.connect(self.do_export_privkeys)
399 ex_history = wallet_menu.addAction(_("&Export History"))
400 ex_history.triggered.connect(self.do_export_history)
404 tools_menu = menubar.addMenu(_("&Tools"))
406 # Settings / Preferences are all reserved keywords in OSX using this as work around
407 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
408 preferences_menu = tools_menu.addAction(preferences_name)
409 preferences_menu.triggered.connect(self.settings_dialog)
411 network = tools_menu.addAction(_("&Network"))
412 network.triggered.connect(self.run_network_dialog)
414 plugins_labels = tools_menu.addAction(_("&Plugins"))
415 plugins_labels.triggered.connect(self.plugins_dialog)
417 tools_menu.addSeparator()
419 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
421 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
422 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
424 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
425 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
427 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
429 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
430 raw_transaction_file.triggered.connect(self.do_process_from_file)
432 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
433 raw_transaction_text.triggered.connect(self.do_process_from_text)
436 help_menu = menubar.addMenu(_("&Help"))
437 show_about = help_menu.addAction(_("&About"))
438 show_about.triggered.connect(self.show_about)
439 web_open = help_menu.addAction(_("&Official website"))
440 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
442 help_menu.addSeparator()
443 doc_open = help_menu.addAction(_("&Documentation"))
444 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
445 report_bug = help_menu.addAction(_("&Report Bug"))
446 report_bug.triggered.connect(self.show_report_bug)
448 self.setMenuBar(menubar)
450 def show_about(self):
451 QMessageBox.about(self, "Electrum",
452 _("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."))
454 def show_report_bug(self):
455 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
456 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
459 def notify_transactions(self):
460 if not self.network.is_connected():
463 print_error("Notifying GUI")
464 if len(self.network.interface.pending_transactions_for_notifications) > 0:
465 # Combine the transactions if there are more then three
466 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
469 for tx in self.network.interface.pending_transactions_for_notifications:
470 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
474 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
475 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
477 self.network.interface.pending_transactions_for_notifications = []
479 for tx in self.network.interface.pending_transactions_for_notifications:
481 self.network.interface.pending_transactions_for_notifications.remove(tx)
482 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
484 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
486 def notify(self, message):
487 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
491 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
492 def getOpenFileName(self, title, filter = ""):
493 directory = self.config.get('io_dir', os.path.expanduser('~'))
494 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
495 if fileName and directory != os.path.dirname(fileName):
496 self.config.set_key('io_dir', os.path.dirname(fileName), True)
499 def getSaveFileName(self, title, filename, filter = ""):
500 directory = self.config.get('io_dir', os.path.expanduser('~'))
501 path = os.path.join( directory, filename )
502 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
503 if fileName and directory != os.path.dirname(fileName):
504 self.config.set_key('io_dir', os.path.dirname(fileName), True)
508 QMainWindow.close(self)
509 run_hook('close_main_window')
511 def connect_slots(self, sender):
512 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
513 self.previous_payto_e=''
515 def timer_actions(self):
516 if self.need_update.is_set():
518 self.need_update.clear()
519 run_hook('timer_actions')
521 def format_amount(self, x, is_diff=False, whitespaces=False):
522 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
524 def read_amount(self, x):
525 if x in['.', '']: return None
526 p = pow(10, self.decimal_point)
527 return int( p * Decimal(x) )
530 assert self.decimal_point in [5,8]
531 return "BTC" if self.decimal_point == 8 else "mBTC"
534 def update_status(self):
535 if self.network.is_connected():
536 if not self.wallet.up_to_date:
537 text = _("Synchronizing...")
538 icon = QIcon(":icons/status_waiting.png")
539 elif self.network.server_lag > 1:
540 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
541 icon = QIcon(":icons/status_lagging.png")
543 c, u = self.wallet.get_account_balance(self.current_account)
544 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
545 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
548 run_hook('set_quote_text', c+u, r)
551 text += " (%s)"%quote
553 self.tray.setToolTip(text)
554 icon = QIcon(":icons/status_connected.png")
556 text = _("Not connected")
557 icon = QIcon(":icons/status_disconnected.png")
559 self.balance_label.setText(text)
560 self.status_button.setIcon( icon )
563 def update_wallet(self):
565 if self.wallet.up_to_date or not self.network.is_connected():
566 self.update_history_tab()
567 self.update_receive_tab()
568 self.update_contacts_tab()
569 self.update_completions()
572 def create_history_tab(self):
573 self.history_list = l = MyTreeWidget(self)
575 for i,width in enumerate(self.column_widths['history']):
576 l.setColumnWidth(i, width)
577 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
578 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
579 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
581 l.customContextMenuRequested.connect(self.create_history_menu)
585 def create_history_menu(self, position):
586 self.history_list.selectedIndexes()
587 item = self.history_list.currentItem()
589 tx_hash = str(item.data(0, Qt.UserRole).toString())
590 if not tx_hash: return
592 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
593 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
594 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
595 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
598 def show_transaction(self, tx):
599 import transaction_dialog
600 d = transaction_dialog.TxDialog(tx, self)
603 def tx_label_clicked(self, item, column):
604 if column==2 and item.isSelected():
606 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
607 self.history_list.editItem( item, column )
608 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
611 def tx_label_changed(self, item, column):
615 tx_hash = str(item.data(0, Qt.UserRole).toString())
616 tx = self.wallet.transactions.get(tx_hash)
617 text = unicode( item.text(2) )
618 self.wallet.set_label(tx_hash, text)
620 item.setForeground(2, QBrush(QColor('black')))
622 text = self.wallet.get_default_label(tx_hash)
623 item.setText(2, text)
624 item.setForeground(2, QBrush(QColor('gray')))
628 def edit_label(self, is_recv):
629 l = self.receive_list if is_recv else self.contacts_list
630 item = l.currentItem()
631 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
632 l.editItem( item, 1 )
633 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
637 def address_label_clicked(self, item, column, l, column_addr, column_label):
638 if column == column_label and item.isSelected():
639 is_editable = item.data(0, 32).toBool()
642 addr = unicode( item.text(column_addr) )
643 label = unicode( item.text(column_label) )
644 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
645 l.editItem( item, column )
646 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
649 def address_label_changed(self, item, column, l, column_addr, column_label):
650 if column == column_label:
651 addr = unicode( item.text(column_addr) )
652 text = unicode( item.text(column_label) )
653 is_editable = item.data(0, 32).toBool()
657 changed = self.wallet.set_label(addr, text)
659 self.update_history_tab()
660 self.update_completions()
662 self.current_item_changed(item)
664 run_hook('item_changed', item, column)
667 def current_item_changed(self, a):
668 run_hook('current_item_changed', a)
672 def update_history_tab(self):
674 self.history_list.clear()
675 for item in self.wallet.get_tx_history(self.current_account):
676 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
677 time_str = _("unknown")
680 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
682 time_str = _("error")
685 time_str = 'unverified'
686 icon = QIcon(":icons/unconfirmed.png")
689 icon = QIcon(":icons/unconfirmed.png")
691 icon = QIcon(":icons/clock%d.png"%conf)
693 icon = QIcon(":icons/confirmed.png")
695 if value is not None:
696 v_str = self.format_amount(value, True, whitespaces=True)
700 balance_str = self.format_amount(balance, whitespaces=True)
703 label, is_default_label = self.wallet.get_label(tx_hash)
705 label = _('Pruned transaction outputs')
706 is_default_label = False
708 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
709 item.setFont(2, QFont(MONOSPACE_FONT))
710 item.setFont(3, QFont(MONOSPACE_FONT))
711 item.setFont(4, QFont(MONOSPACE_FONT))
713 item.setForeground(3, QBrush(QColor("#BC1E1E")))
715 item.setData(0, Qt.UserRole, tx_hash)
716 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
718 item.setForeground(2, QBrush(QColor('grey')))
720 item.setIcon(0, icon)
721 self.history_list.insertTopLevelItem(0,item)
724 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
727 def create_send_tab(self):
732 grid.setColumnMinimumWidth(3,300)
733 grid.setColumnStretch(5,1)
736 self.payto_e = QLineEdit()
737 grid.addWidget(QLabel(_('Pay to')), 1, 0)
738 grid.addWidget(self.payto_e, 1, 1, 1, 3)
740 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)
742 completer = QCompleter()
743 completer.setCaseSensitivity(False)
744 self.payto_e.setCompleter(completer)
745 completer.setModel(self.completions)
747 self.message_e = QLineEdit()
748 grid.addWidget(QLabel(_('Description')), 2, 0)
749 grid.addWidget(self.message_e, 2, 1, 1, 3)
750 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)
752 self.amount_e = AmountEdit(self.base_unit)
753 grid.addWidget(QLabel(_('Amount')), 3, 0)
754 grid.addWidget(self.amount_e, 3, 1, 1, 2)
755 grid.addWidget(HelpButton(
756 _('Amount to be sent.') + '\n\n' \
757 + _('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.') \
758 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
760 self.fee_e = AmountEdit(self.base_unit)
761 grid.addWidget(QLabel(_('Fee')), 4, 0)
762 grid.addWidget(self.fee_e, 4, 1, 1, 2)
763 grid.addWidget(HelpButton(
764 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
765 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
766 + _('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)
769 self.send_button = EnterButton(_("Send"), self.do_send)
770 grid.addWidget(self.send_button, 6, 1)
772 b = EnterButton(_("Clear"),self.do_clear)
773 grid.addWidget(b, 6, 2)
775 self.payto_sig = QLabel('')
776 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
778 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
779 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
788 def entry_changed( is_fee ):
789 self.funds_error = False
791 if self.amount_e.is_shortcut:
792 self.amount_e.is_shortcut = False
793 c, u = self.wallet.get_account_balance(self.current_account)
794 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
795 fee = self.wallet.estimated_fee(inputs)
797 self.amount_e.setText( self.format_amount(amount) )
798 self.fee_e.setText( self.format_amount( fee ) )
801 amount = self.read_amount(str(self.amount_e.text()))
802 fee = self.read_amount(str(self.fee_e.text()))
804 if not is_fee: fee = None
807 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
809 self.fee_e.setText( self.format_amount( fee ) )
812 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
816 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
817 self.funds_error = True
818 text = _( "Not enough funds" )
819 c, u = self.wallet.get_frozen_balance()
820 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
822 self.statusBar().showMessage(text)
823 self.amount_e.setPalette(palette)
824 self.fee_e.setPalette(palette)
826 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
827 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
829 run_hook('create_send_tab', grid)
833 def update_completions(self):
835 for addr,label in self.wallet.labels.items():
836 if addr in self.wallet.addressbook:
837 l.append( label + ' <' + addr + '>')
839 run_hook('update_completions', l)
840 self.completions.setStringList(l)
844 return lambda s, *args: s.do_protect(func, args)
849 label = unicode( self.message_e.text() )
850 r = unicode( self.payto_e.text() )
853 # label or alias, with address in brackets
854 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
855 to_address = m.group(2) if m else r
857 if not is_valid(to_address):
858 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
862 amount = self.read_amount(unicode( self.amount_e.text()))
864 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
867 fee = self.read_amount(unicode( self.fee_e.text()))
869 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
872 confirm_amount = self.config.get('confirm_amount', 100000000)
873 if amount >= confirm_amount:
874 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
877 self.send_tx(to_address, amount, fee, label)
881 def send_tx(self, to_address, amount, fee, label, password):
884 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
885 except BaseException, e:
886 traceback.print_exc(file=sys.stdout)
887 self.show_message(str(e))
890 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
891 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
895 self.wallet.set_label(tx.hash(), label)
898 h = self.wallet.send_tx(tx)
899 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
900 status, msg = self.wallet.receive_tx( h )
902 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
904 self.update_contacts_tab()
906 QMessageBox.warning(self, _('Error'), msg, _('OK'))
908 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
910 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
911 with open(fileName,'w') as f:
912 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
913 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
915 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
917 # add recipient to addressbook
918 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
919 self.wallet.addressbook.append(to_address)
924 def set_url(self, url):
925 address, amount, label, message, signature, identity, url = util.parse_url(url)
927 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
930 self.mini.set_payment_fields(address, amount)
932 if label and self.wallet.labels.get(address) != label:
933 if self.question('Give label "%s" to address %s ?'%(label,address)):
934 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
935 self.wallet.addressbook.append(address)
936 self.wallet.set_label(address, label)
938 run_hook('set_url', url, self.show_message, self.question)
940 self.tabs.setCurrentIndex(1)
941 label = self.wallet.labels.get(address)
942 m_addr = label + ' <'+ address +'>' if label else address
943 self.payto_e.setText(m_addr)
945 self.message_e.setText(message)
947 self.amount_e.setText(amount)
950 self.set_frozen(self.payto_e,True)
951 self.set_frozen(self.amount_e,True)
952 self.set_frozen(self.message_e,True)
953 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
955 self.payto_sig.setVisible(False)
958 self.payto_sig.setVisible(False)
959 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
961 self.set_frozen(e,False)
964 def set_frozen(self,entry,frozen):
966 entry.setReadOnly(True)
967 entry.setFrame(False)
969 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
970 entry.setPalette(palette)
972 entry.setReadOnly(False)
975 palette.setColor(entry.backgroundRole(), QColor('white'))
976 entry.setPalette(palette)
979 def toggle_freeze(self,addr):
981 if addr in self.wallet.frozen_addresses:
982 self.wallet.unfreeze(addr)
984 self.wallet.freeze(addr)
985 self.update_receive_tab()
987 def toggle_priority(self,addr):
989 if addr in self.wallet.prioritized_addresses:
990 self.wallet.unprioritize(addr)
992 self.wallet.prioritize(addr)
993 self.update_receive_tab()
996 def create_list_tab(self, headers):
997 "generic tab creation method"
998 l = MyTreeWidget(self)
999 l.setColumnCount( len(headers) )
1000 l.setHeaderLabels( headers )
1003 vbox = QVBoxLayout()
1010 vbox.addWidget(buttons)
1012 hbox = QHBoxLayout()
1015 buttons.setLayout(hbox)
1020 def create_receive_tab(self):
1021 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1022 l.setContextMenuPolicy(Qt.CustomContextMenu)
1023 l.customContextMenuRequested.connect(self.create_receive_menu)
1024 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1025 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1026 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1027 self.receive_list = l
1028 self.receive_buttons_hbox = hbox
1035 def save_column_widths(self):
1036 self.column_widths["receive"] = []
1037 for i in range(self.receive_list.columnCount() -1):
1038 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1040 self.column_widths["history"] = []
1041 for i in range(self.history_list.columnCount() - 1):
1042 self.column_widths["history"].append(self.history_list.columnWidth(i))
1044 self.column_widths["contacts"] = []
1045 for i in range(self.contacts_list.columnCount() - 1):
1046 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1048 self.config.set_key("column_widths_2", self.column_widths, True)
1051 def create_contacts_tab(self):
1052 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1053 l.setContextMenuPolicy(Qt.CustomContextMenu)
1054 l.customContextMenuRequested.connect(self.create_contact_menu)
1055 for i,width in enumerate(self.column_widths['contacts']):
1056 l.setColumnWidth(i, width)
1058 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1059 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1060 self.contacts_list = l
1061 self.contacts_buttons_hbox = hbox
1066 def delete_imported_key(self, addr):
1067 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1068 self.wallet.delete_imported_key(addr)
1069 self.update_receive_tab()
1070 self.update_history_tab()
1072 def edit_account_label(self, k):
1073 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1075 label = unicode(text)
1076 self.wallet.set_label(k,label)
1077 self.update_receive_tab()
1079 def account_set_expanded(self, item, k, b):
1081 self.accounts_expanded[k] = b
1083 def create_account_menu(self, position, k, item):
1085 if item.isExpanded():
1086 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1088 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1089 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1090 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1091 if self.wallet.account_is_pending(k):
1092 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1093 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1095 def delete_pending_account(self, k):
1096 self.wallet.delete_pending_account(k)
1097 self.update_receive_tab()
1099 def create_receive_menu(self, position):
1100 # fixme: this function apparently has a side effect.
1101 # if it is not called the menu pops up several times
1102 #self.receive_list.selectedIndexes()
1104 item = self.receive_list.itemAt(position)
1107 addr = unicode(item.text(0))
1108 if not is_valid(addr):
1109 k = str(item.data(0,32).toString())
1111 self.create_account_menu(position, k, item)
1113 item.setExpanded(not item.isExpanded())
1117 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1118 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1119 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1120 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1121 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1122 if addr in self.wallet.imported_keys:
1123 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1125 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1126 menu.addAction(t, lambda: self.toggle_freeze(addr))
1127 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1128 menu.addAction(t, lambda: self.toggle_priority(addr))
1130 run_hook('receive_menu', menu)
1131 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1134 def payto(self, addr):
1136 label = self.wallet.labels.get(addr)
1137 m_addr = label + ' <' + addr + '>' if label else addr
1138 self.tabs.setCurrentIndex(1)
1139 self.payto_e.setText(m_addr)
1140 self.amount_e.setFocus()
1143 def delete_contact(self, x):
1144 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1145 self.wallet.delete_contact(x)
1146 self.wallet.set_label(x, None)
1147 self.update_history_tab()
1148 self.update_contacts_tab()
1149 self.update_completions()
1152 def create_contact_menu(self, position):
1153 item = self.contacts_list.itemAt(position)
1155 addr = unicode(item.text(0))
1156 label = unicode(item.text(1))
1157 is_editable = item.data(0,32).toBool()
1158 payto_addr = item.data(0,33).toString()
1160 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1161 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1162 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1164 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1165 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1167 run_hook('create_contact_menu', menu, item)
1168 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1171 def update_receive_item(self, item):
1172 item.setFont(0, QFont(MONOSPACE_FONT))
1173 address = str(item.data(0,0).toString())
1174 label = self.wallet.labels.get(address,'')
1175 item.setData(1,0,label)
1176 item.setData(0,32, True) # is editable
1178 run_hook('update_receive_item', address, item)
1180 if not self.wallet.is_mine(address): return
1182 c, u = self.wallet.get_addr_balance(address)
1183 balance = self.format_amount(c + u)
1184 item.setData(2,0,balance)
1186 if address in self.wallet.frozen_addresses:
1187 item.setBackgroundColor(0, QColor('lightblue'))
1188 elif address in self.wallet.prioritized_addresses:
1189 item.setBackgroundColor(0, QColor('lightgreen'))
1192 def update_receive_tab(self):
1193 l = self.receive_list
1196 l.setColumnHidden(2, False)
1197 l.setColumnHidden(3, False)
1198 for i,width in enumerate(self.column_widths['receive']):
1199 l.setColumnWidth(i, width)
1201 if self.current_account is None:
1202 account_items = self.wallet.accounts.items()
1203 elif self.current_account != -1:
1204 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1208 for k, account in account_items:
1209 name = self.wallet.get_account_name(k)
1210 c,u = self.wallet.get_account_balance(k)
1211 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1212 l.addTopLevelItem(account_item)
1213 account_item.setExpanded(self.accounts_expanded.get(k, True))
1214 account_item.setData(0, 32, k)
1216 if not self.wallet.is_seeded(k):
1217 icon = QIcon(":icons/key.png")
1218 account_item.setIcon(0, icon)
1220 for is_change in ([0,1]):
1221 name = _("Receiving") if not is_change else _("Change")
1222 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1223 account_item.addChild(seq_item)
1224 if not is_change: seq_item.setExpanded(True)
1229 for address in account.get_addresses(is_change):
1230 h = self.wallet.history.get(address,[])
1234 if gap > self.wallet.gap_limit:
1239 num_tx = '*' if h == ['*'] else "%d"%len(h)
1240 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1241 self.update_receive_item(item)
1243 item.setBackgroundColor(1, QColor('red'))
1244 seq_item.addChild(item)
1247 for k, addr in self.wallet.get_pending_accounts():
1248 name = self.wallet.labels.get(k,'')
1249 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1250 self.update_receive_item(item)
1251 l.addTopLevelItem(account_item)
1252 account_item.setExpanded(True)
1253 account_item.setData(0, 32, k)
1254 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1255 account_item.addChild(item)
1256 self.update_receive_item(item)
1259 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1260 c,u = self.wallet.get_imported_balance()
1261 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1262 l.addTopLevelItem(account_item)
1263 account_item.setExpanded(True)
1264 for address in self.wallet.imported_keys.keys():
1265 item = QTreeWidgetItem( [ address, '', '', ''] )
1266 self.update_receive_item(item)
1267 account_item.addChild(item)
1270 # we use column 1 because column 0 may be hidden
1271 l.setCurrentItem(l.topLevelItem(0),1)
1274 def update_contacts_tab(self):
1275 l = self.contacts_list
1278 for address in self.wallet.addressbook:
1279 label = self.wallet.labels.get(address,'')
1280 n = self.wallet.get_num_tx(address)
1281 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1282 item.setFont(0, QFont(MONOSPACE_FONT))
1283 # 32 = label can be edited (bool)
1284 item.setData(0,32, True)
1286 item.setData(0,33, address)
1287 l.addTopLevelItem(item)
1289 run_hook('update_contacts_tab', l)
1290 l.setCurrentItem(l.topLevelItem(0))
1294 def create_console_tab(self):
1295 from console import Console
1296 self.console = console = Console()
1300 def update_console(self):
1301 console = self.console
1302 console.history = self.config.get("console-history",[])
1303 console.history_index = len(console.history)
1305 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1306 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1308 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1310 def mkfunc(f, method):
1311 return lambda *args: apply( f, (method, args, self.password_dialog ))
1313 if m[0]=='_' or m in ['network','wallet']: continue
1314 methods[m] = mkfunc(c._run, m)
1316 console.updateNamespace(methods)
1319 def change_account(self,s):
1320 if s == _("All accounts"):
1321 self.current_account = None
1323 accounts = self.wallet.get_account_names()
1324 for k, v in accounts.items():
1326 self.current_account = k
1327 self.update_history_tab()
1328 self.update_status()
1329 self.update_receive_tab()
1331 def create_status_bar(self):
1334 sb.setFixedHeight(35)
1335 qtVersion = qVersion()
1337 self.balance_label = QLabel("")
1338 sb.addWidget(self.balance_label)
1340 from version_getter import UpdateLabel
1341 self.updatelabel = UpdateLabel(self.config, sb)
1343 self.account_selector = QComboBox()
1344 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1345 sb.addPermanentWidget(self.account_selector)
1347 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1348 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1350 self.lock_icon = QIcon()
1351 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1352 sb.addPermanentWidget( self.password_button )
1354 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1355 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1356 sb.addPermanentWidget( self.seed_button )
1357 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1358 sb.addPermanentWidget( self.status_button )
1360 run_hook('create_status_bar', (sb,))
1362 self.setStatusBar(sb)
1365 def update_lock_icon(self):
1366 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1367 self.password_button.setIcon( icon )
1370 def update_buttons_on_seed(self):
1371 if not self.wallet.is_watching_only():
1372 self.seed_button.show()
1373 self.password_button.show()
1374 self.send_button.setText(_("Send"))
1376 self.password_button.hide()
1377 self.seed_button.hide()
1378 self.send_button.setText(_("Create unsigned transaction"))
1381 def change_password_dialog(self):
1382 from password_dialog import PasswordDialog
1383 d = PasswordDialog(self.wallet, self)
1385 self.update_lock_icon()
1388 def new_contact_dialog(self):
1391 vbox = QVBoxLayout(d)
1392 vbox.addWidget(QLabel(_('New Contact')+':'))
1394 grid = QGridLayout()
1397 grid.addWidget(QLabel(_("Address")), 1, 0)
1398 grid.addWidget(line1, 1, 1)
1399 grid.addWidget(QLabel(_("Name")), 2, 0)
1400 grid.addWidget(line2, 2, 1)
1402 vbox.addLayout(grid)
1403 vbox.addLayout(ok_cancel_buttons(d))
1408 address = str(line1.text())
1409 label = unicode(line2.text())
1411 if not is_valid(address):
1412 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1415 self.wallet.add_contact(address)
1417 self.wallet.set_label(address, label)
1419 self.update_contacts_tab()
1420 self.update_history_tab()
1421 self.update_completions()
1422 self.tabs.setCurrentIndex(3)
1425 def new_account_dialog(self):
1427 dialog = QDialog(self)
1429 dialog.setWindowTitle(_("New Account"))
1431 vbox = QVBoxLayout()
1432 vbox.addWidget(QLabel(_('Account name')+':'))
1435 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1436 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1441 vbox.addLayout(ok_cancel_buttons(dialog))
1442 dialog.setLayout(vbox)
1446 name = str(e.text())
1449 self.wallet.create_pending_account('1', name)
1450 self.update_receive_tab()
1451 self.tabs.setCurrentIndex(2)
1455 def show_master_public_key_old(self):
1456 dialog = QDialog(self)
1458 dialog.setWindowTitle(_("Master Public Key"))
1460 main_text = QTextEdit()
1461 main_text.setText(self.wallet.get_master_public_key())
1462 main_text.setReadOnly(True)
1463 main_text.setMaximumHeight(170)
1464 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1466 ok_button = QPushButton(_("OK"))
1467 ok_button.setDefault(True)
1468 ok_button.clicked.connect(dialog.accept)
1470 main_layout = QGridLayout()
1471 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1473 main_layout.addWidget(main_text, 1, 0)
1474 main_layout.addWidget(qrw, 1, 1 )
1476 vbox = QVBoxLayout()
1477 vbox.addLayout(main_layout)
1478 vbox.addLayout(close_button(dialog))
1479 dialog.setLayout(vbox)
1483 def show_master_public_key(self):
1485 if self.wallet.seed_version == 4:
1486 self.show_master_public_key_old()
1489 dialog = QDialog(self)
1491 dialog.setWindowTitle(_("Master Public Keys"))
1493 chain_text = QTextEdit()
1494 chain_text.setReadOnly(True)
1495 chain_text.setMaximumHeight(170)
1496 chain_qrw = QRCodeWidget()
1498 mpk_text = QTextEdit()
1499 mpk_text.setReadOnly(True)
1500 mpk_text.setMaximumHeight(170)
1501 mpk_qrw = QRCodeWidget()
1503 main_layout = QGridLayout()
1505 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1506 main_layout.addWidget(mpk_text, 1, 1)
1507 main_layout.addWidget(mpk_qrw, 1, 2)
1509 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1510 main_layout.addWidget(chain_text, 2, 1)
1511 main_layout.addWidget(chain_qrw, 2, 2)
1514 c, K, cK = self.wallet.master_public_keys[str(key)]
1515 chain_text.setText(c)
1516 chain_qrw.set_addr(c)
1517 chain_qrw.update_qr()
1522 key_selector = QComboBox()
1523 keys = sorted(self.wallet.master_public_keys.keys())
1524 key_selector.addItems(keys)
1526 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1527 main_layout.addWidget(key_selector, 0, 1)
1528 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1532 vbox = QVBoxLayout()
1533 vbox.addLayout(main_layout)
1534 vbox.addLayout(close_button(dialog))
1536 dialog.setLayout(vbox)
1541 def show_seed_dialog(self, password):
1542 if self.wallet.is_watching_only():
1543 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1546 if self.wallet.seed:
1548 mnemonic = self.wallet.get_mnemonic(password)
1550 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1552 from seed_dialog import SeedDialog
1553 d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1557 for k in self.wallet.master_private_keys.keys():
1558 pk = self.wallet.get_master_private_key(k, password)
1560 from seed_dialog import PrivateKeysDialog
1561 d = PrivateKeysDialog(self,l)
1568 def show_qrcode(self, data, title = _("QR code")):
1572 d.setWindowTitle(title)
1573 d.setMinimumSize(270, 300)
1574 vbox = QVBoxLayout()
1575 qrw = QRCodeWidget(data)
1576 vbox.addWidget(qrw, 1)
1577 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1578 hbox = QHBoxLayout()
1581 filename = os.path.join(self.config.path, "qrcode.bmp")
1584 bmp.save_qrcode(qrw.qr, filename)
1585 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1587 def copy_to_clipboard():
1588 bmp.save_qrcode(qrw.qr, filename)
1589 self.app.clipboard().setImage(QImage(filename))
1590 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1592 b = QPushButton(_("Copy"))
1594 b.clicked.connect(copy_to_clipboard)
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))