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', 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'))
908 self.set_label(tx.hash(), label)
911 h = self.wallet.send_tx(tx)
912 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
913 status, msg = self.wallet.receive_tx( h )
915 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
917 self.update_contacts_tab()
919 QMessageBox.warning(self, _('Error'), msg, _('OK'))
921 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
923 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
924 with open(fileName,'w') as f:
925 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
926 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
928 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
930 # add recipient to addressbook
931 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
932 self.wallet.addressbook.append(to_address)
937 def set_url(self, url):
938 address, amount, label, message, signature, identity, url = util.parse_url(url)
940 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
943 self.mini.set_payment_fields(address, amount)
945 if label and self.wallet.labels.get(address) != label:
946 if self.question('Give label "%s" to address %s ?'%(label,address)):
947 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
948 self.wallet.addressbook.append(address)
949 self.set_label(address, label)
951 run_hook('set_url', url, self.show_message, self.question)
953 self.tabs.setCurrentIndex(1)
954 label = self.wallet.labels.get(address)
955 m_addr = label + ' <'+ address +'>' if label else address
956 self.payto_e.setText(m_addr)
958 self.message_e.setText(message)
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(_("Minimize"), lambda: item.setExpanded(False))
1106 menu.addAction(_("Maximize"), 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'), _("Please enter a list of outputs.") + '\n' \
1752 + _("Format: address, amount. One output per line"), _("Load CSV"))
1755 f = StringIO.StringIO(text)
1756 csvReader = csv.reader(f)
1757 self.do_process_from_csvReader(csvReader)
1762 def do_export_privkeys(self, password):
1763 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.")))
1766 select_export = _('Select file to export your private keys to')
1767 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1769 with open(fileName, "w+") as csvfile:
1770 transaction = csv.writer(csvfile)
1771 transaction.writerow(["address", "private_key"])
1773 addresses = self.wallet.addresses(True)
1775 for addr in addresses:
1776 pk = "".join(self.wallet.get_private_key(addr, password))
1777 transaction.writerow(["%34s"%addr,pk])
1779 self.show_message(_("Private keys exported."))
1781 except (IOError, os.error), reason:
1782 export_error_label = _("Electrum was unable to produce a private key-export.")
1783 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1785 except BaseException, e:
1786 self.show_message(str(e))
1790 def do_import_labels(self):
1791 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1792 if not labelsFile: return
1794 f = open(labelsFile, 'r')
1797 for key, value in json.loads(data).items():
1798 self.wallet.set_label(key, value)
1799 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1800 except (IOError, os.error), reason:
1801 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1804 def do_export_labels(self):
1805 labels = self.wallet.labels
1807 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1809 with open(fileName, 'w+') as f:
1810 json.dump(labels, f)
1811 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1812 except (IOError, os.error), reason:
1813 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1816 def do_export_history(self):
1817 from lite_window import csv_transaction
1818 csv_transaction(self.wallet)
1822 def do_import_privkey(self, password):
1823 if not self.wallet.imported_keys:
1824 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1825 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1826 + _('Are you sure you understand what you are doing?'), 3, 4)
1829 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1832 text = str(text).split()
1837 addr = self.wallet.import_key(key, password)
1838 except BaseException as e:
1844 addrlist.append(addr)
1846 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1848 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1849 self.update_receive_tab()
1850 self.update_history_tab()
1853 def settings_dialog(self):
1855 d.setWindowTitle(_('Electrum Settings'))
1857 vbox = QVBoxLayout()
1859 tabs = QTabWidget(self)
1860 self.settings_tab = tabs
1861 vbox.addWidget(tabs)
1864 grid_ui = QGridLayout(tab1)
1865 grid_ui.setColumnStretch(0,1)
1866 tabs.addTab(tab1, _('Display') )
1868 nz_label = QLabel(_('Display zeros'))
1869 grid_ui.addWidget(nz_label, 0, 0)
1870 nz_e = AmountEdit(None,True)
1871 nz_e.setText("%d"% self.num_zeros)
1872 grid_ui.addWidget(nz_e, 0, 1)
1873 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1874 grid_ui.addWidget(HelpButton(msg), 0, 2)
1875 if not self.config.is_modifiable('num_zeros'):
1876 for w in [nz_e, nz_label]: w.setEnabled(False)
1878 lang_label=QLabel(_('Language') + ':')
1879 grid_ui.addWidget(lang_label, 1, 0)
1880 lang_combo = QComboBox()
1881 from electrum.i18n import languages
1882 lang_combo.addItems(languages.values())
1884 index = languages.keys().index(self.config.get("language",''))
1887 lang_combo.setCurrentIndex(index)
1888 grid_ui.addWidget(lang_combo, 1, 1)
1889 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1890 if not self.config.is_modifiable('language'):
1891 for w in [lang_combo, lang_label]: w.setEnabled(False)
1893 expert_cb = QCheckBox(_('Expert mode'))
1894 expert_cb.setChecked(self.expert_mode)
1895 grid_ui.addWidget(expert_cb, 3, 0)
1896 hh = _('In expert mode, your client will:') + '\n' \
1897 + _(' - Show change addresses in the Receive tab') + '\n' \
1898 + _(' - Display the balance of each address') + '\n' \
1899 + _(' - Add freeze/prioritize actions to addresses.')
1900 grid_ui.addWidget(HelpButton(hh), 3, 2)
1901 grid_ui.setRowStretch(4,1)
1905 grid_wallet = QGridLayout(tab2)
1906 grid_wallet.setColumnStretch(0,1)
1907 tabs.addTab(tab2, _('Wallet') )
1909 fee_label = QLabel(_('Transaction fee'))
1910 grid_wallet.addWidget(fee_label, 0, 0)
1911 fee_e = AmountEdit(self.base_unit)
1912 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1913 grid_wallet.addWidget(fee_e, 0, 2)
1914 msg = _('Fee per kilobyte of transaction.') + ' ' \
1915 + _('Recommended value') + ': ' + self.format_amount(50000)
1916 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1917 if not self.config.is_modifiable('fee_per_kb'):
1918 for w in [fee_e, fee_label]: w.setEnabled(False)
1920 usechange_cb = QCheckBox(_('Use change addresses'))
1921 usechange_cb.setChecked(self.wallet.use_change)
1922 grid_wallet.addWidget(usechange_cb, 1, 0)
1923 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1924 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1926 units = ['BTC', 'mBTC']
1927 unit_label = QLabel(_('Base unit'))
1928 grid_wallet.addWidget(unit_label, 3, 0)
1929 unit_combo = QComboBox()
1930 unit_combo.addItems(units)
1931 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1932 grid_wallet.addWidget(unit_combo, 3, 2)
1933 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1934 + '\n1BTC=1000mBTC.\n' \
1935 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1936 grid_wallet.setRowStretch(4,1)
1939 run_hook('create_settings_tab', tabs)
1941 vbox.addLayout(ok_cancel_buttons(d))
1945 if not d.exec_(): return
1947 fee = unicode(fee_e.text())
1949 fee = self.read_amount(fee)
1951 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1954 self.wallet.set_fee(fee)
1956 nz = unicode(nz_e.text())
1961 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1964 if self.num_zeros != nz:
1966 self.config.set_key('num_zeros', nz, True)
1967 self.update_history_tab()
1968 self.update_receive_tab()
1970 usechange_result = usechange_cb.isChecked()
1971 if self.wallet.use_change != usechange_result:
1972 self.wallet.use_change = usechange_result
1973 self.config.set_key('use_change', self.wallet.use_change, True)
1975 unit_result = units[unit_combo.currentIndex()]
1976 if self.base_unit() != unit_result:
1977 self.decimal_point = 8 if unit_result == 'BTC' else 5
1978 self.config.set_key('decimal_point', self.decimal_point, True)
1979 self.update_history_tab()
1980 self.update_status()
1982 need_restart = False
1984 lang_request = languages.keys()[lang_combo.currentIndex()]
1985 if lang_request != self.config.get('language'):
1986 self.config.set_key("language", lang_request, True)
1990 run_hook('close_settings_dialog')
1993 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1995 self.receive_tab_set_mode(expert_cb.isChecked())
1997 def run_network_dialog(self):
1998 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2000 def closeEvent(self, event):
2002 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2003 self.save_column_widths()
2004 self.config.set_key("console-history", self.console.history[-50:], True)
2009 def plugins_dialog(self):
2010 from electrum.plugins import plugins
2013 d.setWindowTitle(_('Electrum Plugins'))
2016 vbox = QVBoxLayout(d)
2019 scroll = QScrollArea()
2020 scroll.setEnabled(True)
2021 scroll.setWidgetResizable(True)
2022 scroll.setMinimumSize(400,250)
2023 vbox.addWidget(scroll)
2027 w.setMinimumHeight(len(plugins)*35)
2029 grid = QGridLayout()
2030 grid.setColumnStretch(0,1)
2033 def mk_toggle(cb, p):
2034 return lambda: cb.setChecked(p.toggle())
2035 for i, p in enumerate(plugins):
2037 cb = QCheckBox(p.fullname())
2038 cb.setDisabled(not p.is_available())
2039 cb.setChecked(p.is_enabled())
2040 cb.clicked.connect(mk_toggle(cb,p))
2041 grid.addWidget(cb, i, 0)
2042 if p.requires_settings():
2043 grid.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2044 grid.addWidget(HelpButton(p.description()), i, 2)
2046 print_msg(_("Error: cannot display plugin"), p)
2047 traceback.print_exc(file=sys.stdout)
2048 grid.setRowStretch(i+1,1)
2050 vbox.addLayout(close_button(d))
2055 def show_account_details(self, k):
2057 d.setWindowTitle(_('Account Details'))
2060 vbox = QVBoxLayout(d)
2061 roots = self.wallet.get_roots(k)
2063 name = self.wallet.get_account_name(k)
2064 label = QLabel('Name: ' + name)
2065 vbox.addWidget(label)
2067 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2068 vbox.addWidget(QLabel('Type: ' + acctype))
2070 label = QLabel('Derivation: ' + k)
2071 vbox.addWidget(label)
2074 # mpk = self.wallet.master_public_keys[root]
2075 # text = QTextEdit()
2076 # text.setReadOnly(True)
2077 # text.setMaximumHeight(120)
2078 # text.setText(repr(mpk))
2079 # vbox.addWidget(text)
2081 vbox.addLayout(close_button(d))