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])
175 self.setWindowIcon(QIcon(":icons/electrum.png"))
178 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
179 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
180 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
181 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
182 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
184 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
185 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
186 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
188 self.history_list.setFocus(True)
192 self.network.register_callback('updated', lambda: self.need_update.set())
193 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
194 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
195 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
196 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
198 # set initial message
199 self.console.showMessage(self.network.banner)
206 self.config.set_key('lite_mode', False, True)
211 self.config.set_key('lite_mode', True, True)
218 if not self.check_qt_version():
219 if self.config.get('lite_mode') is True:
220 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
221 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
222 self.config.set_key('lite_mode', False, True)
228 actuator = lite_window.MiniActuator(self)
230 # Should probably not modify the current path but instead
231 # change the behaviour of rsrc(...)
232 old_path = QDir.currentPath()
233 actuator.load_theme()
235 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
237 driver = lite_window.MiniDriver(self, self.mini)
239 # Reset path back to original value now that loading the GUI
241 QDir.setCurrent(old_path)
243 if self.config.get('lite_mode') is True:
249 def check_qt_version(self):
250 qtVersion = qVersion()
251 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
254 def update_account_selector(self):
256 accounts = self.wallet.get_account_names()
257 self.account_selector.clear()
258 if len(accounts) > 1:
259 self.account_selector.addItems([_("All accounts")] + accounts.values())
260 self.account_selector.setCurrentIndex(0)
261 self.account_selector.show()
263 self.account_selector.hide()
266 def load_wallet(self, wallet):
269 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
270 self.current_account = self.wallet.storage.get("current_account", None)
272 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
273 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
274 self.setWindowTitle( title )
276 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
277 self.notify_transactions()
278 self.update_account_selector()
279 self.new_account.setEnabled(self.wallet.seed_version>4)
280 self.update_lock_icon()
281 self.update_buttons_on_seed()
282 self.update_console()
284 run_hook('load_wallet', wallet)
287 def select_wallet_file(self):
288 wallet_folder = self.wallet.storage.path
289 re.sub("(\/\w*.dat)$", "", wallet_folder)
290 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
294 def open_wallet(self):
296 filename = self.select_wallet_file()
300 storage = WalletStorage({'wallet_path': filename})
301 if not storage.file_exists:
302 self.show_message("file not found "+ filename)
305 self.wallet.stop_threads()
308 wallet = Wallet(storage)
309 wallet.start_threads(self.network)
311 self.load_wallet(wallet)
315 def backup_wallet(self):
317 path = self.wallet.storage.path
318 wallet_folder = os.path.dirname(path)
319 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
320 new_filename = unicode(new_filename)
321 if not ok or not new_filename:
324 new_path = os.path.join(wallet_folder, new_filename)
327 shutil.copy2(path, new_path)
328 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
329 except (IOError, os.error), reason:
330 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
333 def new_wallet(self):
336 wallet_folder = os.path.dirname(self.wallet.storage.path)
337 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
338 filename = unicode(filename)
339 if not ok or not filename:
341 filename = os.path.join(wallet_folder, filename)
343 storage = WalletStorage({'wallet_path': filename})
344 assert not storage.file_exists
346 wizard = installwizard.InstallWizard(self.config, self.network, storage)
347 wallet = wizard.run()
349 self.load_wallet(wallet)
353 def init_menubar(self):
356 file_menu = menubar.addMenu(_("&File"))
357 open_wallet_action = file_menu.addAction(_("&Open"))
358 open_wallet_action.triggered.connect(self.open_wallet)
360 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
361 new_wallet_action.triggered.connect(self.new_wallet)
363 wallet_backup = file_menu.addAction(_("&Copy"))
364 wallet_backup.triggered.connect(self.backup_wallet)
366 quit_item = file_menu.addAction(_("&Close"))
367 quit_item.triggered.connect(self.close)
369 wallet_menu = menubar.addMenu(_("&Wallet"))
371 new_contact = wallet_menu.addAction(_("&New contact"))
372 new_contact.triggered.connect(self.new_contact_dialog)
374 self.new_account = wallet_menu.addAction(_("&New account"))
375 self.new_account.triggered.connect(self.new_account_dialog)
377 wallet_menu.addSeparator()
379 pw = wallet_menu.addAction(_("&Password"))
380 pw.triggered.connect(self.change_password_dialog)
382 show_seed = wallet_menu.addAction(_("&Seed"))
383 show_seed.triggered.connect(self.show_seed_dialog)
385 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
386 show_mpk.triggered.connect(self.show_master_public_key)
388 wallet_menu.addSeparator()
390 labels_menu = wallet_menu.addMenu(_("&Labels"))
391 import_labels = labels_menu.addAction(_("&Import"))
392 import_labels.triggered.connect(self.do_import_labels)
393 export_labels = labels_menu.addAction(_("&Export"))
394 export_labels.triggered.connect(self.do_export_labels)
396 keys_menu = wallet_menu.addMenu(_("&Private keys"))
397 import_keys = keys_menu.addAction(_("&Import"))
398 import_keys.triggered.connect(self.do_import_privkey)
399 export_keys = keys_menu.addAction(_("&Export"))
400 export_keys.triggered.connect(self.do_export_privkeys)
402 ex_history = wallet_menu.addAction(_("&Export History"))
403 ex_history.triggered.connect(self.do_export_history)
407 tools_menu = menubar.addMenu(_("&Tools"))
409 # Settings / Preferences are all reserved keywords in OSX using this as work around
410 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
411 preferences_menu = tools_menu.addAction(preferences_name)
412 preferences_menu.triggered.connect(self.settings_dialog)
414 network = tools_menu.addAction(_("&Network"))
415 network.triggered.connect(self.run_network_dialog)
417 plugins_labels = tools_menu.addAction(_("&Plugins"))
418 plugins_labels.triggered.connect(self.plugins_dialog)
420 tools_menu.addSeparator()
422 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
424 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
425 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
427 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
428 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
430 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
432 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
433 raw_transaction_file.triggered.connect(self.do_process_from_file)
435 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
436 raw_transaction_text.triggered.connect(self.do_process_from_text)
439 help_menu = menubar.addMenu(_("&Help"))
440 show_about = help_menu.addAction(_("&About"))
441 show_about.triggered.connect(self.show_about)
442 web_open = help_menu.addAction(_("&Official website"))
443 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
445 help_menu.addSeparator()
446 doc_open = help_menu.addAction(_("&Documentation"))
447 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
448 report_bug = help_menu.addAction(_("&Report Bug"))
449 report_bug.triggered.connect(self.show_report_bug)
451 self.setMenuBar(menubar)
453 def show_about(self):
454 QMessageBox.about(self, "Electrum",
455 _("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."))
457 def show_report_bug(self):
458 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
459 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
462 def notify_transactions(self):
463 if not self.network or not self.network.is_connected():
466 print_error("Notifying GUI")
467 if len(self.network.interface.pending_transactions_for_notifications) > 0:
468 # Combine the transactions if there are more then three
469 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
472 for tx in self.network.interface.pending_transactions_for_notifications:
473 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
477 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
478 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
480 self.network.interface.pending_transactions_for_notifications = []
482 for tx in self.network.interface.pending_transactions_for_notifications:
484 self.network.interface.pending_transactions_for_notifications.remove(tx)
485 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
487 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
489 def notify(self, message):
490 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
494 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
495 def getOpenFileName(self, title, filter = ""):
496 directory = self.config.get('io_dir', os.path.expanduser('~'))
497 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
498 if fileName and directory != os.path.dirname(fileName):
499 self.config.set_key('io_dir', os.path.dirname(fileName), True)
502 def getSaveFileName(self, title, filename, filter = ""):
503 directory = self.config.get('io_dir', os.path.expanduser('~'))
504 path = os.path.join( directory, filename )
505 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
506 if fileName and directory != os.path.dirname(fileName):
507 self.config.set_key('io_dir', os.path.dirname(fileName), True)
511 QMainWindow.close(self)
512 run_hook('close_main_window')
514 def connect_slots(self, sender):
515 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
516 self.previous_payto_e=''
518 def timer_actions(self):
519 if self.need_update.is_set():
521 self.need_update.clear()
522 run_hook('timer_actions')
524 def format_amount(self, x, is_diff=False, whitespaces=False):
525 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
527 def read_amount(self, x):
528 if x in['.', '']: return None
529 p = pow(10, self.decimal_point)
530 return int( p * Decimal(x) )
533 assert self.decimal_point in [5,8]
534 return "BTC" if self.decimal_point == 8 else "mBTC"
537 def update_status(self):
538 if self.network is None:
540 icon = QIcon(":icons/status_disconnected.png")
542 elif self.network.is_connected():
543 if not self.wallet.up_to_date:
544 text = _("Synchronizing...")
545 icon = QIcon(":icons/status_waiting.png")
546 elif self.network.server_lag > 1:
547 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
548 icon = QIcon(":icons/status_lagging.png")
550 c, u = self.wallet.get_account_balance(self.current_account)
551 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
552 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
555 run_hook('set_quote_text', c+u, r)
558 text += " (%s)"%quote
560 self.tray.setToolTip(text)
561 icon = QIcon(":icons/status_connected.png")
563 text = _("Not connected")
564 icon = QIcon(":icons/status_disconnected.png")
566 self.balance_label.setText(text)
567 self.status_button.setIcon( icon )
570 def update_wallet(self):
572 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
573 self.update_history_tab()
574 self.update_receive_tab()
575 self.update_contacts_tab()
576 self.update_completions()
579 def create_history_tab(self):
580 self.history_list = l = MyTreeWidget(self)
582 for i,width in enumerate(self.column_widths['history']):
583 l.setColumnWidth(i, width)
584 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
585 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
586 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
588 l.customContextMenuRequested.connect(self.create_history_menu)
592 def create_history_menu(self, position):
593 self.history_list.selectedIndexes()
594 item = self.history_list.currentItem()
596 tx_hash = str(item.data(0, Qt.UserRole).toString())
597 if not tx_hash: return
599 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
600 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
601 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
602 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
605 def show_transaction(self, tx):
606 import transaction_dialog
607 d = transaction_dialog.TxDialog(tx, self)
610 def tx_label_clicked(self, item, column):
611 if column==2 and item.isSelected():
613 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
614 self.history_list.editItem( item, column )
615 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
618 def tx_label_changed(self, item, column):
622 tx_hash = str(item.data(0, Qt.UserRole).toString())
623 tx = self.wallet.transactions.get(tx_hash)
624 text = unicode( item.text(2) )
625 self.wallet.set_label(tx_hash, text)
627 item.setForeground(2, QBrush(QColor('black')))
629 text = self.wallet.get_default_label(tx_hash)
630 item.setText(2, text)
631 item.setForeground(2, QBrush(QColor('gray')))
635 def edit_label(self, is_recv):
636 l = self.receive_list if is_recv else self.contacts_list
637 item = l.currentItem()
638 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
639 l.editItem( item, 1 )
640 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
644 def address_label_clicked(self, item, column, l, column_addr, column_label):
645 if column == column_label and item.isSelected():
646 is_editable = item.data(0, 32).toBool()
649 addr = unicode( item.text(column_addr) )
650 label = unicode( item.text(column_label) )
651 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
652 l.editItem( item, column )
653 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
656 def address_label_changed(self, item, column, l, column_addr, column_label):
657 if column == column_label:
658 addr = unicode( item.text(column_addr) )
659 text = unicode( item.text(column_label) )
660 is_editable = item.data(0, 32).toBool()
664 changed = self.wallet.set_label(addr, text)
666 self.update_history_tab()
667 self.update_completions()
669 self.current_item_changed(item)
671 run_hook('item_changed', item, column)
674 def current_item_changed(self, a):
675 run_hook('current_item_changed', a)
679 def update_history_tab(self):
681 self.history_list.clear()
682 for item in self.wallet.get_tx_history(self.current_account):
683 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
684 time_str = _("unknown")
687 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
689 time_str = _("error")
692 time_str = 'unverified'
693 icon = QIcon(":icons/unconfirmed.png")
696 icon = QIcon(":icons/unconfirmed.png")
698 icon = QIcon(":icons/clock%d.png"%conf)
700 icon = QIcon(":icons/confirmed.png")
702 if value is not None:
703 v_str = self.format_amount(value, True, whitespaces=True)
707 balance_str = self.format_amount(balance, whitespaces=True)
710 label, is_default_label = self.wallet.get_label(tx_hash)
712 label = _('Pruned transaction outputs')
713 is_default_label = False
715 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
716 item.setFont(2, QFont(MONOSPACE_FONT))
717 item.setFont(3, QFont(MONOSPACE_FONT))
718 item.setFont(4, QFont(MONOSPACE_FONT))
720 item.setForeground(3, QBrush(QColor("#BC1E1E")))
722 item.setData(0, Qt.UserRole, tx_hash)
723 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
725 item.setForeground(2, QBrush(QColor('grey')))
727 item.setIcon(0, icon)
728 self.history_list.insertTopLevelItem(0,item)
731 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
734 def create_send_tab(self):
739 grid.setColumnMinimumWidth(3,300)
740 grid.setColumnStretch(5,1)
743 self.payto_e = QLineEdit()
744 grid.addWidget(QLabel(_('Pay to')), 1, 0)
745 grid.addWidget(self.payto_e, 1, 1, 1, 3)
747 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)
749 completer = QCompleter()
750 completer.setCaseSensitivity(False)
751 self.payto_e.setCompleter(completer)
752 completer.setModel(self.completions)
754 self.message_e = QLineEdit()
755 grid.addWidget(QLabel(_('Description')), 2, 0)
756 grid.addWidget(self.message_e, 2, 1, 1, 3)
757 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)
759 self.amount_e = AmountEdit(self.base_unit)
760 grid.addWidget(QLabel(_('Amount')), 3, 0)
761 grid.addWidget(self.amount_e, 3, 1, 1, 2)
762 grid.addWidget(HelpButton(
763 _('Amount to be sent.') + '\n\n' \
764 + _('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.') \
765 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
767 self.fee_e = AmountEdit(self.base_unit)
768 grid.addWidget(QLabel(_('Fee')), 4, 0)
769 grid.addWidget(self.fee_e, 4, 1, 1, 2)
770 grid.addWidget(HelpButton(
771 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
772 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
773 + _('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)
776 self.send_button = EnterButton(_("Send"), self.do_send)
777 grid.addWidget(self.send_button, 6, 1)
779 b = EnterButton(_("Clear"),self.do_clear)
780 grid.addWidget(b, 6, 2)
782 self.payto_sig = QLabel('')
783 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
785 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
786 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
795 def entry_changed( is_fee ):
796 self.funds_error = False
798 if self.amount_e.is_shortcut:
799 self.amount_e.is_shortcut = False
800 c, u = self.wallet.get_account_balance(self.current_account)
801 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
802 fee = self.wallet.estimated_fee(inputs)
804 self.amount_e.setText( self.format_amount(amount) )
805 self.fee_e.setText( self.format_amount( fee ) )
808 amount = self.read_amount(str(self.amount_e.text()))
809 fee = self.read_amount(str(self.fee_e.text()))
811 if not is_fee: fee = None
814 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
816 self.fee_e.setText( self.format_amount( fee ) )
819 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
823 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
824 self.funds_error = True
825 text = _( "Not enough funds" )
826 c, u = self.wallet.get_frozen_balance()
827 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
829 self.statusBar().showMessage(text)
830 self.amount_e.setPalette(palette)
831 self.fee_e.setPalette(palette)
833 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
834 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
836 run_hook('create_send_tab', grid)
840 def update_completions(self):
842 for addr,label in self.wallet.labels.items():
843 if addr in self.wallet.addressbook:
844 l.append( label + ' <' + addr + '>')
846 run_hook('update_completions', l)
847 self.completions.setStringList(l)
851 return lambda s, *args: s.do_protect(func, args)
856 label = unicode( self.message_e.text() )
857 r = unicode( self.payto_e.text() )
860 # label or alias, with address in brackets
861 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
862 to_address = m.group(2) if m else r
864 if not is_valid(to_address):
865 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
869 amount = self.read_amount(unicode( self.amount_e.text()))
871 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
874 fee = self.read_amount(unicode( self.fee_e.text()))
876 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
879 confirm_amount = self.config.get('confirm_amount', 100000000)
880 if amount >= confirm_amount:
881 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
884 self.send_tx(to_address, amount, fee, label)
888 def send_tx(self, to_address, amount, fee, label, password):
891 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
892 except BaseException, e:
893 traceback.print_exc(file=sys.stdout)
894 self.show_message(str(e))
897 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
898 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
902 self.wallet.set_label(tx.hash(), label)
905 h = self.wallet.send_tx(tx)
906 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
907 status, msg = self.wallet.receive_tx( h )
909 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
911 self.update_contacts_tab()
913 QMessageBox.warning(self, _('Error'), msg, _('OK'))
915 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
917 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
918 with open(fileName,'w') as f:
919 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
920 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
922 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
924 # add recipient to addressbook
925 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
926 self.wallet.addressbook.append(to_address)
931 def set_url(self, url):
932 address, amount, label, message, signature, identity, url = util.parse_url(url)
934 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
937 self.mini.set_payment_fields(address, amount)
939 if label and self.wallet.labels.get(address) != label:
940 if self.question('Give label "%s" to address %s ?'%(label,address)):
941 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
942 self.wallet.addressbook.append(address)
943 self.wallet.set_label(address, label)
945 run_hook('set_url', url, self.show_message, self.question)
947 self.tabs.setCurrentIndex(1)
948 label = self.wallet.labels.get(address)
949 m_addr = label + ' <'+ address +'>' if label else address
950 self.payto_e.setText(m_addr)
952 self.message_e.setText(message)
954 self.amount_e.setText(amount)
957 self.set_frozen(self.payto_e,True)
958 self.set_frozen(self.amount_e,True)
959 self.set_frozen(self.message_e,True)
960 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
962 self.payto_sig.setVisible(False)
965 self.payto_sig.setVisible(False)
966 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
968 self.set_frozen(e,False)
971 def set_frozen(self,entry,frozen):
973 entry.setReadOnly(True)
974 entry.setFrame(False)
976 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
977 entry.setPalette(palette)
979 entry.setReadOnly(False)
982 palette.setColor(entry.backgroundRole(), QColor('white'))
983 entry.setPalette(palette)
986 def toggle_freeze(self,addr):
988 if addr in self.wallet.frozen_addresses:
989 self.wallet.unfreeze(addr)
991 self.wallet.freeze(addr)
992 self.update_receive_tab()
994 def toggle_priority(self,addr):
996 if addr in self.wallet.prioritized_addresses:
997 self.wallet.unprioritize(addr)
999 self.wallet.prioritize(addr)
1000 self.update_receive_tab()
1003 def create_list_tab(self, headers):
1004 "generic tab creation method"
1005 l = MyTreeWidget(self)
1006 l.setColumnCount( len(headers) )
1007 l.setHeaderLabels( headers )
1010 vbox = QVBoxLayout()
1017 vbox.addWidget(buttons)
1019 hbox = QHBoxLayout()
1022 buttons.setLayout(hbox)
1027 def create_receive_tab(self):
1028 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1029 l.setContextMenuPolicy(Qt.CustomContextMenu)
1030 l.customContextMenuRequested.connect(self.create_receive_menu)
1031 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1032 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1033 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1034 self.receive_list = l
1035 self.receive_buttons_hbox = hbox
1042 def save_column_widths(self):
1043 self.column_widths["receive"] = []
1044 for i in range(self.receive_list.columnCount() -1):
1045 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1047 self.column_widths["history"] = []
1048 for i in range(self.history_list.columnCount() - 1):
1049 self.column_widths["history"].append(self.history_list.columnWidth(i))
1051 self.column_widths["contacts"] = []
1052 for i in range(self.contacts_list.columnCount() - 1):
1053 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1055 self.config.set_key("column_widths_2", self.column_widths, True)
1058 def create_contacts_tab(self):
1059 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1060 l.setContextMenuPolicy(Qt.CustomContextMenu)
1061 l.customContextMenuRequested.connect(self.create_contact_menu)
1062 for i,width in enumerate(self.column_widths['contacts']):
1063 l.setColumnWidth(i, width)
1065 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1066 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1067 self.contacts_list = l
1068 self.contacts_buttons_hbox = hbox
1073 def delete_imported_key(self, addr):
1074 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1075 self.wallet.delete_imported_key(addr)
1076 self.update_receive_tab()
1077 self.update_history_tab()
1079 def edit_account_label(self, k):
1080 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1082 label = unicode(text)
1083 self.wallet.set_label(k,label)
1084 self.update_receive_tab()
1086 def account_set_expanded(self, item, k, b):
1088 self.accounts_expanded[k] = b
1090 def create_account_menu(self, position, k, item):
1092 if item.isExpanded():
1093 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1095 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1096 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1097 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1098 if self.wallet.account_is_pending(k):
1099 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1100 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1102 def delete_pending_account(self, k):
1103 self.wallet.delete_pending_account(k)
1104 self.update_receive_tab()
1106 def create_receive_menu(self, position):
1107 # fixme: this function apparently has a side effect.
1108 # if it is not called the menu pops up several times
1109 #self.receive_list.selectedIndexes()
1111 item = self.receive_list.itemAt(position)
1114 addr = unicode(item.text(0))
1115 if not is_valid(addr):
1116 k = str(item.data(0,32).toString())
1118 self.create_account_menu(position, k, item)
1120 item.setExpanded(not item.isExpanded())
1124 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1125 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1126 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1127 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1128 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1129 if addr in self.wallet.imported_keys:
1130 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1132 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1133 menu.addAction(t, lambda: self.toggle_freeze(addr))
1134 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1135 menu.addAction(t, lambda: self.toggle_priority(addr))
1137 run_hook('receive_menu', menu)
1138 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1141 def payto(self, addr):
1143 label = self.wallet.labels.get(addr)
1144 m_addr = label + ' <' + addr + '>' if label else addr
1145 self.tabs.setCurrentIndex(1)
1146 self.payto_e.setText(m_addr)
1147 self.amount_e.setFocus()
1150 def delete_contact(self, x):
1151 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1152 self.wallet.delete_contact(x)
1153 self.wallet.set_label(x, None)
1154 self.update_history_tab()
1155 self.update_contacts_tab()
1156 self.update_completions()
1159 def create_contact_menu(self, position):
1160 item = self.contacts_list.itemAt(position)
1162 addr = unicode(item.text(0))
1163 label = unicode(item.text(1))
1164 is_editable = item.data(0,32).toBool()
1165 payto_addr = item.data(0,33).toString()
1167 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1168 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1169 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1171 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1172 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1174 run_hook('create_contact_menu', menu, item)
1175 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1178 def update_receive_item(self, item):
1179 item.setFont(0, QFont(MONOSPACE_FONT))
1180 address = str(item.data(0,0).toString())
1181 label = self.wallet.labels.get(address,'')
1182 item.setData(1,0,label)
1183 item.setData(0,32, True) # is editable
1185 run_hook('update_receive_item', address, item)
1187 if not self.wallet.is_mine(address): return
1189 c, u = self.wallet.get_addr_balance(address)
1190 balance = self.format_amount(c + u)
1191 item.setData(2,0,balance)
1193 if address in self.wallet.frozen_addresses:
1194 item.setBackgroundColor(0, QColor('lightblue'))
1195 elif address in self.wallet.prioritized_addresses:
1196 item.setBackgroundColor(0, QColor('lightgreen'))
1199 def update_receive_tab(self):
1200 l = self.receive_list
1203 l.setColumnHidden(2, False)
1204 l.setColumnHidden(3, False)
1205 for i,width in enumerate(self.column_widths['receive']):
1206 l.setColumnWidth(i, width)
1208 if self.current_account is None:
1209 account_items = self.wallet.accounts.items()
1210 elif self.current_account != -1:
1211 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1215 for k, account in account_items:
1216 name = self.wallet.get_account_name(k)
1217 c,u = self.wallet.get_account_balance(k)
1218 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1219 l.addTopLevelItem(account_item)
1220 account_item.setExpanded(self.accounts_expanded.get(k, True))
1221 account_item.setData(0, 32, k)
1223 if not self.wallet.is_seeded(k):
1224 icon = QIcon(":icons/key.png")
1225 account_item.setIcon(0, icon)
1227 for is_change in ([0,1]):
1228 name = _("Receiving") if not is_change else _("Change")
1229 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1230 account_item.addChild(seq_item)
1231 if not is_change: seq_item.setExpanded(True)
1236 for address in account.get_addresses(is_change):
1237 h = self.wallet.history.get(address,[])
1241 if gap > self.wallet.gap_limit:
1246 num_tx = '*' if h == ['*'] else "%d"%len(h)
1247 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1248 self.update_receive_item(item)
1250 item.setBackgroundColor(1, QColor('red'))
1251 seq_item.addChild(item)
1254 for k, addr in self.wallet.get_pending_accounts():
1255 name = self.wallet.labels.get(k,'')
1256 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1257 self.update_receive_item(item)
1258 l.addTopLevelItem(account_item)
1259 account_item.setExpanded(True)
1260 account_item.setData(0, 32, k)
1261 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1262 account_item.addChild(item)
1263 self.update_receive_item(item)
1266 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1267 c,u = self.wallet.get_imported_balance()
1268 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1269 l.addTopLevelItem(account_item)
1270 account_item.setExpanded(True)
1271 for address in self.wallet.imported_keys.keys():
1272 item = QTreeWidgetItem( [ address, '', '', ''] )
1273 self.update_receive_item(item)
1274 account_item.addChild(item)
1277 # we use column 1 because column 0 may be hidden
1278 l.setCurrentItem(l.topLevelItem(0),1)
1281 def update_contacts_tab(self):
1282 l = self.contacts_list
1285 for address in self.wallet.addressbook:
1286 label = self.wallet.labels.get(address,'')
1287 n = self.wallet.get_num_tx(address)
1288 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1289 item.setFont(0, QFont(MONOSPACE_FONT))
1290 # 32 = label can be edited (bool)
1291 item.setData(0,32, True)
1293 item.setData(0,33, address)
1294 l.addTopLevelItem(item)
1296 run_hook('update_contacts_tab', l)
1297 l.setCurrentItem(l.topLevelItem(0))
1301 def create_console_tab(self):
1302 from console import Console
1303 self.console = console = Console()
1307 def update_console(self):
1308 console = self.console
1309 console.history = self.config.get("console-history",[])
1310 console.history_index = len(console.history)
1312 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1313 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1315 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1317 def mkfunc(f, method):
1318 return lambda *args: apply( f, (method, args, self.password_dialog ))
1320 if m[0]=='_' or m in ['network','wallet']: continue
1321 methods[m] = mkfunc(c._run, m)
1323 console.updateNamespace(methods)
1326 def change_account(self,s):
1327 if s == _("All accounts"):
1328 self.current_account = None
1330 accounts = self.wallet.get_account_names()
1331 for k, v in accounts.items():
1333 self.current_account = k
1334 self.update_history_tab()
1335 self.update_status()
1336 self.update_receive_tab()
1338 def create_status_bar(self):
1341 sb.setFixedHeight(35)
1342 qtVersion = qVersion()
1344 self.balance_label = QLabel("")
1345 sb.addWidget(self.balance_label)
1347 from version_getter import UpdateLabel
1348 self.updatelabel = UpdateLabel(self.config, sb)
1350 self.account_selector = QComboBox()
1351 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1352 sb.addPermanentWidget(self.account_selector)
1354 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1355 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1357 self.lock_icon = QIcon()
1358 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1359 sb.addPermanentWidget( self.password_button )
1361 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1362 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1363 sb.addPermanentWidget( self.seed_button )
1364 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1365 sb.addPermanentWidget( self.status_button )
1367 run_hook('create_status_bar', (sb,))
1369 self.setStatusBar(sb)
1372 def update_lock_icon(self):
1373 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1374 self.password_button.setIcon( icon )
1377 def update_buttons_on_seed(self):
1378 if not self.wallet.is_watching_only():
1379 self.seed_button.show()
1380 self.password_button.show()
1381 self.send_button.setText(_("Send"))
1383 self.password_button.hide()
1384 self.seed_button.hide()
1385 self.send_button.setText(_("Create unsigned transaction"))
1388 def change_password_dialog(self):
1389 from password_dialog import PasswordDialog
1390 d = PasswordDialog(self.wallet, self)
1392 self.update_lock_icon()
1395 def new_contact_dialog(self):
1398 vbox = QVBoxLayout(d)
1399 vbox.addWidget(QLabel(_('New Contact')+':'))
1401 grid = QGridLayout()
1404 grid.addWidget(QLabel(_("Address")), 1, 0)
1405 grid.addWidget(line1, 1, 1)
1406 grid.addWidget(QLabel(_("Name")), 2, 0)
1407 grid.addWidget(line2, 2, 1)
1409 vbox.addLayout(grid)
1410 vbox.addLayout(ok_cancel_buttons(d))
1415 address = str(line1.text())
1416 label = unicode(line2.text())
1418 if not is_valid(address):
1419 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1422 self.wallet.add_contact(address)
1424 self.wallet.set_label(address, label)
1426 self.update_contacts_tab()
1427 self.update_history_tab()
1428 self.update_completions()
1429 self.tabs.setCurrentIndex(3)
1432 def new_account_dialog(self):
1434 dialog = QDialog(self)
1436 dialog.setWindowTitle(_("New Account"))
1438 vbox = QVBoxLayout()
1439 vbox.addWidget(QLabel(_('Account name')+':'))
1442 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1443 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1448 vbox.addLayout(ok_cancel_buttons(dialog))
1449 dialog.setLayout(vbox)
1453 name = str(e.text())
1456 self.wallet.create_pending_account('1', name)
1457 self.update_receive_tab()
1458 self.tabs.setCurrentIndex(2)
1462 def show_master_public_key_old(self):
1463 dialog = QDialog(self)
1465 dialog.setWindowTitle(_("Master Public Key"))
1467 main_text = QTextEdit()
1468 main_text.setText(self.wallet.get_master_public_key())
1469 main_text.setReadOnly(True)
1470 main_text.setMaximumHeight(170)
1471 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1473 ok_button = QPushButton(_("OK"))
1474 ok_button.setDefault(True)
1475 ok_button.clicked.connect(dialog.accept)
1477 main_layout = QGridLayout()
1478 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1480 main_layout.addWidget(main_text, 1, 0)
1481 main_layout.addWidget(qrw, 1, 1 )
1483 vbox = QVBoxLayout()
1484 vbox.addLayout(main_layout)
1485 vbox.addLayout(close_button(dialog))
1486 dialog.setLayout(vbox)
1490 def show_master_public_key(self):
1492 if self.wallet.seed_version == 4:
1493 self.show_master_public_key_old()
1496 dialog = QDialog(self)
1498 dialog.setWindowTitle(_("Master Public Keys"))
1500 chain_text = QTextEdit()
1501 chain_text.setReadOnly(True)
1502 chain_text.setMaximumHeight(170)
1503 chain_qrw = QRCodeWidget()
1505 mpk_text = QTextEdit()
1506 mpk_text.setReadOnly(True)
1507 mpk_text.setMaximumHeight(170)
1508 mpk_qrw = QRCodeWidget()
1510 main_layout = QGridLayout()
1512 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1513 main_layout.addWidget(mpk_text, 1, 1)
1514 main_layout.addWidget(mpk_qrw, 1, 2)
1516 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1517 main_layout.addWidget(chain_text, 2, 1)
1518 main_layout.addWidget(chain_qrw, 2, 2)
1521 c, K, cK = self.wallet.master_public_keys[str(key)]
1522 chain_text.setText(c)
1523 chain_qrw.set_addr(c)
1524 chain_qrw.update_qr()
1529 key_selector = QComboBox()
1530 keys = sorted(self.wallet.master_public_keys.keys())
1531 key_selector.addItems(keys)
1533 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1534 main_layout.addWidget(key_selector, 0, 1)
1535 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1539 vbox = QVBoxLayout()
1540 vbox.addLayout(main_layout)
1541 vbox.addLayout(close_button(dialog))
1543 dialog.setLayout(vbox)
1548 def show_seed_dialog(self, password):
1549 if self.wallet.is_watching_only():
1550 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1553 if self.wallet.seed:
1555 mnemonic = self.wallet.get_mnemonic(password)
1557 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1559 from seed_dialog import SeedDialog
1560 d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1564 for k in self.wallet.master_private_keys.keys():
1565 pk = self.wallet.get_master_private_key(k, password)
1567 from seed_dialog import PrivateKeysDialog
1568 d = PrivateKeysDialog(self,l)
1575 def show_qrcode(self, data, title = _("QR code")):
1579 d.setWindowTitle(title)
1580 d.setMinimumSize(270, 300)
1581 vbox = QVBoxLayout()
1582 qrw = QRCodeWidget(data)
1583 vbox.addWidget(qrw, 1)
1584 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1585 hbox = QHBoxLayout()
1588 filename = os.path.join(self.config.path, "qrcode.bmp")
1591 bmp.save_qrcode(qrw.qr, filename)
1592 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1594 def copy_to_clipboard():
1595 bmp.save_qrcode(qrw.qr, filename)
1596 self.app.clipboard().setImage(QImage(filename))
1597 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1599 b = QPushButton(_("Copy"))
1601 b.clicked.connect(copy_to_clipboard)
1603 b = QPushButton(_("Save"))
1605 b.clicked.connect(print_qr)
1607 b = QPushButton(_("Close"))
1609 b.clicked.connect(d.accept)
1612 vbox.addLayout(hbox)
1617 def do_protect(self, func, args):
1618 if self.wallet.use_encryption:
1619 password = self.password_dialog()
1625 if args != (False,):
1626 args = (self,) + args + (password,)
1628 args = (self,password)
1633 def show_private_key(self, address, password):
1634 if not address: return
1636 pk_list = self.wallet.get_private_key(address, password)
1637 except BaseException, e:
1638 self.show_message(str(e))
1640 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1644 def do_sign(self, address, message, signature, password):
1645 message = unicode(message.toPlainText())
1646 message = message.encode('utf-8')
1648 sig = self.wallet.sign_message(str(address.text()), message, password)
1649 signature.setText(sig)
1650 except BaseException, e:
1651 self.show_message(str(e))
1653 def sign_message(self, address):
1654 if not address: return
1657 d.setWindowTitle(_('Sign Message'))
1658 d.setMinimumSize(410, 290)
1660 tab_widget = QTabWidget()
1662 layout = QGridLayout(tab)
1664 sign_address = QLineEdit()
1666 sign_address.setText(address)
1667 layout.addWidget(QLabel(_('Address')), 1, 0)
1668 layout.addWidget(sign_address, 1, 1)
1670 sign_message = QTextEdit()
1671 layout.addWidget(QLabel(_('Message')), 2, 0)
1672 layout.addWidget(sign_message, 2, 1)
1673 layout.setRowStretch(2,3)
1675 sign_signature = QTextEdit()
1676 layout.addWidget(QLabel(_('Signature')), 3, 0)
1677 layout.addWidget(sign_signature, 3, 1)
1678 layout.setRowStretch(3,1)
1681 hbox = QHBoxLayout()
1682 b = QPushButton(_("Sign"))
1684 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1685 b = QPushButton(_("Close"))
1686 b.clicked.connect(d.accept)
1688 layout.addLayout(hbox, 4, 1)
1689 tab_widget.addTab(tab, _("Sign"))
1693 layout = QGridLayout(tab)
1695 verify_address = QLineEdit()
1696 layout.addWidget(QLabel(_('Address')), 1, 0)
1697 layout.addWidget(verify_address, 1, 1)
1699 verify_message = QTextEdit()
1700 layout.addWidget(QLabel(_('Message')), 2, 0)
1701 layout.addWidget(verify_message, 2, 1)
1702 layout.setRowStretch(2,3)
1704 verify_signature = QTextEdit()
1705 layout.addWidget(QLabel(_('Signature')), 3, 0)
1706 layout.addWidget(verify_signature, 3, 1)
1707 layout.setRowStretch(3,1)
1710 message = unicode(verify_message.toPlainText())
1711 message = message.encode('utf-8')
1712 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1713 self.show_message(_("Signature verified"))
1715 self.show_message(_("Error: wrong signature"))
1717 hbox = QHBoxLayout()
1718 b = QPushButton(_("Verify"))
1719 b.clicked.connect(do_verify)
1721 b = QPushButton(_("Close"))
1722 b.clicked.connect(d.accept)
1724 layout.addLayout(hbox, 4, 1)
1725 tab_widget.addTab(tab, _("Verify"))
1727 vbox = QVBoxLayout()
1728 vbox.addWidget(tab_widget)
1735 def question(self, msg):
1736 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1738 def show_message(self, msg):
1739 QMessageBox.information(self, _('Message'), msg, _('OK'))
1741 def password_dialog(self ):
1748 vbox = QVBoxLayout()
1749 msg = _('Please enter your password')
1750 vbox.addWidget(QLabel(msg))
1752 grid = QGridLayout()
1754 grid.addWidget(QLabel(_('Password')), 1, 0)
1755 grid.addWidget(pw, 1, 1)
1756 vbox.addLayout(grid)
1758 vbox.addLayout(ok_cancel_buttons(d))
1761 run_hook('password_dialog', pw, grid, 1)
1762 if not d.exec_(): return
1763 return unicode(pw.text())
1772 def tx_from_text(self, txt):
1773 "json or raw hexadecimal"
1776 tx = Transaction(txt)
1782 tx_dict = json.loads(str(txt))
1783 assert "hex" in tx_dict.keys()
1784 assert "complete" in tx_dict.keys()
1785 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1786 if not tx_dict["complete"]:
1787 assert "input_info" in tx_dict.keys()
1788 input_info = json.loads(tx_dict['input_info'])
1789 tx.add_input_info(input_info)
1794 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1798 def read_tx_from_file(self):
1799 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1803 with open(fileName, "r") as f:
1804 file_content = f.read()
1805 except (ValueError, IOError, os.error), reason:
1806 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1808 return self.tx_from_text(file_content)
1812 def sign_raw_transaction(self, tx, input_info, password):
1813 self.wallet.signrawtransaction(tx, input_info, [], password)
1815 def do_process_from_text(self):
1816 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1819 tx = self.tx_from_text(text)
1821 self.show_transaction(tx)
1823 def do_process_from_file(self):
1824 tx = self.read_tx_from_file()
1826 self.show_transaction(tx)
1828 def do_process_from_csvReader(self, csvReader):
1831 for row in csvReader:
1833 amount = float(row[1])
1834 amount = int(100000000*amount)
1835 outputs.append((address, amount))
1836 except (ValueError, IOError, os.error), reason:
1837 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1841 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1842 except BaseException, e:
1843 self.show_message(str(e))
1846 self.show_transaction(tx)
1848 def do_process_from_csv_file(self):
1849 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1853 with open(fileName, "r") as f:
1854 csvReader = csv.reader(f)
1855 self.do_process_from_csvReader(csvReader)
1856 except (ValueError, IOError, os.error), reason:
1857 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1860 def do_process_from_csv_text(self):
1861 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1862 + _("Format: address, amount. One output per line"), _("Load CSV"))
1865 f = StringIO.StringIO(text)
1866 csvReader = csv.reader(f)
1867 self.do_process_from_csvReader(csvReader)
1872 def do_export_privkeys(self, password):
1873 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.")))
1876 select_export = _('Select file to export your private keys to')
1877 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1879 with open(fileName, "w+") as csvfile:
1880 transaction = csv.writer(csvfile)
1881 transaction.writerow(["address", "private_key"])
1883 addresses = self.wallet.addresses(True)
1885 for addr in addresses:
1886 pk = "".join(self.wallet.get_private_key(addr, password))
1887 transaction.writerow(["%34s"%addr,pk])
1889 self.show_message(_("Private keys exported."))
1891 except (IOError, os.error), reason:
1892 export_error_label = _("Electrum was unable to produce a private key-export.")
1893 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1895 except BaseException, e:
1896 self.show_message(str(e))
1900 def do_import_labels(self):
1901 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1902 if not labelsFile: return
1904 f = open(labelsFile, 'r')
1907 for key, value in json.loads(data).items():
1908 self.wallet.set_label(key, value)
1909 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1910 except (IOError, os.error), reason:
1911 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1914 def do_export_labels(self):
1915 labels = self.wallet.labels
1917 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1919 with open(fileName, 'w+') as f:
1920 json.dump(labels, f)
1921 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1922 except (IOError, os.error), reason:
1923 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1926 def do_export_history(self):
1927 from lite_window import csv_transaction
1928 csv_transaction(self.wallet)
1932 def do_import_privkey(self, password):
1933 if not self.wallet.imported_keys:
1934 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1935 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1936 + _('Are you sure you understand what you are doing?'), 3, 4)
1939 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1942 text = str(text).split()
1947 addr = self.wallet.import_key(key, password)
1948 except BaseException as e:
1954 addrlist.append(addr)
1956 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1958 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1959 self.update_receive_tab()
1960 self.update_history_tab()
1963 def settings_dialog(self):
1965 d.setWindowTitle(_('Electrum Settings'))
1967 vbox = QVBoxLayout()
1968 grid = QGridLayout()
1969 grid.setColumnStretch(0,1)
1971 nz_label = QLabel(_('Display zeros') + ':')
1972 grid.addWidget(nz_label, 0, 0)
1973 nz_e = AmountEdit(None,True)
1974 nz_e.setText("%d"% self.num_zeros)
1975 grid.addWidget(nz_e, 0, 1)
1976 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1977 grid.addWidget(HelpButton(msg), 0, 2)
1978 if not self.config.is_modifiable('num_zeros'):
1979 for w in [nz_e, nz_label]: w.setEnabled(False)
1981 lang_label=QLabel(_('Language') + ':')
1982 grid.addWidget(lang_label, 1, 0)
1983 lang_combo = QComboBox()
1984 from electrum.i18n import languages
1985 lang_combo.addItems(languages.values())
1987 index = languages.keys().index(self.config.get("language",''))
1990 lang_combo.setCurrentIndex(index)
1991 grid.addWidget(lang_combo, 1, 1)
1992 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1993 if not self.config.is_modifiable('language'):
1994 for w in [lang_combo, lang_label]: w.setEnabled(False)
1997 fee_label = QLabel(_('Transaction fee') + ':')
1998 grid.addWidget(fee_label, 2, 0)
1999 fee_e = AmountEdit(self.base_unit)
2000 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2001 grid.addWidget(fee_e, 2, 1)
2002 msg = _('Fee per kilobyte of transaction.') + ' ' \
2003 + _('Recommended value') + ': ' + self.format_amount(50000)
2004 grid.addWidget(HelpButton(msg), 2, 2)
2005 if not self.config.is_modifiable('fee_per_kb'):
2006 for w in [fee_e, fee_label]: w.setEnabled(False)
2008 units = ['BTC', 'mBTC']
2009 unit_label = QLabel(_('Base unit') + ':')
2010 grid.addWidget(unit_label, 3, 0)
2011 unit_combo = QComboBox()
2012 unit_combo.addItems(units)
2013 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2014 grid.addWidget(unit_combo, 3, 1)
2015 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2016 + '\n1BTC=1000mBTC.\n' \
2017 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2019 usechange_cb = QCheckBox(_('Use change addresses'))
2020 usechange_cb.setChecked(self.wallet.use_change)
2021 grid.addWidget(usechange_cb, 4, 0)
2022 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2023 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2025 grid.setRowStretch(5,1)
2027 vbox.addLayout(grid)
2028 vbox.addLayout(ok_cancel_buttons(d))
2032 if not d.exec_(): return
2034 fee = unicode(fee_e.text())
2036 fee = self.read_amount(fee)
2038 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2041 self.wallet.set_fee(fee)
2043 nz = unicode(nz_e.text())
2048 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2051 if self.num_zeros != nz:
2053 self.config.set_key('num_zeros', nz, True)
2054 self.update_history_tab()
2055 self.update_receive_tab()
2057 usechange_result = usechange_cb.isChecked()
2058 if self.wallet.use_change != usechange_result:
2059 self.wallet.use_change = usechange_result
2060 self.wallet.storage.put('use_change', self.wallet.use_change)
2062 unit_result = units[unit_combo.currentIndex()]
2063 if self.base_unit() != unit_result:
2064 self.decimal_point = 8 if unit_result == 'BTC' else 5
2065 self.config.set_key('decimal_point', self.decimal_point, True)
2066 self.update_history_tab()
2067 self.update_status()
2069 need_restart = False
2071 lang_request = languages.keys()[lang_combo.currentIndex()]
2072 if lang_request != self.config.get('language'):
2073 self.config.set_key("language", lang_request, True)
2076 run_hook('close_settings_dialog')
2079 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2082 def run_network_dialog(self):
2083 if not self.network:
2085 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2087 def closeEvent(self, event):
2089 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2090 self.save_column_widths()
2091 self.config.set_key("console-history", self.console.history[-50:], True)
2092 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2097 def plugins_dialog(self):
2098 from electrum.plugins import plugins
2101 d.setWindowTitle(_('Electrum Plugins'))
2104 vbox = QVBoxLayout(d)
2107 scroll = QScrollArea()
2108 scroll.setEnabled(True)
2109 scroll.setWidgetResizable(True)
2110 scroll.setMinimumSize(400,250)
2111 vbox.addWidget(scroll)
2115 w.setMinimumHeight(len(plugins)*35)
2117 grid = QGridLayout()
2118 grid.setColumnStretch(0,1)
2121 def do_toggle(cb, p, w):
2124 if w: w.setEnabled(r)
2126 def mk_toggle(cb, p, w):
2127 return lambda: do_toggle(cb,p,w)
2129 for i, p in enumerate(plugins):
2131 cb = QCheckBox(p.fullname())
2132 cb.setDisabled(not p.is_available())
2133 cb.setChecked(p.is_enabled())
2134 grid.addWidget(cb, i, 0)
2135 if p.requires_settings():
2136 w = p.settings_widget(self)
2137 w.setEnabled( p.is_enabled() )
2138 grid.addWidget(w, i, 1)
2141 cb.clicked.connect(mk_toggle(cb,p,w))
2142 grid.addWidget(HelpButton(p.description()), i, 2)
2144 print_msg(_("Error: cannot display plugin"), p)
2145 traceback.print_exc(file=sys.stdout)
2146 grid.setRowStretch(i+1,1)
2148 vbox.addLayout(close_button(d))
2153 def show_account_details(self, k):
2155 d.setWindowTitle(_('Account Details'))
2158 vbox = QVBoxLayout(d)
2159 roots = self.wallet.get_roots(k)
2161 name = self.wallet.get_account_name(k)
2162 label = QLabel('Name: ' + name)
2163 vbox.addWidget(label)
2165 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2166 vbox.addWidget(QLabel('Type: ' + acctype))
2168 label = QLabel('Derivation: ' + k)
2169 vbox.addWidget(label)
2172 # mpk = self.wallet.master_public_keys[root]
2173 # text = QTextEdit()
2174 # text.setReadOnly(True)
2175 # text.setMaximumHeight(120)
2176 # text.setText(repr(mpk))
2177 # vbox.addWidget(text)
2179 vbox.addLayout(close_button(d))