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
30 sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
32 from PyQt4.QtGui import *
33 from PyQt4.QtCore import *
34 import PyQt4.QtCore as QtCore
36 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
41 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
43 from electrum.wallet import format_satoshis
44 from electrum import Transaction
45 from electrum import mnemonic
46 from electrum import util, bitcoin, commands, Interface, Wallet
47 from electrum import SimpleConfig, Wallet, WalletStorage
50 from electrum import bmp, pyqrnative
53 from amountedit import AmountEdit
54 from network_dialog import NetworkDialog
55 from qrcodewidget import QRCodeWidget
57 from decimal import Decimal
65 if platform.system() == 'Windows':
66 MONOSPACE_FONT = 'Lucida Console'
67 elif platform.system() == 'Darwin':
68 MONOSPACE_FONT = 'Monaco'
70 MONOSPACE_FONT = 'monospace'
72 from electrum import ELECTRUM_VERSION
82 class StatusBarButton(QPushButton):
83 def __init__(self, icon, tooltip, func):
84 QPushButton.__init__(self, icon, '')
85 self.setToolTip(tooltip)
87 self.setMaximumWidth(25)
88 self.clicked.connect(func)
90 self.setIconSize(QSize(25,25))
92 def keyPressEvent(self, e):
93 if e.key() == QtCore.Qt.Key_Return:
105 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
107 class ElectrumWindow(QMainWindow):
108 def changeEvent(self, event):
109 flags = self.windowFlags();
110 if event and event.type() == QtCore.QEvent.WindowStateChange:
111 if self.windowState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(True)
113 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
114 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
115 # Electrum from closing.
116 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
117 # self.setWindowFlags(flags & ~Qt.ToolTip)
118 elif event.oldState() & QtCore.Qt.WindowMinimized:
119 self.build_menu(False)
120 #self.setWindowFlags(flags | Qt.ToolTip)
122 def build_menu(self, is_hidden = False):
124 if self.isMinimized():
125 m.addAction(_("Show"), self.showNormal)
127 m.addAction(_("Hide"), self.showMinimized)
130 m.addAction(_("Exit Electrum"), self.close)
131 self.tray.setContextMenu(m)
133 def tray_activated(self, reason):
134 if reason == QSystemTrayIcon.DoubleClick:
138 def __init__(self, config, network, go_lite):
139 QMainWindow.__init__(self)
142 self.network = network
143 self.go_lite = go_lite
146 self._close_electrum = False
148 self.current_account = self.config.get("current_account", None)
150 self.icon = QIcon(':icons/electrum.png')
151 self.tray = QSystemTrayIcon(self.icon, self)
152 self.tray.setToolTip('Electrum')
153 self.tray.activated.connect(self.tray_activated)
157 self.create_status_bar()
159 self.need_update = threading.Event()
161 self.expert_mode = config.get('classic_expert_mode', False)
162 self.decimal_point = config.get('decimal_point', 8)
163 self.num_zeros = int(config.get('num_zeros',0))
165 set_language(config.get('language'))
167 self.funds_error = False
168 self.completions = QStringListModel()
170 self.tabs = tabs = QTabWidget(self)
171 self.column_widths = self.config.get("column_widths", default_column_widths )
172 tabs.addTab(self.create_history_tab(), _('History') )
173 tabs.addTab(self.create_send_tab(), _('Send') )
174 tabs.addTab(self.create_receive_tab(), _('Receive') )
175 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
176 tabs.addTab(self.create_console_tab(), _('Console') )
177 tabs.setMinimumSize(600, 400)
178 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
179 self.setCentralWidget(tabs)
181 g = self.config.get("winpos-qt",[100, 100, 840, 400])
182 self.setGeometry(g[0], g[1], g[2], g[3])
186 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
187 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
188 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
189 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
190 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
192 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
193 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
194 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
196 self.history_list.setFocus(True)
198 self.exchanger = exchange_rate.Exchanger(self)
199 self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
201 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
202 if platform.system() == 'Windows':
203 n = 3 if self.wallet.seed else 2
204 tabs.setCurrentIndex (n)
205 tabs.setCurrentIndex (0)
207 # plugins that need to change the GUI do it here
208 self.run_hook('init')
213 def load_wallet(self, wallet):
217 self.network.register_callback('updated', lambda: self.need_update.set())
218 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
219 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
220 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
221 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
222 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
223 if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
224 self.setWindowTitle( title )
226 # set initial message
227 self.console.showMessage(self.network.banner)
228 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
229 self.notify_transactions()
232 accounts = self.wallet.get_account_names()
233 self.account_selector.clear()
234 if len(accounts) > 1:
235 self.account_selector.addItems([_("All accounts")] + accounts.values())
236 self.account_selector.setCurrentIndex(0)
237 self.account_selector.show()
239 self.account_selector.hide()
241 self.new_account.setEnabled(self.wallet.seed_version>4)
243 self.update_lock_icon()
244 self.update_buttons_on_seed()
245 self.update_console()
247 self.run_hook('load_wallet')
250 def select_wallet_file(self):
251 wallet_folder = self.wallet.storage.path
252 re.sub("(\/\w*.dat)$", "", wallet_folder)
253 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
257 def open_wallet(self):
259 filename = self.select_wallet_file()
263 storage = WalletStorage({'wallet_path': filename})
264 if not storage.file_exists:
265 self.show_message("file not found "+ filename)
268 self.wallet.stop_threads()
271 wallet = Wallet(storage)
272 wallet.start_threads(self.network)
274 self.load_wallet(wallet)
278 def backup_wallet(self):
280 path = self.wallet.storage.path
281 wallet_folder = os.path.dirname(path)
282 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
283 new_filename = unicode(new_filename)
284 if not ok or not new_filename:
287 new_path = os.path.join(wallet_folder, new_filename)
290 shutil.copy2(path, new_path)
291 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
292 except (IOError, os.error), reason:
293 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
296 def new_wallet(self):
299 wallet_folder = os.path.dirname(self.wallet.storage.path)
300 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
301 filename = unicode(filename)
302 if not ok or not filename:
304 filename = os.path.join(wallet_folder, filename)
306 storage = WalletStorage({'wallet_path': filename})
307 assert not storage.file_exists
309 wizard = installwizard.InstallWizard(self.config, self.network, storage)
310 wallet = wizard.run()
312 self.load_wallet(wallet)
316 def init_menubar(self):
319 file_menu = menubar.addMenu(_("&File"))
320 open_wallet_action = file_menu.addAction(_("&Open"))
321 open_wallet_action.triggered.connect(self.open_wallet)
323 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
324 new_wallet_action.triggered.connect(self.new_wallet)
326 wallet_backup = file_menu.addAction(_("&Copy"))
327 wallet_backup.triggered.connect(self.backup_wallet)
329 quit_item = file_menu.addAction(_("&Close"))
330 quit_item.triggered.connect(self.close)
332 wallet_menu = menubar.addMenu(_("&Wallet"))
334 # Settings / Preferences are all reserved keywords in OSX using this as work around
335 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
336 preferences_menu = wallet_menu.addAction(preferences_name)
337 preferences_menu.triggered.connect(self.settings_dialog)
339 wallet_menu.addSeparator()
341 raw_transaction_menu = wallet_menu.addMenu(_("&Load raw transaction"))
343 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
344 raw_transaction_file.triggered.connect(self.do_process_from_file)
346 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
347 raw_transaction_text.triggered.connect(self.do_process_from_text)
349 csv_transaction_menu = wallet_menu.addMenu(_("&Create transaction"))
351 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
352 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
354 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
355 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
357 wallet_menu.addSeparator()
359 show_menu = wallet_menu.addMenu(_("Show"))
361 #if self.wallet.seed:
362 show_seed = show_menu.addAction(_("&Seed"))
363 show_seed.triggered.connect(self.show_seed_dialog)
365 show_mpk = show_menu.addAction(_("&Master Public Key"))
366 show_mpk.triggered.connect(self.show_master_public_key)
368 wallet_menu.addSeparator()
369 new_contact = wallet_menu.addAction(_("&New contact"))
370 new_contact.triggered.connect(self.new_contact_dialog)
372 self.new_account = wallet_menu.addAction(_("&New account"))
373 self.new_account.triggered.connect(self.new_account_dialog)
375 import_menu = menubar.addMenu(_("&Import"))
376 in_labels = import_menu.addAction(_("&Labels"))
377 in_labels.triggered.connect(self.do_import_labels)
379 in_private_keys = import_menu.addAction(_("&Private keys"))
380 in_private_keys.triggered.connect(self.do_import_privkey)
382 export_menu = menubar.addMenu(_("&Export"))
383 ex_private_keys = export_menu.addAction(_("&Private keys"))
384 ex_private_keys.triggered.connect(self.do_export_privkeys)
386 ex_history = export_menu.addAction(_("&History"))
387 ex_history.triggered.connect(self.do_export_history)
389 ex_labels = export_menu.addAction(_("&Labels"))
390 ex_labels.triggered.connect(self.do_export_labels)
392 help_menu = menubar.addMenu(_("&Help"))
393 show_about = help_menu.addAction(_("&About"))
394 show_about.triggered.connect(self.show_about)
395 web_open = help_menu.addAction(_("&Official website"))
396 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
398 help_menu.addSeparator()
399 doc_open = help_menu.addAction(_("&Documentation"))
400 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
401 report_bug = help_menu.addAction(_("&Report Bug"))
402 report_bug.triggered.connect(self.show_report_bug)
404 self.setMenuBar(menubar)
406 def show_about(self):
407 QMessageBox.about(self, "Electrum",
408 _("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."))
410 def show_report_bug(self):
411 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
412 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
415 def notify_transactions(self):
416 print_error("Notifying GUI")
417 if len(self.network.interface.pending_transactions_for_notifications) > 0:
418 # Combine the transactions if there are more then three
419 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
422 for tx in self.network.interface.pending_transactions_for_notifications:
423 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
427 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
428 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
430 self.network.interface.pending_transactions_for_notifications = []
432 for tx in self.network.interface.pending_transactions_for_notifications:
434 self.network.interface.pending_transactions_for_notifications.remove(tx)
435 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
437 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
439 def notify(self, message):
440 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
443 def init_plugins(self):
444 import imp, pkgutil, __builtin__
445 if __builtin__.use_local_modules:
446 fp, pathname, description = imp.find_module('plugins')
447 plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])]
448 plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names)
449 imp.load_module('electrum_plugins', fp, pathname, description)
450 plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names)
452 import electrum_plugins
453 plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)]
454 plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names]
457 for name, p in zip(plugin_names, plugins):
459 self.plugins.append( p.Plugin(self, name) )
461 print_msg(_("Error: cannot initialize plugin"),p)
462 traceback.print_exc(file=sys.stdout)
465 def run_hook(self, name, *args):
466 for p in self.plugins:
467 if not p.is_enabled():
476 print_error("Plugin error")
477 traceback.print_exc(file=sys.stdout)
483 def set_label(self, name, text = None):
485 old_text = self.wallet.labels.get(name)
488 self.wallet.labels[name] = text
489 self.wallet.storage.put('labels', self.wallet.labels)
493 self.wallet.labels.pop(name)
495 self.run_hook('set_label', name, text, changed)
499 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
500 def getOpenFileName(self, title, filter = ""):
501 directory = self.config.get('io_dir', os.path.expanduser('~'))
502 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
503 if fileName and directory != os.path.dirname(fileName):
504 self.config.set_key('io_dir', os.path.dirname(fileName), True)
507 def getSaveFileName(self, title, filename, filter = ""):
508 directory = self.config.get('io_dir', os.path.expanduser('~'))
509 path = os.path.join( directory, filename )
510 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
511 if fileName and directory != os.path.dirname(fileName):
512 self.config.set_key('io_dir', os.path.dirname(fileName), True)
516 QMainWindow.close(self)
517 self.run_hook('close_main_window')
519 def connect_slots(self, sender):
520 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
521 self.previous_payto_e=''
523 def timer_actions(self):
524 if self.need_update.is_set():
526 self.need_update.clear()
527 self.run_hook('timer_actions')
529 def format_amount(self, x, is_diff=False, whitespaces=False):
530 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
532 def read_amount(self, x):
533 if x in['.', '']: return None
534 p = pow(10, self.decimal_point)
535 return int( p * Decimal(x) )
538 assert self.decimal_point in [5,8]
539 return "BTC" if self.decimal_point == 8 else "mBTC"
541 def update_status(self):
542 if self.network.interface and self.network.interface.is_connected:
543 if not self.wallet.up_to_date:
544 text = _("Synchronizing...")
545 icon = QIcon(":icons/status_waiting.png")
547 c, u = self.wallet.get_account_balance(self.current_account)
548 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
549 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
550 text += self.create_quote_text(Decimal(c+u)/100000000)
551 self.tray.setToolTip(text)
552 icon = QIcon(":icons/status_connected.png")
554 text = _("Not connected")
555 icon = QIcon(":icons/status_disconnected.png")
557 self.balance_label.setText(text)
558 self.status_button.setIcon( icon )
560 def update_wallet(self):
562 if self.wallet.up_to_date or not self.network.interface.is_connected:
563 self.update_history_tab()
564 self.update_receive_tab()
565 self.update_contacts_tab()
566 self.update_completions()
569 def create_quote_text(self, btc_balance):
570 quote_currency = self.config.get("currency", "None")
571 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
572 if quote_balance is None:
575 quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
578 def create_history_tab(self):
579 self.history_list = l = MyTreeWidget(self)
581 for i,width in enumerate(self.column_widths['history']):
582 l.setColumnWidth(i, width)
583 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
584 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
585 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
587 l.customContextMenuRequested.connect(self.create_history_menu)
591 def create_history_menu(self, position):
592 self.history_list.selectedIndexes()
593 item = self.history_list.currentItem()
595 tx_hash = str(item.data(0, Qt.UserRole).toString())
596 if not tx_hash: return
598 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
599 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
600 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
601 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
604 def show_transaction(self, tx):
605 import transaction_dialog
606 d = transaction_dialog.TxDialog(tx, self)
609 def tx_label_clicked(self, item, column):
610 if column==2 and item.isSelected():
612 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
613 self.history_list.editItem( item, column )
614 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
617 def tx_label_changed(self, item, column):
621 tx_hash = str(item.data(0, Qt.UserRole).toString())
622 tx = self.wallet.transactions.get(tx_hash)
623 text = unicode( item.text(2) )
624 self.set_label(tx_hash, text)
626 item.setForeground(2, QBrush(QColor('black')))
628 text = self.wallet.get_default_label(tx_hash)
629 item.setText(2, text)
630 item.setForeground(2, QBrush(QColor('gray')))
634 def edit_label(self, is_recv):
635 l = self.receive_list if is_recv else self.contacts_list
636 item = l.currentItem()
637 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
638 l.editItem( item, 1 )
639 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
643 def address_label_clicked(self, item, column, l, column_addr, column_label):
644 if column == column_label and item.isSelected():
645 is_editable = item.data(0, 32).toBool()
648 addr = unicode( item.text(column_addr) )
649 label = unicode( item.text(column_label) )
650 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
651 l.editItem( item, column )
652 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
655 def address_label_changed(self, item, column, l, column_addr, column_label):
656 if column == column_label:
657 addr = unicode( item.text(column_addr) )
658 text = unicode( item.text(column_label) )
659 is_editable = item.data(0, 32).toBool()
663 changed = self.set_label(addr, text)
665 self.update_history_tab()
666 self.update_completions()
668 self.current_item_changed(item)
670 self.run_hook('item_changed', item, column)
673 def current_item_changed(self, a):
674 self.run_hook('current_item_changed', a)
678 def update_history_tab(self):
680 self.history_list.clear()
681 for item in self.wallet.get_tx_history(self.current_account):
682 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
685 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
687 time_str = _("unknown")
690 time_str = 'unverified'
691 icon = QIcon(":icons/unconfirmed.png")
694 icon = QIcon(":icons/unconfirmed.png")
696 icon = QIcon(":icons/clock%d.png"%conf)
698 icon = QIcon(":icons/confirmed.png")
700 if value is not None:
701 v_str = self.format_amount(value, True, whitespaces=True)
705 balance_str = self.format_amount(balance, whitespaces=True)
708 label, is_default_label = self.wallet.get_label(tx_hash)
710 label = _('Pruned transaction outputs')
711 is_default_label = False
713 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
714 item.setFont(2, QFont(MONOSPACE_FONT))
715 item.setFont(3, QFont(MONOSPACE_FONT))
716 item.setFont(4, QFont(MONOSPACE_FONT))
718 item.setForeground(3, QBrush(QColor("#BC1E1E")))
720 item.setData(0, Qt.UserRole, tx_hash)
721 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
723 item.setForeground(2, QBrush(QColor('grey')))
725 item.setIcon(0, icon)
726 self.history_list.insertTopLevelItem(0,item)
729 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
732 def create_send_tab(self):
737 grid.setColumnMinimumWidth(3,300)
738 grid.setColumnStretch(5,1)
741 self.payto_e = QLineEdit()
742 grid.addWidget(QLabel(_('Pay to')), 1, 0)
743 grid.addWidget(self.payto_e, 1, 1, 1, 3)
745 grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
747 completer = QCompleter()
748 completer.setCaseSensitivity(False)
749 self.payto_e.setCompleter(completer)
750 completer.setModel(self.completions)
752 self.message_e = QLineEdit()
753 grid.addWidget(QLabel(_('Description')), 2, 0)
754 grid.addWidget(self.message_e, 2, 1, 1, 3)
755 grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
757 self.amount_e = AmountEdit(self.base_unit)
758 grid.addWidget(QLabel(_('Amount')), 3, 0)
759 grid.addWidget(self.amount_e, 3, 1, 1, 2)
760 grid.addWidget(HelpButton(
761 _('Amount to be sent.') + '\n\n' \
762 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') \
763 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
765 self.fee_e = AmountEdit(self.base_unit)
766 grid.addWidget(QLabel(_('Fee')), 4, 0)
767 grid.addWidget(self.fee_e, 4, 1, 1, 2)
768 grid.addWidget(HelpButton(
769 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
770 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
771 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
774 self.send_button = EnterButton(_("Send"), self.do_send)
775 grid.addWidget(self.send_button, 6, 1)
777 b = EnterButton(_("Clear"),self.do_clear)
778 grid.addWidget(b, 6, 2)
780 self.payto_sig = QLabel('')
781 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
783 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
784 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
793 def entry_changed( is_fee ):
794 self.funds_error = False
796 if self.amount_e.is_shortcut:
797 self.amount_e.is_shortcut = False
798 c, u = self.wallet.get_account_balance(self.current_account)
799 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
800 fee = self.wallet.estimated_fee(inputs)
802 self.amount_e.setText( self.format_amount(amount) )
803 self.fee_e.setText( self.format_amount( fee ) )
806 amount = self.read_amount(str(self.amount_e.text()))
807 fee = self.read_amount(str(self.fee_e.text()))
809 if not is_fee: fee = None
812 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
814 self.fee_e.setText( self.format_amount( fee ) )
817 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
821 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
822 self.funds_error = True
823 text = _( "Not enough funds" )
824 c, u = self.wallet.get_frozen_balance()
825 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
827 self.statusBar().showMessage(text)
828 self.amount_e.setPalette(palette)
829 self.fee_e.setPalette(palette)
831 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
832 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
834 self.run_hook('create_send_tab', grid)
838 def update_completions(self):
840 for addr,label in self.wallet.labels.items():
841 if addr in self.wallet.addressbook:
842 l.append( label + ' <' + addr + '>')
844 self.run_hook('update_completions', l)
845 self.completions.setStringList(l)
849 return lambda s, *args: s.do_protect(func, args)
854 label = unicode( self.message_e.text() )
855 r = unicode( self.payto_e.text() )
858 # label or alias, with address in brackets
859 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
860 to_address = m.group(2) if m else r
862 if not is_valid(to_address):
863 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
867 amount = self.read_amount(unicode( self.amount_e.text()))
869 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
872 fee = self.read_amount(unicode( self.fee_e.text()))
874 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
877 confirm_amount = self.config.get('confirm_amount', 100000000)
878 if amount >= confirm_amount:
879 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
882 self.send_tx(to_address, amount, fee, label)
886 def send_tx(self, to_address, amount, fee, label, password):
889 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
890 except BaseException, e:
891 traceback.print_exc(file=sys.stdout)
892 self.show_message(str(e))
895 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
896 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
899 self.run_hook('send_tx', tx)
902 self.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)
933 if self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
935 if label and self.wallet.labels.get(address) != label:
936 if self.question('Give label "%s" to address %s ?'%(label,address)):
937 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
938 self.wallet.addressbook.append(address)
939 self.set_label(address, label)
941 self.run_hook('set_url', url, self.show_message, self.question)
943 self.tabs.setCurrentIndex(1)
944 label = self.wallet.labels.get(address)
945 m_addr = label + ' <'+ address +'>' if label else address
946 self.payto_e.setText(m_addr)
948 self.message_e.setText(message)
949 self.amount_e.setText(amount)
951 self.set_frozen(self.payto_e,True)
952 self.set_frozen(self.amount_e,True)
953 self.set_frozen(self.message_e,True)
954 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
956 self.payto_sig.setVisible(False)
959 self.payto_sig.setVisible(False)
960 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
962 self.set_frozen(e,False)
965 def set_frozen(self,entry,frozen):
967 entry.setReadOnly(True)
968 entry.setFrame(False)
970 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
971 entry.setPalette(palette)
973 entry.setReadOnly(False)
976 palette.setColor(entry.backgroundRole(), QColor('white'))
977 entry.setPalette(palette)
980 def toggle_freeze(self,addr):
982 if addr in self.wallet.frozen_addresses:
983 self.wallet.unfreeze(addr)
985 self.wallet.freeze(addr)
986 self.update_receive_tab()
988 def toggle_priority(self,addr):
990 if addr in self.wallet.prioritized_addresses:
991 self.wallet.unprioritize(addr)
993 self.wallet.prioritize(addr)
994 self.update_receive_tab()
997 def create_list_tab(self, headers):
998 "generic tab creation method"
999 l = MyTreeWidget(self)
1000 l.setColumnCount( len(headers) )
1001 l.setHeaderLabels( headers )
1004 vbox = QVBoxLayout()
1011 vbox.addWidget(buttons)
1013 hbox = QHBoxLayout()
1016 buttons.setLayout(hbox)
1021 def create_receive_tab(self):
1022 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1023 l.setContextMenuPolicy(Qt.CustomContextMenu)
1024 l.customContextMenuRequested.connect(self.create_receive_menu)
1025 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1026 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1027 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1028 self.receive_list = l
1029 self.receive_buttons_hbox = hbox
1034 def receive_tab_set_mode(self, i):
1035 self.save_column_widths()
1036 self.expert_mode = (i == 1)
1037 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1038 self.update_receive_tab()
1041 def save_column_widths(self):
1042 if not self.expert_mode:
1043 widths = [ self.receive_list.columnWidth(0) ]
1046 for i in range(self.receive_list.columnCount() -1):
1047 widths.append(self.receive_list.columnWidth(i))
1048 self.column_widths["receive"][self.expert_mode] = widths
1050 self.column_widths["history"] = []
1051 for i in range(self.history_list.columnCount() - 1):
1052 self.column_widths["history"].append(self.history_list.columnWidth(i))
1054 self.column_widths["contacts"] = []
1055 for i in range(self.contacts_list.columnCount() - 1):
1056 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1058 self.config.set_key("column_widths", self.column_widths, True)
1061 def create_contacts_tab(self):
1062 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1063 l.setContextMenuPolicy(Qt.CustomContextMenu)
1064 l.customContextMenuRequested.connect(self.create_contact_menu)
1065 for i,width in enumerate(self.column_widths['contacts']):
1066 l.setColumnWidth(i, width)
1068 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1069 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1070 self.contacts_list = l
1071 self.contacts_buttons_hbox = hbox
1076 def delete_imported_key(self, addr):
1077 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1078 self.wallet.delete_imported_key(addr)
1079 self.update_receive_tab()
1080 self.update_history_tab()
1083 def create_receive_menu(self, position):
1084 # fixme: this function apparently has a side effect.
1085 # if it is not called the menu pops up several times
1086 #self.receive_list.selectedIndexes()
1088 item = self.receive_list.itemAt(position)
1090 addr = unicode(item.text(0))
1091 if not is_valid(addr):
1092 item.setExpanded(not item.isExpanded())
1095 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1096 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1097 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1098 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1099 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1100 if addr in self.wallet.imported_keys:
1101 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1103 if self.expert_mode:
1104 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1105 menu.addAction(t, lambda: self.toggle_freeze(addr))
1106 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1107 menu.addAction(t, lambda: self.toggle_priority(addr))
1109 self.run_hook('receive_menu', menu)
1110 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1113 def payto(self, addr):
1115 label = self.wallet.labels.get(addr)
1116 m_addr = label + ' <' + addr + '>' if label else addr
1117 self.tabs.setCurrentIndex(1)
1118 self.payto_e.setText(m_addr)
1119 self.amount_e.setFocus()
1122 def delete_contact(self, x):
1123 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1124 self.wallet.delete_contact(x)
1125 self.set_label(x, None)
1126 self.update_history_tab()
1127 self.update_contacts_tab()
1128 self.update_completions()
1131 def create_contact_menu(self, position):
1132 item = self.contacts_list.itemAt(position)
1134 addr = unicode(item.text(0))
1135 label = unicode(item.text(1))
1136 is_editable = item.data(0,32).toBool()
1137 payto_addr = item.data(0,33).toString()
1139 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1140 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1141 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1143 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1144 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1146 self.run_hook('create_contact_menu', menu, item)
1147 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1150 def update_receive_item(self, item):
1151 item.setFont(0, QFont(MONOSPACE_FONT))
1152 address = str(item.data(0,0).toString())
1153 label = self.wallet.labels.get(address,'')
1154 item.setData(1,0,label)
1155 item.setData(0,32, True) # is editable
1157 self.run_hook('update_receive_item', address, item)
1159 c, u = self.wallet.get_addr_balance(address)
1160 balance = self.format_amount(c + u)
1161 item.setData(2,0,balance)
1163 if self.expert_mode:
1164 if address in self.wallet.frozen_addresses:
1165 item.setBackgroundColor(0, QColor('lightblue'))
1166 elif address in self.wallet.prioritized_addresses:
1167 item.setBackgroundColor(0, QColor('lightgreen'))
1170 def update_receive_tab(self):
1171 l = self.receive_list
1174 l.setColumnHidden(2, not self.expert_mode)
1175 l.setColumnHidden(3, not self.expert_mode)
1176 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1177 l.setColumnWidth(i, width)
1179 if self.current_account is None:
1180 account_items = self.wallet.accounts.items()
1181 elif self.current_account != -1:
1182 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1186 for k, account in account_items:
1187 name = self.wallet.get_account_name(k)
1188 c,u = self.wallet.get_account_balance(k)
1189 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1190 l.addTopLevelItem(account_item)
1191 account_item.setExpanded(True)
1193 for is_change in ([0,1] if self.expert_mode else [0]):
1194 if self.expert_mode:
1195 name = _("Receiving") if not is_change else _("Change")
1196 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1197 account_item.addChild(seq_item)
1198 if not is_change: seq_item.setExpanded(True)
1200 seq_item = account_item
1204 for address in account.get_addresses(is_change):
1205 h = self.wallet.history.get(address,[])
1209 if gap > self.wallet.gap_limit:
1214 num_tx = '*' if h == ['*'] else "%d"%len(h)
1215 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1216 self.update_receive_item(item)
1218 item.setBackgroundColor(1, QColor('red'))
1219 seq_item.addChild(item)
1222 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1223 c,u = self.wallet.get_imported_balance()
1224 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1225 l.addTopLevelItem(account_item)
1226 account_item.setExpanded(True)
1227 for address in self.wallet.imported_keys.keys():
1228 item = QTreeWidgetItem( [ address, '', '', ''] )
1229 self.update_receive_item(item)
1230 account_item.addChild(item)
1233 # we use column 1 because column 0 may be hidden
1234 l.setCurrentItem(l.topLevelItem(0),1)
1237 def update_contacts_tab(self):
1238 l = self.contacts_list
1241 for address in self.wallet.addressbook:
1242 label = self.wallet.labels.get(address,'')
1243 n = self.wallet.get_num_tx(address)
1244 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1245 item.setFont(0, QFont(MONOSPACE_FONT))
1246 # 32 = label can be edited (bool)
1247 item.setData(0,32, True)
1249 item.setData(0,33, address)
1250 l.addTopLevelItem(item)
1252 self.run_hook('update_contacts_tab', l)
1253 l.setCurrentItem(l.topLevelItem(0))
1257 def create_console_tab(self):
1258 from qt_console import Console
1259 self.console = console = Console()
1263 def update_console(self):
1264 console = self.console
1265 console.history = self.config.get("console-history",[])
1266 console.history_index = len(console.history)
1268 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1269 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1271 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1273 def mkfunc(f, method):
1274 return lambda *args: apply( f, (method, args, self.password_dialog ))
1276 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1277 methods[m] = mkfunc(c._run, m)
1279 console.updateNamespace(methods)
1282 def change_account(self,s):
1283 if s == _("All accounts"):
1284 self.current_account = None
1286 accounts = self.wallet.get_account_names()
1287 for k, v in accounts.items():
1289 self.current_account = k
1290 self.update_history_tab()
1291 self.update_status()
1292 self.update_receive_tab()
1294 def create_status_bar(self):
1297 sb.setFixedHeight(35)
1298 qtVersion = qVersion()
1300 self.balance_label = QLabel("")
1301 sb.addWidget(self.balance_label)
1303 from version_getter import UpdateLabel
1304 self.updatelabel = UpdateLabel(self.config, sb)
1306 self.account_selector = QComboBox()
1307 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1308 sb.addPermanentWidget(self.account_selector)
1310 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1311 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1313 self.lock_icon = QIcon()
1314 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1315 sb.addPermanentWidget( self.password_button )
1317 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1318 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1319 sb.addPermanentWidget( self.seed_button )
1320 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1321 sb.addPermanentWidget( self.status_button )
1323 self.run_hook('create_status_bar', (sb,))
1325 self.setStatusBar(sb)
1328 def update_lock_icon(self):
1329 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1330 self.password_button.setIcon( icon )
1333 def update_buttons_on_seed(self):
1334 if self.wallet.seed:
1335 self.seed_button.show()
1336 self.password_button.show()
1337 self.send_button.setText(_("Send"))
1339 self.password_button.hide()
1340 self.seed_button.hide()
1341 self.send_button.setText(_("Create unsigned transaction"))
1344 def change_password_dialog(self):
1345 from password_dialog import PasswordDialog
1346 d = PasswordDialog(self.wallet, self)
1348 self.update_lock_icon()
1351 def new_contact_dialog(self):
1352 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1353 address = unicode(text)
1355 if is_valid(address):
1356 self.wallet.add_contact(address)
1357 self.update_contacts_tab()
1358 self.update_history_tab()
1359 self.update_completions()
1361 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1364 def new_account_dialog(self):
1366 dialog = QDialog(self)
1368 dialog.setWindowTitle(_("New Account"))
1370 addr = self.wallet.new_account_address()
1371 vbox = QVBoxLayout()
1372 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1373 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1374 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1375 vbox.addWidget(QLabel(msg))
1376 vbox.addWidget(QLabel(_('Address')+':'))
1381 vbox.addLayout(ok_cancel_buttons(dialog))
1382 dialog.setLayout(vbox)
1389 def show_master_public_key(self):
1390 dialog = QDialog(self)
1392 dialog.setWindowTitle(_("Master Public Key"))
1394 main_text = QTextEdit()
1395 main_text.setText(self.wallet.get_master_public_key())
1396 main_text.setReadOnly(True)
1397 main_text.setMaximumHeight(170)
1398 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1400 ok_button = QPushButton(_("OK"))
1401 ok_button.setDefault(True)
1402 ok_button.clicked.connect(dialog.accept)
1404 main_layout = QGridLayout()
1405 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1407 main_layout.addWidget(main_text, 1, 0)
1408 main_layout.addWidget(qrw, 1, 1 )
1410 vbox = QVBoxLayout()
1411 vbox.addLayout(main_layout)
1412 hbox = QHBoxLayout()
1414 hbox.addWidget(ok_button)
1415 vbox.addLayout(hbox)
1417 dialog.setLayout(vbox)
1422 def show_seed_dialog(self, password):
1423 if not self.wallet.seed:
1424 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1427 seed = self.wallet.decode_seed(password)
1429 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1432 from seed_dialog import SeedDialog
1433 d = SeedDialog(self)
1434 d.show_seed(seed, self.wallet.imported_keys)
1438 def show_qrcode(self, data, title = _("QR code")):
1442 d.setWindowTitle(title)
1443 d.setMinimumSize(270, 300)
1444 vbox = QVBoxLayout()
1445 qrw = QRCodeWidget(data)
1446 vbox.addWidget(qrw, 1)
1447 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1448 hbox = QHBoxLayout()
1452 filename = "qrcode.bmp"
1453 bmp.save_qrcode(qrw.qr, filename)
1454 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1456 b = QPushButton(_("Save"))
1458 b.clicked.connect(print_qr)
1460 b = QPushButton(_("Close"))
1462 b.clicked.connect(d.accept)
1465 vbox.addLayout(hbox)
1470 def do_protect(self, func, args):
1471 if self.wallet.use_encryption:
1472 password = self.password_dialog()
1478 if args != (False,):
1479 args = (self,) + args + (password,)
1481 args = (self,password)
1486 def show_private_key(self, address, password):
1487 if not address: return
1489 pk_list = self.wallet.get_private_key(address, password)
1490 except BaseException, e:
1491 self.show_message(str(e))
1493 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1497 def do_sign(self, address, message, signature, password):
1498 message = unicode(message.toPlainText())
1499 message = message.encode('utf-8')
1501 sig = self.wallet.sign_message(str(address.text()), message, password)
1502 signature.setText(sig)
1503 except BaseException, e:
1504 self.show_message(str(e))
1506 def sign_message(self, address):
1507 if not address: return
1510 d.setWindowTitle(_('Sign Message'))
1511 d.setMinimumSize(410, 290)
1513 tab_widget = QTabWidget()
1515 layout = QGridLayout(tab)
1517 sign_address = QLineEdit()
1519 sign_address.setText(address)
1520 layout.addWidget(QLabel(_('Address')), 1, 0)
1521 layout.addWidget(sign_address, 1, 1)
1523 sign_message = QTextEdit()
1524 layout.addWidget(QLabel(_('Message')), 2, 0)
1525 layout.addWidget(sign_message, 2, 1)
1526 layout.setRowStretch(2,3)
1528 sign_signature = QTextEdit()
1529 layout.addWidget(QLabel(_('Signature')), 3, 0)
1530 layout.addWidget(sign_signature, 3, 1)
1531 layout.setRowStretch(3,1)
1534 hbox = QHBoxLayout()
1535 b = QPushButton(_("Sign"))
1537 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1538 b = QPushButton(_("Close"))
1539 b.clicked.connect(d.accept)
1541 layout.addLayout(hbox, 4, 1)
1542 tab_widget.addTab(tab, _("Sign"))
1546 layout = QGridLayout(tab)
1548 verify_address = QLineEdit()
1549 layout.addWidget(QLabel(_('Address')), 1, 0)
1550 layout.addWidget(verify_address, 1, 1)
1552 verify_message = QTextEdit()
1553 layout.addWidget(QLabel(_('Message')), 2, 0)
1554 layout.addWidget(verify_message, 2, 1)
1555 layout.setRowStretch(2,3)
1557 verify_signature = QTextEdit()
1558 layout.addWidget(QLabel(_('Signature')), 3, 0)
1559 layout.addWidget(verify_signature, 3, 1)
1560 layout.setRowStretch(3,1)
1563 message = unicode(verify_message.toPlainText())
1564 message = message.encode('utf-8')
1565 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1566 self.show_message(_("Signature verified"))
1568 self.show_message(_("Error: wrong signature"))
1570 hbox = QHBoxLayout()
1571 b = QPushButton(_("Verify"))
1572 b.clicked.connect(do_verify)
1574 b = QPushButton(_("Close"))
1575 b.clicked.connect(d.accept)
1577 layout.addLayout(hbox, 4, 1)
1578 tab_widget.addTab(tab, _("Verify"))
1580 vbox = QVBoxLayout()
1581 vbox.addWidget(tab_widget)
1588 def question(self, msg):
1589 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1591 def show_message(self, msg):
1592 QMessageBox.information(self, _('Message'), msg, _('OK'))
1594 def password_dialog(self ):
1601 vbox = QVBoxLayout()
1602 msg = _('Please enter your password')
1603 vbox.addWidget(QLabel(msg))
1605 grid = QGridLayout()
1607 grid.addWidget(QLabel(_('Password')), 1, 0)
1608 grid.addWidget(pw, 1, 1)
1609 vbox.addLayout(grid)
1611 vbox.addLayout(ok_cancel_buttons(d))
1614 self.run_hook('password_dialog', pw, grid, 1)
1615 if not d.exec_(): return
1616 return unicode(pw.text())
1625 def tx_from_text(self, txt):
1626 "json or raw hexadecimal"
1629 tx = Transaction(txt)
1635 tx_dict = json.loads(str(txt))
1636 assert "hex" in tx_dict.keys()
1637 assert "complete" in tx_dict.keys()
1638 if not tx_dict["complete"]:
1639 assert "input_info" in tx_dict.keys()
1640 tx = Transaction(tx_dict["hex"])
1645 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1649 def read_tx_from_file(self):
1650 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1654 with open(fileName, "r") as f:
1655 file_content = f.read()
1656 except (ValueError, IOError, os.error), reason:
1657 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1659 return self.tx_from_text(file_content)
1663 def sign_raw_transaction(self, tx, input_info, password):
1664 self.wallet.signrawtransaction(tx, input_info, [], password)
1666 def do_process_from_text(self):
1667 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1670 tx = self.tx_from_text(text)
1672 self.show_transaction(tx)
1674 def do_process_from_file(self):
1675 tx = self.read_tx_from_file()
1677 self.show_transaction(tx)
1679 def do_process_from_csvReader(self, csvReader):
1682 for row in csvReader:
1684 amount = float(row[1])
1685 amount = int(100000000*amount)
1686 outputs.append((address, amount))
1687 except (ValueError, IOError, os.error), reason:
1688 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1692 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1693 except BaseException, e:
1694 self.show_message(str(e))
1697 self.show_transaction(tx)
1699 def do_process_from_csv_file(self):
1700 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1704 with open(fileName, "r") as f:
1705 csvReader = csv.reader(f)
1706 self.do_process_from_csvReader(csvReader)
1707 except (ValueError, IOError, os.error), reason:
1708 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1711 def do_process_from_csv_text(self):
1712 text = text_dialog(self, _('Input CSV'), _("CSV:"), _("Load CSV"))
1715 f = StringIO.StringIO(text)
1716 csvReader = csv.reader(f)
1717 self.do_process_from_csvReader(csvReader)
1722 def do_export_privkeys(self, password):
1723 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.")))
1726 select_export = _('Select file to export your private keys to')
1727 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1729 with open(fileName, "w+") as csvfile:
1730 transaction = csv.writer(csvfile)
1731 transaction.writerow(["address", "private_key"])
1733 addresses = self.wallet.addresses(True)
1735 for addr in addresses:
1736 pk = "".join(self.wallet.get_private_key(addr, password))
1737 transaction.writerow(["%34s"%addr,pk])
1739 self.show_message(_("Private keys exported."))
1741 except (IOError, os.error), reason:
1742 export_error_label = _("Electrum was unable to produce a private key-export.")
1743 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1745 except BaseException, e:
1746 self.show_message(str(e))
1750 def do_import_labels(self):
1751 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1752 if not labelsFile: return
1754 f = open(labelsFile, 'r')
1757 for key, value in json.loads(data).items():
1758 self.wallet.set_label(key, value)
1759 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1760 except (IOError, os.error), reason:
1761 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1764 def do_export_labels(self):
1765 labels = self.wallet.labels
1767 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1769 with open(fileName, 'w+') as f:
1770 json.dump(labels, f)
1771 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1772 except (IOError, os.error), reason:
1773 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1776 def do_export_history(self):
1777 from lite_window import csv_transaction
1778 csv_transaction(self.wallet)
1782 def do_import_privkey(self, password):
1783 if not self.wallet.imported_keys:
1784 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1785 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1786 + _('Are you sure you understand what you are doing?'), 3, 4)
1789 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1792 text = str(text).split()
1797 addr = self.wallet.import_key(key, password)
1798 except BaseException as e:
1804 addrlist.append(addr)
1806 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1808 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1809 self.update_receive_tab()
1810 self.update_history_tab()
1813 def settings_dialog(self):
1815 d.setWindowTitle(_('Electrum Settings'))
1817 vbox = QVBoxLayout()
1819 tabs = QTabWidget(self)
1820 self.settings_tab = tabs
1821 vbox.addWidget(tabs)
1824 grid_ui = QGridLayout(tab1)
1825 grid_ui.setColumnStretch(0,1)
1826 tabs.addTab(tab1, _('Display') )
1828 nz_label = QLabel(_('Display zeros'))
1829 grid_ui.addWidget(nz_label, 0, 0)
1830 nz_e = AmountEdit(None,True)
1831 nz_e.setText("%d"% self.num_zeros)
1832 grid_ui.addWidget(nz_e, 0, 1)
1833 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1834 grid_ui.addWidget(HelpButton(msg), 0, 2)
1835 if not self.config.is_modifiable('num_zeros'):
1836 for w in [nz_e, nz_label]: w.setEnabled(False)
1838 lang_label=QLabel(_('Language') + ':')
1839 grid_ui.addWidget(lang_label, 1, 0)
1840 lang_combo = QComboBox()
1841 from electrum.i18n import languages
1842 lang_combo.addItems(languages.values())
1844 index = languages.keys().index(self.config.get("language",''))
1847 lang_combo.setCurrentIndex(index)
1848 grid_ui.addWidget(lang_combo, 1, 1)
1849 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1850 if not self.config.is_modifiable('language'):
1851 for w in [lang_combo, lang_label]: w.setEnabled(False)
1853 currencies = self.exchanger.get_currencies()
1854 currencies.insert(0, "None")
1856 cur_label=QLabel(_('Currency') + ':')
1857 grid_ui.addWidget(cur_label , 2, 0)
1858 cur_combo = QComboBox()
1859 cur_combo.addItems(currencies)
1861 index = currencies.index(self.config.get('currency', "None"))
1864 cur_combo.setCurrentIndex(index)
1865 grid_ui.addWidget(cur_combo, 2, 1)
1866 grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1868 expert_cb = QCheckBox(_('Expert mode'))
1869 expert_cb.setChecked(self.expert_mode)
1870 grid_ui.addWidget(expert_cb, 3, 0)
1871 hh = _('In expert mode, your client will:') + '\n' \
1872 + _(' - Show change addresses in the Receive tab') + '\n' \
1873 + _(' - Display the balance of each address') + '\n' \
1874 + _(' - Add freeze/prioritize actions to addresses.')
1875 grid_ui.addWidget(HelpButton(hh), 3, 2)
1876 grid_ui.setRowStretch(4,1)
1880 grid_wallet = QGridLayout(tab2)
1881 grid_wallet.setColumnStretch(0,1)
1882 tabs.addTab(tab2, _('Wallet') )
1884 fee_label = QLabel(_('Transaction fee'))
1885 grid_wallet.addWidget(fee_label, 0, 0)
1886 fee_e = AmountEdit(self.base_unit)
1887 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1888 grid_wallet.addWidget(fee_e, 0, 2)
1889 msg = _('Fee per kilobyte of transaction.') + ' ' \
1890 + _('Recommended value') + ': ' + self.format_amount(50000)
1891 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1892 if not self.config.is_modifiable('fee_per_kb'):
1893 for w in [fee_e, fee_label]: w.setEnabled(False)
1895 usechange_cb = QCheckBox(_('Use change addresses'))
1896 usechange_cb.setChecked(self.wallet.use_change)
1897 grid_wallet.addWidget(usechange_cb, 1, 0)
1898 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1899 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1901 units = ['BTC', 'mBTC']
1902 unit_label = QLabel(_('Base unit'))
1903 grid_wallet.addWidget(unit_label, 3, 0)
1904 unit_combo = QComboBox()
1905 unit_combo.addItems(units)
1906 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1907 grid_wallet.addWidget(unit_combo, 3, 2)
1908 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1909 + '\n1BTC=1000mBTC.\n' \
1910 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1911 grid_wallet.setRowStretch(4,1)
1915 tab5 = QScrollArea()
1916 tab5.setEnabled(True)
1917 tab5.setWidgetResizable(True)
1919 grid_plugins = QGridLayout()
1920 grid_plugins.setColumnStretch(0,1)
1923 w.setLayout(grid_plugins)
1926 w.setMinimumHeight(len(self.plugins)*35)
1928 tabs.addTab(tab5, _('Plugins') )
1929 def mk_toggle(cb, p):
1930 return lambda: cb.setChecked(p.toggle())
1931 for i, p in enumerate(self.plugins):
1933 cb = QCheckBox(p.fullname())
1934 cb.setDisabled(not p.is_available())
1935 cb.setChecked(p.is_enabled())
1936 cb.clicked.connect(mk_toggle(cb,p))
1937 grid_plugins.addWidget(cb, i, 0)
1938 if p.requires_settings():
1939 grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
1940 grid_plugins.addWidget(HelpButton(p.description()), i, 2)
1942 print_msg(_("Error: cannot display plugin"), p)
1943 traceback.print_exc(file=sys.stdout)
1944 grid_plugins.setRowStretch(i+1,1)
1946 self.run_hook('create_settings_tab', tabs)
1948 vbox.addLayout(ok_cancel_buttons(d))
1952 if not d.exec_(): return
1954 fee = unicode(fee_e.text())
1956 fee = self.read_amount(fee)
1958 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1961 self.wallet.set_fee(fee)
1963 nz = unicode(nz_e.text())
1968 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1971 if self.num_zeros != nz:
1973 self.config.set_key('num_zeros', nz, True)
1974 self.update_history_tab()
1975 self.update_receive_tab()
1977 usechange_result = usechange_cb.isChecked()
1978 if self.wallet.use_change != usechange_result:
1979 self.wallet.use_change = usechange_result
1980 self.config.set_key('use_change', self.wallet.use_change, True)
1982 unit_result = units[unit_combo.currentIndex()]
1983 if self.base_unit() != unit_result:
1984 self.decimal_point = 8 if unit_result == 'BTC' else 5
1985 self.config.set_key('decimal_point', self.decimal_point, True)
1986 self.update_history_tab()
1987 self.update_status()
1989 need_restart = False
1991 lang_request = languages.keys()[lang_combo.currentIndex()]
1992 if lang_request != self.config.get('language'):
1993 self.config.set_key("language", lang_request, True)
1996 cur_request = str(currencies[cur_combo.currentIndex()])
1997 if cur_request != self.config.get('currency', "None"):
1998 self.config.set_key('currency', cur_request, True)
1999 self.update_wallet()
2001 self.run_hook('close_settings_dialog')
2004 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2006 self.receive_tab_set_mode(expert_cb.isChecked())
2008 def run_network_dialog(self):
2009 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2011 def closeEvent(self, event):
2013 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2014 self.save_column_widths()
2015 self.config.set_key("console-history", self.console.history[-50:], True)