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
37 from electrum.plugins import run_hook
42 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
44 from electrum.wallet import format_satoshis
45 from electrum import Transaction
46 from electrum import mnemonic
47 from electrum import util, bitcoin, commands, Interface, Wallet
48 from electrum import SimpleConfig, Wallet, WalletStorage
51 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):
139 QMainWindow.__init__(self)
142 self.network = network
144 self._close_electrum = False
146 self.current_account = self.config.get("current_account", None)
148 self.icon = QIcon(':icons/electrum.png')
149 self.tray = QSystemTrayIcon(self.icon, self)
150 self.tray.setToolTip('Electrum')
151 self.tray.activated.connect(self.tray_activated)
155 self.create_status_bar()
157 self.need_update = threading.Event()
159 self.expert_mode = config.get('classic_expert_mode', False)
160 self.decimal_point = config.get('decimal_point', 8)
161 self.num_zeros = int(config.get('num_zeros',0))
163 set_language(config.get('language'))
165 self.funds_error = False
166 self.completions = QStringListModel()
168 self.tabs = tabs = QTabWidget(self)
169 self.column_widths = self.config.get("column_widths", default_column_widths )
170 tabs.addTab(self.create_history_tab(), _('History') )
171 tabs.addTab(self.create_send_tab(), _('Send') )
172 tabs.addTab(self.create_receive_tab(), _('Receive') )
173 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
174 tabs.addTab(self.create_console_tab(), _('Console') )
175 tabs.setMinimumSize(600, 400)
176 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
177 self.setCentralWidget(tabs)
179 g = self.config.get("winpos-qt",[100, 100, 840, 400])
180 self.setGeometry(g[0], g[1], g[2], g[3])
184 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
185 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
186 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
187 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
188 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
190 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
191 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
192 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
194 self.history_list.setFocus(True)
197 self.network.register_callback('updated', lambda: self.need_update.set())
198 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
199 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
200 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
201 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
202 # set initial message
203 self.console.showMessage(self.network.banner)
205 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
206 if platform.system() == 'Windows':
207 n = 3 if self.wallet.seed else 2
208 tabs.setCurrentIndex (n)
209 tabs.setCurrentIndex (0)
216 self.config.set_key('lite_mode', False, True)
221 self.config.set_key('lite_mode', True, True)
228 if not self.check_qt_version():
229 if self.config.get('lite_mode') is True:
230 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
231 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
232 self.config.set_key('lite_mode', False, True)
237 actuator = lite_window.MiniActuator(self)
239 # Should probably not modify the current path but instead
240 # change the behaviour of rsrc(...)
241 old_path = QDir.currentPath()
242 actuator.load_theme()
244 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
246 driver = lite_window.MiniDriver(self, self.mini)
248 # Reset path back to original value now that loading the GUI
250 QDir.setCurrent(old_path)
252 if self.config.get('lite_mode') is True:
258 def check_qt_version(self):
259 qtVersion = qVersion()
260 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
264 def load_wallet(self, wallet):
268 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
269 if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
270 self.setWindowTitle( title )
272 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
273 self.notify_transactions()
276 accounts = self.wallet.get_account_names()
277 self.account_selector.clear()
278 if len(accounts) > 1:
279 self.account_selector.addItems([_("All accounts")] + accounts.values())
280 self.account_selector.setCurrentIndex(0)
281 self.account_selector.show()
283 self.account_selector.hide()
285 self.new_account.setEnabled(self.wallet.seed_version>4)
287 self.update_lock_icon()
288 self.update_buttons_on_seed()
289 self.update_console()
291 run_hook('load_wallet')
294 def select_wallet_file(self):
295 wallet_folder = self.wallet.storage.path
296 re.sub("(\/\w*.dat)$", "", wallet_folder)
297 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
301 def open_wallet(self):
303 filename = self.select_wallet_file()
307 storage = WalletStorage({'wallet_path': filename})
308 if not storage.file_exists:
309 self.show_message("file not found "+ filename)
312 self.wallet.stop_threads()
315 wallet = Wallet(storage)
316 wallet.start_threads(self.network)
318 self.load_wallet(wallet)
322 def backup_wallet(self):
324 path = self.wallet.storage.path
325 wallet_folder = os.path.dirname(path)
326 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
327 new_filename = unicode(new_filename)
328 if not ok or not new_filename:
331 new_path = os.path.join(wallet_folder, new_filename)
334 shutil.copy2(path, new_path)
335 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
336 except (IOError, os.error), reason:
337 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
340 def new_wallet(self):
343 wallet_folder = os.path.dirname(self.wallet.storage.path)
344 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
345 filename = unicode(filename)
346 if not ok or not filename:
348 filename = os.path.join(wallet_folder, filename)
350 storage = WalletStorage({'wallet_path': filename})
351 assert not storage.file_exists
353 wizard = installwizard.InstallWizard(self.config, self.network, storage)
354 wallet = wizard.run()
356 self.load_wallet(wallet)
360 def init_menubar(self):
363 file_menu = menubar.addMenu(_("&File"))
364 open_wallet_action = file_menu.addAction(_("&Open"))
365 open_wallet_action.triggered.connect(self.open_wallet)
367 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
368 new_wallet_action.triggered.connect(self.new_wallet)
370 wallet_backup = file_menu.addAction(_("&Copy"))
371 wallet_backup.triggered.connect(self.backup_wallet)
373 quit_item = file_menu.addAction(_("&Close"))
374 quit_item.triggered.connect(self.close)
376 wallet_menu = menubar.addMenu(_("&Wallet"))
378 new_contact = wallet_menu.addAction(_("&New contact"))
379 new_contact.triggered.connect(self.new_contact_dialog)
381 self.new_account = wallet_menu.addAction(_("&New account"))
382 self.new_account.triggered.connect(self.new_account_dialog)
384 wallet_menu.addSeparator()
386 #if self.wallet.seed:
387 show_seed = wallet_menu.addAction(_("&Seed"))
388 show_seed.triggered.connect(self.show_seed_dialog)
390 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
391 show_mpk.triggered.connect(self.show_master_public_key)
393 wallet_menu.addSeparator()
395 csv_transaction_menu = wallet_menu.addMenu(_("&Create transaction"))
397 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
398 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
400 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
401 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
403 raw_transaction_menu = wallet_menu.addMenu(_("&Load transaction"))
405 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
406 raw_transaction_file.triggered.connect(self.do_process_from_file)
408 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
409 raw_transaction_text.triggered.connect(self.do_process_from_text)
412 tools_menu = menubar.addMenu(_("&Tools"))
414 # Settings / Preferences are all reserved keywords in OSX using this as work around
415 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
416 preferences_menu = tools_menu.addAction(preferences_name)
417 preferences_menu.triggered.connect(self.settings_dialog)
419 plugins_labels = tools_menu.addAction(_("&Plugins"))
420 plugins_labels.triggered.connect(self.plugins_dialog)
422 wallet_menu.addSeparator()
424 labels_menu = tools_menu.addMenu(_("&Labels"))
425 import_labels = labels_menu.addAction(_("&Import"))
426 import_labels.triggered.connect(self.do_import_labels)
427 export_labels = labels_menu.addAction(_("&Export"))
428 export_labels.triggered.connect(self.do_export_labels)
430 keys_menu = tools_menu.addMenu(_("&Private keys"))
431 import_keys = keys_menu.addAction(_("&Import"))
432 import_keys.triggered.connect(self.do_import_privkey)
433 export_keys = keys_menu.addAction(_("&Export"))
434 export_keys.triggered.connect(self.do_export_privkeys)
436 ex_history = tools_menu.addAction(_("&Export History"))
437 ex_history.triggered.connect(self.do_export_history)
440 help_menu = menubar.addMenu(_("&Help"))
441 show_about = help_menu.addAction(_("&About"))
442 show_about.triggered.connect(self.show_about)
443 web_open = help_menu.addAction(_("&Official website"))
444 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
446 help_menu.addSeparator()
447 doc_open = help_menu.addAction(_("&Documentation"))
448 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
449 report_bug = help_menu.addAction(_("&Report Bug"))
450 report_bug.triggered.connect(self.show_report_bug)
452 self.setMenuBar(menubar)
454 def show_about(self):
455 QMessageBox.about(self, "Electrum",
456 _("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."))
458 def show_report_bug(self):
459 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
460 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
463 def notify_transactions(self):
464 print_error("Notifying GUI")
465 if len(self.network.interface.pending_transactions_for_notifications) > 0:
466 # Combine the transactions if there are more then three
467 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
470 for tx in self.network.interface.pending_transactions_for_notifications:
471 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
475 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
476 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
478 self.network.interface.pending_transactions_for_notifications = []
480 for tx in self.network.interface.pending_transactions_for_notifications:
482 self.network.interface.pending_transactions_for_notifications.remove(tx)
483 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
485 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
487 def notify(self, message):
488 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
492 def set_label(self, name, text = None):
494 old_text = self.wallet.labels.get(name)
497 self.wallet.labels[name] = text
498 self.wallet.storage.put('labels', self.wallet.labels)
502 self.wallet.labels.pop(name)
504 run_hook('set_label', name, text, changed)
508 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
509 def getOpenFileName(self, title, filter = ""):
510 directory = self.config.get('io_dir', os.path.expanduser('~'))
511 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
512 if fileName and directory != os.path.dirname(fileName):
513 self.config.set_key('io_dir', os.path.dirname(fileName), True)
516 def getSaveFileName(self, title, filename, filter = ""):
517 directory = self.config.get('io_dir', os.path.expanduser('~'))
518 path = os.path.join( directory, filename )
519 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
520 if fileName and directory != os.path.dirname(fileName):
521 self.config.set_key('io_dir', os.path.dirname(fileName), True)
525 QMainWindow.close(self)
526 run_hook('close_main_window')
528 def connect_slots(self, sender):
529 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
530 self.previous_payto_e=''
532 def timer_actions(self):
533 if self.need_update.is_set():
535 self.need_update.clear()
536 run_hook('timer_actions')
538 def format_amount(self, x, is_diff=False, whitespaces=False):
539 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
541 def read_amount(self, x):
542 if x in['.', '']: return None
543 p = pow(10, self.decimal_point)
544 return int( p * Decimal(x) )
547 assert self.decimal_point in [5,8]
548 return "BTC" if self.decimal_point == 8 else "mBTC"
551 def update_status(self):
552 if self.network.interface and self.network.interface.is_connected:
553 if not self.wallet.up_to_date:
554 text = _("Synchronizing...")
555 icon = QIcon(":icons/status_waiting.png")
557 c, u = self.wallet.get_account_balance(self.current_account)
558 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
559 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
562 run_hook('set_quote_text', c+u, r)
565 text += " (%s)"%quote
567 self.tray.setToolTip(text)
568 icon = QIcon(":icons/status_connected.png")
570 text = _("Not connected")
571 icon = QIcon(":icons/status_disconnected.png")
573 self.balance_label.setText(text)
574 self.status_button.setIcon( icon )
577 def update_wallet(self):
579 if self.wallet.up_to_date or not self.network.interface.is_connected:
580 self.update_history_tab()
581 self.update_receive_tab()
582 self.update_contacts_tab()
583 self.update_completions()
586 def create_history_tab(self):
587 self.history_list = l = MyTreeWidget(self)
589 for i,width in enumerate(self.column_widths['history']):
590 l.setColumnWidth(i, width)
591 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
592 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
593 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
595 l.customContextMenuRequested.connect(self.create_history_menu)
599 def create_history_menu(self, position):
600 self.history_list.selectedIndexes()
601 item = self.history_list.currentItem()
603 tx_hash = str(item.data(0, Qt.UserRole).toString())
604 if not tx_hash: return
606 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
607 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
608 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
609 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
612 def show_transaction(self, tx):
613 import transaction_dialog
614 d = transaction_dialog.TxDialog(tx, self)
617 def tx_label_clicked(self, item, column):
618 if column==2 and item.isSelected():
620 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
621 self.history_list.editItem( item, column )
622 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
625 def tx_label_changed(self, item, column):
629 tx_hash = str(item.data(0, Qt.UserRole).toString())
630 tx = self.wallet.transactions.get(tx_hash)
631 text = unicode( item.text(2) )
632 self.set_label(tx_hash, text)
634 item.setForeground(2, QBrush(QColor('black')))
636 text = self.wallet.get_default_label(tx_hash)
637 item.setText(2, text)
638 item.setForeground(2, QBrush(QColor('gray')))
642 def edit_label(self, is_recv):
643 l = self.receive_list if is_recv else self.contacts_list
644 item = l.currentItem()
645 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
646 l.editItem( item, 1 )
647 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
651 def address_label_clicked(self, item, column, l, column_addr, column_label):
652 if column == column_label and item.isSelected():
653 is_editable = item.data(0, 32).toBool()
656 addr = unicode( item.text(column_addr) )
657 label = unicode( item.text(column_label) )
658 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
659 l.editItem( item, column )
660 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
663 def address_label_changed(self, item, column, l, column_addr, column_label):
664 if column == column_label:
665 addr = unicode( item.text(column_addr) )
666 text = unicode( item.text(column_label) )
667 is_editable = item.data(0, 32).toBool()
671 changed = self.set_label(addr, text)
673 self.update_history_tab()
674 self.update_completions()
676 self.current_item_changed(item)
678 run_hook('item_changed', item, column)
681 def current_item_changed(self, a):
682 run_hook('current_item_changed', a)
686 def update_history_tab(self):
688 self.history_list.clear()
689 for item in self.wallet.get_tx_history(self.current_account):
690 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
693 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
695 time_str = _("unknown")
698 time_str = 'unverified'
699 icon = QIcon(":icons/unconfirmed.png")
702 icon = QIcon(":icons/unconfirmed.png")
704 icon = QIcon(":icons/clock%d.png"%conf)
706 icon = QIcon(":icons/confirmed.png")
708 if value is not None:
709 v_str = self.format_amount(value, True, whitespaces=True)
713 balance_str = self.format_amount(balance, whitespaces=True)
716 label, is_default_label = self.wallet.get_label(tx_hash)
718 label = _('Pruned transaction outputs')
719 is_default_label = False
721 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
722 item.setFont(2, QFont(MONOSPACE_FONT))
723 item.setFont(3, QFont(MONOSPACE_FONT))
724 item.setFont(4, QFont(MONOSPACE_FONT))
726 item.setForeground(3, QBrush(QColor("#BC1E1E")))
728 item.setData(0, Qt.UserRole, tx_hash)
729 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
731 item.setForeground(2, QBrush(QColor('grey')))
733 item.setIcon(0, icon)
734 self.history_list.insertTopLevelItem(0,item)
737 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
740 def create_send_tab(self):
745 grid.setColumnMinimumWidth(3,300)
746 grid.setColumnStretch(5,1)
749 self.payto_e = QLineEdit()
750 grid.addWidget(QLabel(_('Pay to')), 1, 0)
751 grid.addWidget(self.payto_e, 1, 1, 1, 3)
753 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)
755 completer = QCompleter()
756 completer.setCaseSensitivity(False)
757 self.payto_e.setCompleter(completer)
758 completer.setModel(self.completions)
760 self.message_e = QLineEdit()
761 grid.addWidget(QLabel(_('Description')), 2, 0)
762 grid.addWidget(self.message_e, 2, 1, 1, 3)
763 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)
765 self.amount_e = AmountEdit(self.base_unit)
766 grid.addWidget(QLabel(_('Amount')), 3, 0)
767 grid.addWidget(self.amount_e, 3, 1, 1, 2)
768 grid.addWidget(HelpButton(
769 _('Amount to be sent.') + '\n\n' \
770 + _('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.') \
771 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
773 self.fee_e = AmountEdit(self.base_unit)
774 grid.addWidget(QLabel(_('Fee')), 4, 0)
775 grid.addWidget(self.fee_e, 4, 1, 1, 2)
776 grid.addWidget(HelpButton(
777 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
778 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
779 + _('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)
782 self.send_button = EnterButton(_("Send"), self.do_send)
783 grid.addWidget(self.send_button, 6, 1)
785 b = EnterButton(_("Clear"),self.do_clear)
786 grid.addWidget(b, 6, 2)
788 self.payto_sig = QLabel('')
789 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
791 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
792 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
801 def entry_changed( is_fee ):
802 self.funds_error = False
804 if self.amount_e.is_shortcut:
805 self.amount_e.is_shortcut = False
806 c, u = self.wallet.get_account_balance(self.current_account)
807 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
808 fee = self.wallet.estimated_fee(inputs)
810 self.amount_e.setText( self.format_amount(amount) )
811 self.fee_e.setText( self.format_amount( fee ) )
814 amount = self.read_amount(str(self.amount_e.text()))
815 fee = self.read_amount(str(self.fee_e.text()))
817 if not is_fee: fee = None
820 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
822 self.fee_e.setText( self.format_amount( fee ) )
825 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
829 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
830 self.funds_error = True
831 text = _( "Not enough funds" )
832 c, u = self.wallet.get_frozen_balance()
833 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
835 self.statusBar().showMessage(text)
836 self.amount_e.setPalette(palette)
837 self.fee_e.setPalette(palette)
839 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
840 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
842 run_hook('create_send_tab', grid)
846 def update_completions(self):
848 for addr,label in self.wallet.labels.items():
849 if addr in self.wallet.addressbook:
850 l.append( label + ' <' + addr + '>')
852 run_hook('update_completions', l)
853 self.completions.setStringList(l)
857 return lambda s, *args: s.do_protect(func, args)
862 label = unicode( self.message_e.text() )
863 r = unicode( self.payto_e.text() )
866 # label or alias, with address in brackets
867 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
868 to_address = m.group(2) if m else r
870 if not is_valid(to_address):
871 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
875 amount = self.read_amount(unicode( self.amount_e.text()))
877 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
880 fee = self.read_amount(unicode( self.fee_e.text()))
882 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
885 confirm_amount = self.config.get('confirm_amount', 100000000)
886 if amount >= confirm_amount:
887 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
890 self.send_tx(to_address, amount, fee, label)
894 def send_tx(self, to_address, amount, fee, label, password):
897 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
898 except BaseException, e:
899 traceback.print_exc(file=sys.stdout)
900 self.show_message(str(e))
903 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
904 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
907 run_hook('send_tx', tx)
910 self.set_label(tx.hash(), label)
913 h = self.wallet.send_tx(tx)
914 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
915 status, msg = self.wallet.receive_tx( h )
917 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
919 self.update_contacts_tab()
921 QMessageBox.warning(self, _('Error'), msg, _('OK'))
923 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
925 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
926 with open(fileName,'w') as f:
927 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
928 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
930 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
932 # add recipient to addressbook
933 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
934 self.wallet.addressbook.append(to_address)
939 def set_url(self, url):
940 address, amount, label, message, signature, identity, url = util.parse_url(url)
942 self.mini.set_payment_fields(address, amount)
944 if label and self.wallet.labels.get(address) != label:
945 if self.question('Give label "%s" to address %s ?'%(label,address)):
946 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
947 self.wallet.addressbook.append(address)
948 self.set_label(address, label)
950 run_hook('set_url', url, self.show_message, self.question)
952 self.tabs.setCurrentIndex(1)
953 label = self.wallet.labels.get(address)
954 m_addr = label + ' <'+ address +'>' if label else address
955 self.payto_e.setText(m_addr)
957 self.message_e.setText(message)
959 if self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
960 self.amount_e.setText(amount)
963 self.set_frozen(self.payto_e,True)
964 self.set_frozen(self.amount_e,True)
965 self.set_frozen(self.message_e,True)
966 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
968 self.payto_sig.setVisible(False)
971 self.payto_sig.setVisible(False)
972 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
974 self.set_frozen(e,False)
977 def set_frozen(self,entry,frozen):
979 entry.setReadOnly(True)
980 entry.setFrame(False)
982 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
983 entry.setPalette(palette)
985 entry.setReadOnly(False)
988 palette.setColor(entry.backgroundRole(), QColor('white'))
989 entry.setPalette(palette)
992 def toggle_freeze(self,addr):
994 if addr in self.wallet.frozen_addresses:
995 self.wallet.unfreeze(addr)
997 self.wallet.freeze(addr)
998 self.update_receive_tab()
1000 def toggle_priority(self,addr):
1002 if addr in self.wallet.prioritized_addresses:
1003 self.wallet.unprioritize(addr)
1005 self.wallet.prioritize(addr)
1006 self.update_receive_tab()
1009 def create_list_tab(self, headers):
1010 "generic tab creation method"
1011 l = MyTreeWidget(self)
1012 l.setColumnCount( len(headers) )
1013 l.setHeaderLabels( headers )
1016 vbox = QVBoxLayout()
1023 vbox.addWidget(buttons)
1025 hbox = QHBoxLayout()
1028 buttons.setLayout(hbox)
1033 def create_receive_tab(self):
1034 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1035 l.setContextMenuPolicy(Qt.CustomContextMenu)
1036 l.customContextMenuRequested.connect(self.create_receive_menu)
1037 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1038 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1039 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1040 self.receive_list = l
1041 self.receive_buttons_hbox = hbox
1046 def receive_tab_set_mode(self, i):
1047 self.save_column_widths()
1048 self.expert_mode = (i == 1)
1049 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1050 self.update_receive_tab()
1053 def save_column_widths(self):
1054 if not self.expert_mode:
1055 widths = [ self.receive_list.columnWidth(0) ]
1058 for i in range(self.receive_list.columnCount() -1):
1059 widths.append(self.receive_list.columnWidth(i))
1060 self.column_widths["receive"][self.expert_mode] = widths
1062 self.column_widths["history"] = []
1063 for i in range(self.history_list.columnCount() - 1):
1064 self.column_widths["history"].append(self.history_list.columnWidth(i))
1066 self.column_widths["contacts"] = []
1067 for i in range(self.contacts_list.columnCount() - 1):
1068 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1070 self.config.set_key("column_widths", self.column_widths, True)
1073 def create_contacts_tab(self):
1074 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1075 l.setContextMenuPolicy(Qt.CustomContextMenu)
1076 l.customContextMenuRequested.connect(self.create_contact_menu)
1077 for i,width in enumerate(self.column_widths['contacts']):
1078 l.setColumnWidth(i, width)
1080 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1081 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1082 self.contacts_list = l
1083 self.contacts_buttons_hbox = hbox
1088 def delete_imported_key(self, addr):
1089 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1090 self.wallet.delete_imported_key(addr)
1091 self.update_receive_tab()
1092 self.update_history_tab()
1094 def edit_account_label(self, k):
1095 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1097 label = unicode(text)
1098 self.set_label(k,label)
1099 self.update_receive_tab()
1101 def create_account_menu(self, position, k, item):
1103 if item.isExpanded():
1104 menu.addAction(_("Reduce"), lambda: item.setExpanded(False))
1106 menu.addAction(_("Expand"), lambda: item.setExpanded(True))
1107 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1108 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1109 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1111 def create_receive_menu(self, position):
1112 # fixme: this function apparently has a side effect.
1113 # if it is not called the menu pops up several times
1114 #self.receive_list.selectedIndexes()
1116 item = self.receive_list.itemAt(position)
1119 addr = unicode(item.text(0))
1120 if not is_valid(addr):
1121 k = str(item.data(0,32).toString())
1123 self.create_account_menu(position, k, item)
1125 item.setExpanded(not item.isExpanded())
1129 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1130 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1131 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1132 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1133 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1134 if addr in self.wallet.imported_keys:
1135 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1137 if self.expert_mode:
1138 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1139 menu.addAction(t, lambda: self.toggle_freeze(addr))
1140 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1141 menu.addAction(t, lambda: self.toggle_priority(addr))
1143 run_hook('receive_menu', menu)
1144 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1147 def payto(self, addr):
1149 label = self.wallet.labels.get(addr)
1150 m_addr = label + ' <' + addr + '>' if label else addr
1151 self.tabs.setCurrentIndex(1)
1152 self.payto_e.setText(m_addr)
1153 self.amount_e.setFocus()
1156 def delete_contact(self, x):
1157 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1158 self.wallet.delete_contact(x)
1159 self.set_label(x, None)
1160 self.update_history_tab()
1161 self.update_contacts_tab()
1162 self.update_completions()
1165 def create_contact_menu(self, position):
1166 item = self.contacts_list.itemAt(position)
1168 addr = unicode(item.text(0))
1169 label = unicode(item.text(1))
1170 is_editable = item.data(0,32).toBool()
1171 payto_addr = item.data(0,33).toString()
1173 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1174 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1175 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1177 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1178 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1180 run_hook('create_contact_menu', menu, item)
1181 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1184 def update_receive_item(self, item):
1185 item.setFont(0, QFont(MONOSPACE_FONT))
1186 address = str(item.data(0,0).toString())
1187 label = self.wallet.labels.get(address,'')
1188 item.setData(1,0,label)
1189 item.setData(0,32, True) # is editable
1191 run_hook('update_receive_item', address, item)
1193 c, u = self.wallet.get_addr_balance(address)
1194 balance = self.format_amount(c + u)
1195 item.setData(2,0,balance)
1197 if self.expert_mode:
1198 if address in self.wallet.frozen_addresses:
1199 item.setBackgroundColor(0, QColor('lightblue'))
1200 elif address in self.wallet.prioritized_addresses:
1201 item.setBackgroundColor(0, QColor('lightgreen'))
1204 def update_receive_tab(self):
1205 l = self.receive_list
1208 l.setColumnHidden(2, not self.expert_mode)
1209 l.setColumnHidden(3, not self.expert_mode)
1210 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1211 l.setColumnWidth(i, width)
1213 if self.current_account is None:
1214 account_items = self.wallet.accounts.items()
1215 elif self.current_account != -1:
1216 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1220 for k, account in account_items:
1221 name = self.wallet.get_account_name(k)
1222 c,u = self.wallet.get_account_balance(k)
1223 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1224 l.addTopLevelItem(account_item)
1225 account_item.setExpanded(True)
1226 account_item.setData(0, 32, k)
1228 if not self.wallet.is_seeded(k):
1229 icon = QIcon(":icons/key.png")
1230 account_item.setIcon(0, icon)
1232 for is_change in ([0,1] if self.expert_mode else [0]):
1233 if self.expert_mode:
1234 name = _("Receiving") if not is_change else _("Change")
1235 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1236 account_item.addChild(seq_item)
1237 if not is_change: seq_item.setExpanded(True)
1239 seq_item = account_item
1243 for address in account.get_addresses(is_change):
1244 h = self.wallet.history.get(address,[])
1248 if gap > self.wallet.gap_limit:
1253 num_tx = '*' if h == ['*'] else "%d"%len(h)
1254 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1255 self.update_receive_item(item)
1257 item.setBackgroundColor(1, QColor('red'))
1258 seq_item.addChild(item)
1261 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1262 c,u = self.wallet.get_imported_balance()
1263 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1264 l.addTopLevelItem(account_item)
1265 account_item.setExpanded(True)
1266 for address in self.wallet.imported_keys.keys():
1267 item = QTreeWidgetItem( [ address, '', '', ''] )
1268 self.update_receive_item(item)
1269 account_item.addChild(item)
1272 # we use column 1 because column 0 may be hidden
1273 l.setCurrentItem(l.topLevelItem(0),1)
1276 def update_contacts_tab(self):
1277 l = self.contacts_list
1280 for address in self.wallet.addressbook:
1281 label = self.wallet.labels.get(address,'')
1282 n = self.wallet.get_num_tx(address)
1283 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1284 item.setFont(0, QFont(MONOSPACE_FONT))
1285 # 32 = label can be edited (bool)
1286 item.setData(0,32, True)
1288 item.setData(0,33, address)
1289 l.addTopLevelItem(item)
1291 run_hook('update_contacts_tab', l)
1292 l.setCurrentItem(l.topLevelItem(0))
1296 def create_console_tab(self):
1297 from console import Console
1298 self.console = console = Console()
1302 def update_console(self):
1303 console = self.console
1304 console.history = self.config.get("console-history",[])
1305 console.history_index = len(console.history)
1307 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1308 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1310 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1312 def mkfunc(f, method):
1313 return lambda *args: apply( f, (method, args, self.password_dialog ))
1315 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1316 methods[m] = mkfunc(c._run, m)
1318 console.updateNamespace(methods)
1321 def change_account(self,s):
1322 if s == _("All accounts"):
1323 self.current_account = None
1325 accounts = self.wallet.get_account_names()
1326 for k, v in accounts.items():
1328 self.current_account = k
1329 self.update_history_tab()
1330 self.update_status()
1331 self.update_receive_tab()
1333 def create_status_bar(self):
1336 sb.setFixedHeight(35)
1337 qtVersion = qVersion()
1339 self.balance_label = QLabel("")
1340 sb.addWidget(self.balance_label)
1342 from version_getter import UpdateLabel
1343 self.updatelabel = UpdateLabel(self.config, sb)
1345 self.account_selector = QComboBox()
1346 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1347 sb.addPermanentWidget(self.account_selector)
1349 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1350 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1352 self.lock_icon = QIcon()
1353 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1354 sb.addPermanentWidget( self.password_button )
1356 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1357 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1358 sb.addPermanentWidget( self.seed_button )
1359 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1360 sb.addPermanentWidget( self.status_button )
1362 run_hook('create_status_bar', (sb,))
1364 self.setStatusBar(sb)
1367 def update_lock_icon(self):
1368 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1369 self.password_button.setIcon( icon )
1372 def update_buttons_on_seed(self):
1373 if self.wallet.seed:
1374 self.seed_button.show()
1375 self.password_button.show()
1376 self.send_button.setText(_("Send"))
1378 self.password_button.hide()
1379 self.seed_button.hide()
1380 self.send_button.setText(_("Create unsigned transaction"))
1383 def change_password_dialog(self):
1384 from password_dialog import PasswordDialog
1385 d = PasswordDialog(self.wallet, self)
1387 self.update_lock_icon()
1390 def new_contact_dialog(self):
1391 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1392 address = unicode(text)
1394 if is_valid(address):
1395 self.wallet.add_contact(address)
1396 self.update_contacts_tab()
1397 self.update_history_tab()
1398 self.update_completions()
1400 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1403 def new_account_dialog(self):
1405 dialog = QDialog(self)
1407 dialog.setWindowTitle(_("New Account"))
1409 addr = self.wallet.new_account_address()
1410 vbox = QVBoxLayout()
1411 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1412 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1413 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1414 vbox.addWidget(QLabel(msg))
1415 vbox.addWidget(QLabel(_('Address')+':'))
1420 vbox.addLayout(ok_cancel_buttons(dialog))
1421 dialog.setLayout(vbox)
1428 def show_master_public_key(self):
1429 dialog = QDialog(self)
1431 dialog.setWindowTitle(_("Master Public Key"))
1433 main_text = QTextEdit()
1434 main_text.setText(self.wallet.get_master_public_key())
1435 main_text.setReadOnly(True)
1436 main_text.setMaximumHeight(170)
1437 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1439 ok_button = QPushButton(_("OK"))
1440 ok_button.setDefault(True)
1441 ok_button.clicked.connect(dialog.accept)
1443 main_layout = QGridLayout()
1444 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1446 main_layout.addWidget(main_text, 1, 0)
1447 main_layout.addWidget(qrw, 1, 1 )
1449 vbox = QVBoxLayout()
1450 vbox.addLayout(main_layout)
1451 hbox = QHBoxLayout()
1453 hbox.addWidget(ok_button)
1454 vbox.addLayout(hbox)
1456 dialog.setLayout(vbox)
1461 def show_seed_dialog(self, password):
1462 if not self.wallet.seed:
1463 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1466 seed = self.wallet.decode_seed(password)
1468 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1471 from seed_dialog import SeedDialog
1472 d = SeedDialog(self)
1473 d.show_seed(seed, self.wallet.imported_keys)
1477 def show_qrcode(self, data, title = _("QR code")):
1481 d.setWindowTitle(title)
1482 d.setMinimumSize(270, 300)
1483 vbox = QVBoxLayout()
1484 qrw = QRCodeWidget(data)
1485 vbox.addWidget(qrw, 1)
1486 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1487 hbox = QHBoxLayout()
1491 filename = "qrcode.bmp"
1492 bmp.save_qrcode(qrw.qr, filename)
1493 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1495 b = QPushButton(_("Save"))
1497 b.clicked.connect(print_qr)
1499 b = QPushButton(_("Close"))
1501 b.clicked.connect(d.accept)
1504 vbox.addLayout(hbox)
1509 def do_protect(self, func, args):
1510 if self.wallet.use_encryption:
1511 password = self.password_dialog()
1517 if args != (False,):
1518 args = (self,) + args + (password,)
1520 args = (self,password)
1525 def show_private_key(self, address, password):
1526 if not address: return
1528 pk_list = self.wallet.get_private_key(address, password)
1529 except BaseException, e:
1530 self.show_message(str(e))
1532 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1536 def do_sign(self, address, message, signature, password):
1537 message = unicode(message.toPlainText())
1538 message = message.encode('utf-8')
1540 sig = self.wallet.sign_message(str(address.text()), message, password)
1541 signature.setText(sig)
1542 except BaseException, e:
1543 self.show_message(str(e))
1545 def sign_message(self, address):
1546 if not address: return
1549 d.setWindowTitle(_('Sign Message'))
1550 d.setMinimumSize(410, 290)
1552 tab_widget = QTabWidget()
1554 layout = QGridLayout(tab)
1556 sign_address = QLineEdit()
1558 sign_address.setText(address)
1559 layout.addWidget(QLabel(_('Address')), 1, 0)
1560 layout.addWidget(sign_address, 1, 1)
1562 sign_message = QTextEdit()
1563 layout.addWidget(QLabel(_('Message')), 2, 0)
1564 layout.addWidget(sign_message, 2, 1)
1565 layout.setRowStretch(2,3)
1567 sign_signature = QTextEdit()
1568 layout.addWidget(QLabel(_('Signature')), 3, 0)
1569 layout.addWidget(sign_signature, 3, 1)
1570 layout.setRowStretch(3,1)
1573 hbox = QHBoxLayout()
1574 b = QPushButton(_("Sign"))
1576 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1577 b = QPushButton(_("Close"))
1578 b.clicked.connect(d.accept)
1580 layout.addLayout(hbox, 4, 1)
1581 tab_widget.addTab(tab, _("Sign"))
1585 layout = QGridLayout(tab)
1587 verify_address = QLineEdit()
1588 layout.addWidget(QLabel(_('Address')), 1, 0)
1589 layout.addWidget(verify_address, 1, 1)
1591 verify_message = QTextEdit()
1592 layout.addWidget(QLabel(_('Message')), 2, 0)
1593 layout.addWidget(verify_message, 2, 1)
1594 layout.setRowStretch(2,3)
1596 verify_signature = QTextEdit()
1597 layout.addWidget(QLabel(_('Signature')), 3, 0)
1598 layout.addWidget(verify_signature, 3, 1)
1599 layout.setRowStretch(3,1)
1602 message = unicode(verify_message.toPlainText())
1603 message = message.encode('utf-8')
1604 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1605 self.show_message(_("Signature verified"))
1607 self.show_message(_("Error: wrong signature"))
1609 hbox = QHBoxLayout()
1610 b = QPushButton(_("Verify"))
1611 b.clicked.connect(do_verify)
1613 b = QPushButton(_("Close"))
1614 b.clicked.connect(d.accept)
1616 layout.addLayout(hbox, 4, 1)
1617 tab_widget.addTab(tab, _("Verify"))
1619 vbox = QVBoxLayout()
1620 vbox.addWidget(tab_widget)
1627 def question(self, msg):
1628 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1630 def show_message(self, msg):
1631 QMessageBox.information(self, _('Message'), msg, _('OK'))
1633 def password_dialog(self ):
1640 vbox = QVBoxLayout()
1641 msg = _('Please enter your password')
1642 vbox.addWidget(QLabel(msg))
1644 grid = QGridLayout()
1646 grid.addWidget(QLabel(_('Password')), 1, 0)
1647 grid.addWidget(pw, 1, 1)
1648 vbox.addLayout(grid)
1650 vbox.addLayout(ok_cancel_buttons(d))
1653 run_hook('password_dialog', pw, grid, 1)
1654 if not d.exec_(): return
1655 return unicode(pw.text())
1664 def tx_from_text(self, txt):
1665 "json or raw hexadecimal"
1668 tx = Transaction(txt)
1674 tx_dict = json.loads(str(txt))
1675 assert "hex" in tx_dict.keys()
1676 assert "complete" in tx_dict.keys()
1677 if not tx_dict["complete"]:
1678 assert "input_info" in tx_dict.keys()
1679 tx = Transaction(tx_dict["hex"])
1684 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1688 def read_tx_from_file(self):
1689 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1693 with open(fileName, "r") as f:
1694 file_content = f.read()
1695 except (ValueError, IOError, os.error), reason:
1696 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1698 return self.tx_from_text(file_content)
1702 def sign_raw_transaction(self, tx, input_info, password):
1703 self.wallet.signrawtransaction(tx, input_info, [], password)
1705 def do_process_from_text(self):
1706 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1709 tx = self.tx_from_text(text)
1711 self.show_transaction(tx)
1713 def do_process_from_file(self):
1714 tx = self.read_tx_from_file()
1716 self.show_transaction(tx)
1718 def do_process_from_csvReader(self, csvReader):
1721 for row in csvReader:
1723 amount = float(row[1])
1724 amount = int(100000000*amount)
1725 outputs.append((address, amount))
1726 except (ValueError, IOError, os.error), reason:
1727 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1731 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1732 except BaseException, e:
1733 self.show_message(str(e))
1736 self.show_transaction(tx)
1738 def do_process_from_csv_file(self):
1739 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1743 with open(fileName, "r") as f:
1744 csvReader = csv.reader(f)
1745 self.do_process_from_csvReader(csvReader)
1746 except (ValueError, IOError, os.error), reason:
1747 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1750 def do_process_from_csv_text(self):
1751 text = text_dialog(self, _('Input CSV'), _("CSV:"), _("Load CSV"))
1754 f = StringIO.StringIO(text)
1755 csvReader = csv.reader(f)
1756 self.do_process_from_csvReader(csvReader)
1761 def do_export_privkeys(self, password):
1762 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.")))
1765 select_export = _('Select file to export your private keys to')
1766 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1768 with open(fileName, "w+") as csvfile:
1769 transaction = csv.writer(csvfile)
1770 transaction.writerow(["address", "private_key"])
1772 addresses = self.wallet.addresses(True)
1774 for addr in addresses:
1775 pk = "".join(self.wallet.get_private_key(addr, password))
1776 transaction.writerow(["%34s"%addr,pk])
1778 self.show_message(_("Private keys exported."))
1780 except (IOError, os.error), reason:
1781 export_error_label = _("Electrum was unable to produce a private key-export.")
1782 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1784 except BaseException, e:
1785 self.show_message(str(e))
1789 def do_import_labels(self):
1790 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1791 if not labelsFile: return
1793 f = open(labelsFile, 'r')
1796 for key, value in json.loads(data).items():
1797 self.wallet.set_label(key, value)
1798 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1799 except (IOError, os.error), reason:
1800 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1803 def do_export_labels(self):
1804 labels = self.wallet.labels
1806 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1808 with open(fileName, 'w+') as f:
1809 json.dump(labels, f)
1810 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1811 except (IOError, os.error), reason:
1812 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1815 def do_export_history(self):
1816 from lite_window import csv_transaction
1817 csv_transaction(self.wallet)
1821 def do_import_privkey(self, password):
1822 if not self.wallet.imported_keys:
1823 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1824 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1825 + _('Are you sure you understand what you are doing?'), 3, 4)
1828 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1831 text = str(text).split()
1836 addr = self.wallet.import_key(key, password)
1837 except BaseException as e:
1843 addrlist.append(addr)
1845 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1847 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1848 self.update_receive_tab()
1849 self.update_history_tab()
1852 def settings_dialog(self):
1854 d.setWindowTitle(_('Electrum Settings'))
1856 vbox = QVBoxLayout()
1858 tabs = QTabWidget(self)
1859 self.settings_tab = tabs
1860 vbox.addWidget(tabs)
1863 grid_ui = QGridLayout(tab1)
1864 grid_ui.setColumnStretch(0,1)
1865 tabs.addTab(tab1, _('Display') )
1867 nz_label = QLabel(_('Display zeros'))
1868 grid_ui.addWidget(nz_label, 0, 0)
1869 nz_e = AmountEdit(None,True)
1870 nz_e.setText("%d"% self.num_zeros)
1871 grid_ui.addWidget(nz_e, 0, 1)
1872 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1873 grid_ui.addWidget(HelpButton(msg), 0, 2)
1874 if not self.config.is_modifiable('num_zeros'):
1875 for w in [nz_e, nz_label]: w.setEnabled(False)
1877 lang_label=QLabel(_('Language') + ':')
1878 grid_ui.addWidget(lang_label, 1, 0)
1879 lang_combo = QComboBox()
1880 from electrum.i18n import languages
1881 lang_combo.addItems(languages.values())
1883 index = languages.keys().index(self.config.get("language",''))
1886 lang_combo.setCurrentIndex(index)
1887 grid_ui.addWidget(lang_combo, 1, 1)
1888 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1889 if not self.config.is_modifiable('language'):
1890 for w in [lang_combo, lang_label]: w.setEnabled(False)
1892 expert_cb = QCheckBox(_('Expert mode'))
1893 expert_cb.setChecked(self.expert_mode)
1894 grid_ui.addWidget(expert_cb, 3, 0)
1895 hh = _('In expert mode, your client will:') + '\n' \
1896 + _(' - Show change addresses in the Receive tab') + '\n' \
1897 + _(' - Display the balance of each address') + '\n' \
1898 + _(' - Add freeze/prioritize actions to addresses.')
1899 grid_ui.addWidget(HelpButton(hh), 3, 2)
1900 grid_ui.setRowStretch(4,1)
1904 grid_wallet = QGridLayout(tab2)
1905 grid_wallet.setColumnStretch(0,1)
1906 tabs.addTab(tab2, _('Wallet') )
1908 fee_label = QLabel(_('Transaction fee'))
1909 grid_wallet.addWidget(fee_label, 0, 0)
1910 fee_e = AmountEdit(self.base_unit)
1911 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1912 grid_wallet.addWidget(fee_e, 0, 2)
1913 msg = _('Fee per kilobyte of transaction.') + ' ' \
1914 + _('Recommended value') + ': ' + self.format_amount(50000)
1915 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1916 if not self.config.is_modifiable('fee_per_kb'):
1917 for w in [fee_e, fee_label]: w.setEnabled(False)
1919 usechange_cb = QCheckBox(_('Use change addresses'))
1920 usechange_cb.setChecked(self.wallet.use_change)
1921 grid_wallet.addWidget(usechange_cb, 1, 0)
1922 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1923 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1925 units = ['BTC', 'mBTC']
1926 unit_label = QLabel(_('Base unit'))
1927 grid_wallet.addWidget(unit_label, 3, 0)
1928 unit_combo = QComboBox()
1929 unit_combo.addItems(units)
1930 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1931 grid_wallet.addWidget(unit_combo, 3, 2)
1932 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1933 + '\n1BTC=1000mBTC.\n' \
1934 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1935 grid_wallet.setRowStretch(4,1)
1938 run_hook('create_settings_tab', tabs)
1940 vbox.addLayout(ok_cancel_buttons(d))
1944 if not d.exec_(): return
1946 fee = unicode(fee_e.text())
1948 fee = self.read_amount(fee)
1950 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1953 self.wallet.set_fee(fee)
1955 nz = unicode(nz_e.text())
1960 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1963 if self.num_zeros != nz:
1965 self.config.set_key('num_zeros', nz, True)
1966 self.update_history_tab()
1967 self.update_receive_tab()
1969 usechange_result = usechange_cb.isChecked()
1970 if self.wallet.use_change != usechange_result:
1971 self.wallet.use_change = usechange_result
1972 self.config.set_key('use_change', self.wallet.use_change, True)
1974 unit_result = units[unit_combo.currentIndex()]
1975 if self.base_unit() != unit_result:
1976 self.decimal_point = 8 if unit_result == 'BTC' else 5
1977 self.config.set_key('decimal_point', self.decimal_point, True)
1978 self.update_history_tab()
1979 self.update_status()
1981 need_restart = False
1983 lang_request = languages.keys()[lang_combo.currentIndex()]
1984 if lang_request != self.config.get('language'):
1985 self.config.set_key("language", lang_request, True)
1989 run_hook('close_settings_dialog')
1992 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1994 self.receive_tab_set_mode(expert_cb.isChecked())
1996 def run_network_dialog(self):
1997 NetworkDialog(self.wallet.network, self.config, self).do_exec()
1999 def closeEvent(self, event):
2001 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2002 self.save_column_widths()
2003 self.config.set_key("console-history", self.console.history[-50:], True)
2008 def plugins_dialog(self):
2009 from electrum.plugins import plugins
2012 d.setWindowTitle(_('Electrum Plugins'))
2015 vbox = QVBoxLayout(d)
2018 scroll = QScrollArea()
2019 scroll.setEnabled(True)
2020 scroll.setWidgetResizable(True)
2021 scroll.setMinimumSize(400,250)
2022 vbox.addWidget(scroll)
2026 w.setMinimumHeight(len(plugins)*35)
2028 grid = QGridLayout()
2029 grid.setColumnStretch(0,1)
2032 def mk_toggle(cb, p):
2033 return lambda: cb.setChecked(p.toggle())
2034 for i, p in enumerate(plugins):
2036 cb = QCheckBox(p.fullname())
2037 cb.setDisabled(not p.is_available())
2038 cb.setChecked(p.is_enabled())
2039 cb.clicked.connect(mk_toggle(cb,p))
2040 grid.addWidget(cb, i, 0)
2041 if p.requires_settings():
2042 grid.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2043 grid.addWidget(HelpButton(p.description()), i, 2)
2045 print_msg(_("Error: cannot display plugin"), p)
2046 traceback.print_exc(file=sys.stdout)
2047 grid.setRowStretch(i+1,1)
2049 vbox.addLayout(close_button(d))
2054 def show_account_details(self, k):
2056 d.setWindowTitle(_('Account Details'))
2059 vbox = QVBoxLayout(d)
2060 roots = self.wallet.get_roots(k)
2062 name = self.wallet.get_account_name(k)
2063 label = QLabel('Name: ' + name)
2064 vbox.addWidget(label)
2066 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2067 vbox.addWidget(QLabel('Type: ' + acctype))
2069 label = QLabel('Derivation: ' + k)
2070 vbox.addWidget(label)
2073 # mpk = self.wallet.master_public_keys[root]
2074 # text = QTextEdit()
2075 # text.setReadOnly(True)
2076 # text.setMaximumHeight(120)
2077 # text.setText(repr(mpk))
2078 # vbox.addWidget(text)
2080 vbox.addLayout(close_button(d))