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 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
945 self.mini.set_payment_fields(address, amount)
947 if label and self.wallet.labels.get(address) != label:
948 if self.question('Give label "%s" to address %s ?'%(label,address)):
949 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
950 self.wallet.addressbook.append(address)
951 self.set_label(address, label)
953 run_hook('set_url', url, self.show_message, self.question)
955 self.tabs.setCurrentIndex(1)
956 label = self.wallet.labels.get(address)
957 m_addr = label + ' <'+ address +'>' if label else address
958 self.payto_e.setText(m_addr)
960 self.message_e.setText(message)
962 self.amount_e.setText(amount)
965 self.set_frozen(self.payto_e,True)
966 self.set_frozen(self.amount_e,True)
967 self.set_frozen(self.message_e,True)
968 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
970 self.payto_sig.setVisible(False)
973 self.payto_sig.setVisible(False)
974 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
976 self.set_frozen(e,False)
979 def set_frozen(self,entry,frozen):
981 entry.setReadOnly(True)
982 entry.setFrame(False)
984 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
985 entry.setPalette(palette)
987 entry.setReadOnly(False)
990 palette.setColor(entry.backgroundRole(), QColor('white'))
991 entry.setPalette(palette)
994 def toggle_freeze(self,addr):
996 if addr in self.wallet.frozen_addresses:
997 self.wallet.unfreeze(addr)
999 self.wallet.freeze(addr)
1000 self.update_receive_tab()
1002 def toggle_priority(self,addr):
1004 if addr in self.wallet.prioritized_addresses:
1005 self.wallet.unprioritize(addr)
1007 self.wallet.prioritize(addr)
1008 self.update_receive_tab()
1011 def create_list_tab(self, headers):
1012 "generic tab creation method"
1013 l = MyTreeWidget(self)
1014 l.setColumnCount( len(headers) )
1015 l.setHeaderLabels( headers )
1018 vbox = QVBoxLayout()
1025 vbox.addWidget(buttons)
1027 hbox = QHBoxLayout()
1030 buttons.setLayout(hbox)
1035 def create_receive_tab(self):
1036 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1037 l.setContextMenuPolicy(Qt.CustomContextMenu)
1038 l.customContextMenuRequested.connect(self.create_receive_menu)
1039 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1040 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1041 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1042 self.receive_list = l
1043 self.receive_buttons_hbox = hbox
1048 def receive_tab_set_mode(self, i):
1049 self.save_column_widths()
1050 self.expert_mode = (i == 1)
1051 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1052 self.update_receive_tab()
1055 def save_column_widths(self):
1056 if not self.expert_mode:
1057 widths = [ self.receive_list.columnWidth(0) ]
1060 for i in range(self.receive_list.columnCount() -1):
1061 widths.append(self.receive_list.columnWidth(i))
1062 self.column_widths["receive"][self.expert_mode] = widths
1064 self.column_widths["history"] = []
1065 for i in range(self.history_list.columnCount() - 1):
1066 self.column_widths["history"].append(self.history_list.columnWidth(i))
1068 self.column_widths["contacts"] = []
1069 for i in range(self.contacts_list.columnCount() - 1):
1070 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1072 self.config.set_key("column_widths", self.column_widths, True)
1075 def create_contacts_tab(self):
1076 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1077 l.setContextMenuPolicy(Qt.CustomContextMenu)
1078 l.customContextMenuRequested.connect(self.create_contact_menu)
1079 for i,width in enumerate(self.column_widths['contacts']):
1080 l.setColumnWidth(i, width)
1082 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1083 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1084 self.contacts_list = l
1085 self.contacts_buttons_hbox = hbox
1090 def delete_imported_key(self, addr):
1091 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1092 self.wallet.delete_imported_key(addr)
1093 self.update_receive_tab()
1094 self.update_history_tab()
1096 def edit_account_label(self, k):
1097 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1099 label = unicode(text)
1100 self.set_label(k,label)
1101 self.update_receive_tab()
1103 def create_account_menu(self, position, k, item):
1105 if item.isExpanded():
1106 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1108 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1109 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1110 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1111 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1113 def create_receive_menu(self, position):
1114 # fixme: this function apparently has a side effect.
1115 # if it is not called the menu pops up several times
1116 #self.receive_list.selectedIndexes()
1118 item = self.receive_list.itemAt(position)
1121 addr = unicode(item.text(0))
1122 if not is_valid(addr):
1123 k = str(item.data(0,32).toString())
1125 self.create_account_menu(position, k, item)
1127 item.setExpanded(not item.isExpanded())
1131 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1132 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1133 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1134 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1135 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1136 if addr in self.wallet.imported_keys:
1137 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1139 if self.expert_mode:
1140 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1141 menu.addAction(t, lambda: self.toggle_freeze(addr))
1142 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1143 menu.addAction(t, lambda: self.toggle_priority(addr))
1145 run_hook('receive_menu', menu)
1146 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1149 def payto(self, addr):
1151 label = self.wallet.labels.get(addr)
1152 m_addr = label + ' <' + addr + '>' if label else addr
1153 self.tabs.setCurrentIndex(1)
1154 self.payto_e.setText(m_addr)
1155 self.amount_e.setFocus()
1158 def delete_contact(self, x):
1159 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1160 self.wallet.delete_contact(x)
1161 self.set_label(x, None)
1162 self.update_history_tab()
1163 self.update_contacts_tab()
1164 self.update_completions()
1167 def create_contact_menu(self, position):
1168 item = self.contacts_list.itemAt(position)
1170 addr = unicode(item.text(0))
1171 label = unicode(item.text(1))
1172 is_editable = item.data(0,32).toBool()
1173 payto_addr = item.data(0,33).toString()
1175 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1176 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1177 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1179 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1180 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1182 run_hook('create_contact_menu', menu, item)
1183 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1186 def update_receive_item(self, item):
1187 item.setFont(0, QFont(MONOSPACE_FONT))
1188 address = str(item.data(0,0).toString())
1189 label = self.wallet.labels.get(address,'')
1190 item.setData(1,0,label)
1191 item.setData(0,32, True) # is editable
1193 run_hook('update_receive_item', address, item)
1195 c, u = self.wallet.get_addr_balance(address)
1196 balance = self.format_amount(c + u)
1197 item.setData(2,0,balance)
1199 if self.expert_mode:
1200 if address in self.wallet.frozen_addresses:
1201 item.setBackgroundColor(0, QColor('lightblue'))
1202 elif address in self.wallet.prioritized_addresses:
1203 item.setBackgroundColor(0, QColor('lightgreen'))
1206 def update_receive_tab(self):
1207 l = self.receive_list
1210 l.setColumnHidden(2, not self.expert_mode)
1211 l.setColumnHidden(3, not self.expert_mode)
1212 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1213 l.setColumnWidth(i, width)
1215 if self.current_account is None:
1216 account_items = self.wallet.accounts.items()
1217 elif self.current_account != -1:
1218 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1222 for k, account in account_items:
1223 name = self.wallet.get_account_name(k)
1224 c,u = self.wallet.get_account_balance(k)
1225 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1226 l.addTopLevelItem(account_item)
1227 account_item.setExpanded(True)
1228 account_item.setData(0, 32, k)
1230 if not self.wallet.is_seeded(k):
1231 icon = QIcon(":icons/key.png")
1232 account_item.setIcon(0, icon)
1234 for is_change in ([0,1] if self.expert_mode else [0]):
1235 if self.expert_mode:
1236 name = _("Receiving") if not is_change else _("Change")
1237 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1238 account_item.addChild(seq_item)
1239 if not is_change: seq_item.setExpanded(True)
1241 seq_item = account_item
1245 for address in account.get_addresses(is_change):
1246 h = self.wallet.history.get(address,[])
1250 if gap > self.wallet.gap_limit:
1255 num_tx = '*' if h == ['*'] else "%d"%len(h)
1256 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1257 self.update_receive_item(item)
1259 item.setBackgroundColor(1, QColor('red'))
1260 seq_item.addChild(item)
1263 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1264 c,u = self.wallet.get_imported_balance()
1265 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1266 l.addTopLevelItem(account_item)
1267 account_item.setExpanded(True)
1268 for address in self.wallet.imported_keys.keys():
1269 item = QTreeWidgetItem( [ address, '', '', ''] )
1270 self.update_receive_item(item)
1271 account_item.addChild(item)
1274 # we use column 1 because column 0 may be hidden
1275 l.setCurrentItem(l.topLevelItem(0),1)
1278 def update_contacts_tab(self):
1279 l = self.contacts_list
1282 for address in self.wallet.addressbook:
1283 label = self.wallet.labels.get(address,'')
1284 n = self.wallet.get_num_tx(address)
1285 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1286 item.setFont(0, QFont(MONOSPACE_FONT))
1287 # 32 = label can be edited (bool)
1288 item.setData(0,32, True)
1290 item.setData(0,33, address)
1291 l.addTopLevelItem(item)
1293 run_hook('update_contacts_tab', l)
1294 l.setCurrentItem(l.topLevelItem(0))
1298 def create_console_tab(self):
1299 from console import Console
1300 self.console = console = Console()
1304 def update_console(self):
1305 console = self.console
1306 console.history = self.config.get("console-history",[])
1307 console.history_index = len(console.history)
1309 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1310 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1312 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1314 def mkfunc(f, method):
1315 return lambda *args: apply( f, (method, args, self.password_dialog ))
1317 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1318 methods[m] = mkfunc(c._run, m)
1320 console.updateNamespace(methods)
1323 def change_account(self,s):
1324 if s == _("All accounts"):
1325 self.current_account = None
1327 accounts = self.wallet.get_account_names()
1328 for k, v in accounts.items():
1330 self.current_account = k
1331 self.update_history_tab()
1332 self.update_status()
1333 self.update_receive_tab()
1335 def create_status_bar(self):
1338 sb.setFixedHeight(35)
1339 qtVersion = qVersion()
1341 self.balance_label = QLabel("")
1342 sb.addWidget(self.balance_label)
1344 from version_getter import UpdateLabel
1345 self.updatelabel = UpdateLabel(self.config, sb)
1347 self.account_selector = QComboBox()
1348 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1349 sb.addPermanentWidget(self.account_selector)
1351 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1352 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1354 self.lock_icon = QIcon()
1355 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1356 sb.addPermanentWidget( self.password_button )
1358 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1359 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1360 sb.addPermanentWidget( self.seed_button )
1361 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1362 sb.addPermanentWidget( self.status_button )
1364 run_hook('create_status_bar', (sb,))
1366 self.setStatusBar(sb)
1369 def update_lock_icon(self):
1370 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1371 self.password_button.setIcon( icon )
1374 def update_buttons_on_seed(self):
1375 if self.wallet.seed:
1376 self.seed_button.show()
1377 self.password_button.show()
1378 self.send_button.setText(_("Send"))
1380 self.password_button.hide()
1381 self.seed_button.hide()
1382 self.send_button.setText(_("Create unsigned transaction"))
1385 def change_password_dialog(self):
1386 from password_dialog import PasswordDialog
1387 d = PasswordDialog(self.wallet, self)
1389 self.update_lock_icon()
1392 def new_contact_dialog(self):
1393 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1394 address = unicode(text)
1396 if is_valid(address):
1397 self.wallet.add_contact(address)
1398 self.update_contacts_tab()
1399 self.update_history_tab()
1400 self.update_completions()
1402 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1405 def new_account_dialog(self):
1407 dialog = QDialog(self)
1409 dialog.setWindowTitle(_("New Account"))
1411 addr = self.wallet.new_account_address()
1412 vbox = QVBoxLayout()
1413 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1414 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1415 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1416 vbox.addWidget(QLabel(msg))
1417 vbox.addWidget(QLabel(_('Address')+':'))
1422 vbox.addLayout(ok_cancel_buttons(dialog))
1423 dialog.setLayout(vbox)
1430 def show_master_public_key(self):
1431 dialog = QDialog(self)
1433 dialog.setWindowTitle(_("Master Public Key"))
1435 main_text = QTextEdit()
1436 main_text.setText(self.wallet.get_master_public_key())
1437 main_text.setReadOnly(True)
1438 main_text.setMaximumHeight(170)
1439 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1441 ok_button = QPushButton(_("OK"))
1442 ok_button.setDefault(True)
1443 ok_button.clicked.connect(dialog.accept)
1445 main_layout = QGridLayout()
1446 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1448 main_layout.addWidget(main_text, 1, 0)
1449 main_layout.addWidget(qrw, 1, 1 )
1451 vbox = QVBoxLayout()
1452 vbox.addLayout(main_layout)
1453 hbox = QHBoxLayout()
1455 hbox.addWidget(ok_button)
1456 vbox.addLayout(hbox)
1458 dialog.setLayout(vbox)
1463 def show_seed_dialog(self, password):
1464 if not self.wallet.seed:
1465 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1468 seed = self.wallet.decode_seed(password)
1470 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1473 from seed_dialog import SeedDialog
1474 d = SeedDialog(self)
1475 d.show_seed(seed, self.wallet.imported_keys)
1479 def show_qrcode(self, data, title = _("QR code")):
1483 d.setWindowTitle(title)
1484 d.setMinimumSize(270, 300)
1485 vbox = QVBoxLayout()
1486 qrw = QRCodeWidget(data)
1487 vbox.addWidget(qrw, 1)
1488 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1489 hbox = QHBoxLayout()
1493 filename = "qrcode.bmp"
1494 bmp.save_qrcode(qrw.qr, filename)
1495 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1497 b = QPushButton(_("Save"))
1499 b.clicked.connect(print_qr)
1501 b = QPushButton(_("Close"))
1503 b.clicked.connect(d.accept)
1506 vbox.addLayout(hbox)
1511 def do_protect(self, func, args):
1512 if self.wallet.use_encryption:
1513 password = self.password_dialog()
1519 if args != (False,):
1520 args = (self,) + args + (password,)
1522 args = (self,password)
1527 def show_private_key(self, address, password):
1528 if not address: return
1530 pk_list = self.wallet.get_private_key(address, password)
1531 except BaseException, e:
1532 self.show_message(str(e))
1534 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1538 def do_sign(self, address, message, signature, password):
1539 message = unicode(message.toPlainText())
1540 message = message.encode('utf-8')
1542 sig = self.wallet.sign_message(str(address.text()), message, password)
1543 signature.setText(sig)
1544 except BaseException, e:
1545 self.show_message(str(e))
1547 def sign_message(self, address):
1548 if not address: return
1551 d.setWindowTitle(_('Sign Message'))
1552 d.setMinimumSize(410, 290)
1554 tab_widget = QTabWidget()
1556 layout = QGridLayout(tab)
1558 sign_address = QLineEdit()
1560 sign_address.setText(address)
1561 layout.addWidget(QLabel(_('Address')), 1, 0)
1562 layout.addWidget(sign_address, 1, 1)
1564 sign_message = QTextEdit()
1565 layout.addWidget(QLabel(_('Message')), 2, 0)
1566 layout.addWidget(sign_message, 2, 1)
1567 layout.setRowStretch(2,3)
1569 sign_signature = QTextEdit()
1570 layout.addWidget(QLabel(_('Signature')), 3, 0)
1571 layout.addWidget(sign_signature, 3, 1)
1572 layout.setRowStretch(3,1)
1575 hbox = QHBoxLayout()
1576 b = QPushButton(_("Sign"))
1578 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1579 b = QPushButton(_("Close"))
1580 b.clicked.connect(d.accept)
1582 layout.addLayout(hbox, 4, 1)
1583 tab_widget.addTab(tab, _("Sign"))
1587 layout = QGridLayout(tab)
1589 verify_address = QLineEdit()
1590 layout.addWidget(QLabel(_('Address')), 1, 0)
1591 layout.addWidget(verify_address, 1, 1)
1593 verify_message = QTextEdit()
1594 layout.addWidget(QLabel(_('Message')), 2, 0)
1595 layout.addWidget(verify_message, 2, 1)
1596 layout.setRowStretch(2,3)
1598 verify_signature = QTextEdit()
1599 layout.addWidget(QLabel(_('Signature')), 3, 0)
1600 layout.addWidget(verify_signature, 3, 1)
1601 layout.setRowStretch(3,1)
1604 message = unicode(verify_message.toPlainText())
1605 message = message.encode('utf-8')
1606 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1607 self.show_message(_("Signature verified"))
1609 self.show_message(_("Error: wrong signature"))
1611 hbox = QHBoxLayout()
1612 b = QPushButton(_("Verify"))
1613 b.clicked.connect(do_verify)
1615 b = QPushButton(_("Close"))
1616 b.clicked.connect(d.accept)
1618 layout.addLayout(hbox, 4, 1)
1619 tab_widget.addTab(tab, _("Verify"))
1621 vbox = QVBoxLayout()
1622 vbox.addWidget(tab_widget)
1629 def question(self, msg):
1630 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1632 def show_message(self, msg):
1633 QMessageBox.information(self, _('Message'), msg, _('OK'))
1635 def password_dialog(self ):
1642 vbox = QVBoxLayout()
1643 msg = _('Please enter your password')
1644 vbox.addWidget(QLabel(msg))
1646 grid = QGridLayout()
1648 grid.addWidget(QLabel(_('Password')), 1, 0)
1649 grid.addWidget(pw, 1, 1)
1650 vbox.addLayout(grid)
1652 vbox.addLayout(ok_cancel_buttons(d))
1655 run_hook('password_dialog', pw, grid, 1)
1656 if not d.exec_(): return
1657 return unicode(pw.text())
1666 def tx_from_text(self, txt):
1667 "json or raw hexadecimal"
1670 tx = Transaction(txt)
1676 tx_dict = json.loads(str(txt))
1677 assert "hex" in tx_dict.keys()
1678 assert "complete" in tx_dict.keys()
1679 if not tx_dict["complete"]:
1680 assert "input_info" in tx_dict.keys()
1681 tx = Transaction(tx_dict["hex"])
1686 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1690 def read_tx_from_file(self):
1691 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1695 with open(fileName, "r") as f:
1696 file_content = f.read()
1697 except (ValueError, IOError, os.error), reason:
1698 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1700 return self.tx_from_text(file_content)
1704 def sign_raw_transaction(self, tx, input_info, password):
1705 self.wallet.signrawtransaction(tx, input_info, [], password)
1707 def do_process_from_text(self):
1708 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1711 tx = self.tx_from_text(text)
1713 self.show_transaction(tx)
1715 def do_process_from_file(self):
1716 tx = self.read_tx_from_file()
1718 self.show_transaction(tx)
1720 def do_process_from_csvReader(self, csvReader):
1723 for row in csvReader:
1725 amount = float(row[1])
1726 amount = int(100000000*amount)
1727 outputs.append((address, amount))
1728 except (ValueError, IOError, os.error), reason:
1729 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1733 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1734 except BaseException, e:
1735 self.show_message(str(e))
1738 self.show_transaction(tx)
1740 def do_process_from_csv_file(self):
1741 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1745 with open(fileName, "r") as f:
1746 csvReader = csv.reader(f)
1747 self.do_process_from_csvReader(csvReader)
1748 except (ValueError, IOError, os.error), reason:
1749 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1752 def do_process_from_csv_text(self):
1753 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1754 + _("Format: address, amount. One output per line"), _("Load CSV"))
1757 f = StringIO.StringIO(text)
1758 csvReader = csv.reader(f)
1759 self.do_process_from_csvReader(csvReader)
1764 def do_export_privkeys(self, password):
1765 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.")))
1768 select_export = _('Select file to export your private keys to')
1769 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1771 with open(fileName, "w+") as csvfile:
1772 transaction = csv.writer(csvfile)
1773 transaction.writerow(["address", "private_key"])
1775 addresses = self.wallet.addresses(True)
1777 for addr in addresses:
1778 pk = "".join(self.wallet.get_private_key(addr, password))
1779 transaction.writerow(["%34s"%addr,pk])
1781 self.show_message(_("Private keys exported."))
1783 except (IOError, os.error), reason:
1784 export_error_label = _("Electrum was unable to produce a private key-export.")
1785 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1787 except BaseException, e:
1788 self.show_message(str(e))
1792 def do_import_labels(self):
1793 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1794 if not labelsFile: return
1796 f = open(labelsFile, 'r')
1799 for key, value in json.loads(data).items():
1800 self.wallet.set_label(key, value)
1801 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1802 except (IOError, os.error), reason:
1803 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1806 def do_export_labels(self):
1807 labels = self.wallet.labels
1809 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1811 with open(fileName, 'w+') as f:
1812 json.dump(labels, f)
1813 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1814 except (IOError, os.error), reason:
1815 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1818 def do_export_history(self):
1819 from lite_window import csv_transaction
1820 csv_transaction(self.wallet)
1824 def do_import_privkey(self, password):
1825 if not self.wallet.imported_keys:
1826 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1827 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1828 + _('Are you sure you understand what you are doing?'), 3, 4)
1831 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1834 text = str(text).split()
1839 addr = self.wallet.import_key(key, password)
1840 except BaseException as e:
1846 addrlist.append(addr)
1848 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1850 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1851 self.update_receive_tab()
1852 self.update_history_tab()
1855 def settings_dialog(self):
1857 d.setWindowTitle(_('Electrum Settings'))
1859 vbox = QVBoxLayout()
1861 tabs = QTabWidget(self)
1862 self.settings_tab = tabs
1863 vbox.addWidget(tabs)
1866 grid_ui = QGridLayout(tab1)
1867 grid_ui.setColumnStretch(0,1)
1868 tabs.addTab(tab1, _('Display') )
1870 nz_label = QLabel(_('Display zeros'))
1871 grid_ui.addWidget(nz_label, 0, 0)
1872 nz_e = AmountEdit(None,True)
1873 nz_e.setText("%d"% self.num_zeros)
1874 grid_ui.addWidget(nz_e, 0, 1)
1875 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1876 grid_ui.addWidget(HelpButton(msg), 0, 2)
1877 if not self.config.is_modifiable('num_zeros'):
1878 for w in [nz_e, nz_label]: w.setEnabled(False)
1880 lang_label=QLabel(_('Language') + ':')
1881 grid_ui.addWidget(lang_label, 1, 0)
1882 lang_combo = QComboBox()
1883 from electrum.i18n import languages
1884 lang_combo.addItems(languages.values())
1886 index = languages.keys().index(self.config.get("language",''))
1889 lang_combo.setCurrentIndex(index)
1890 grid_ui.addWidget(lang_combo, 1, 1)
1891 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1892 if not self.config.is_modifiable('language'):
1893 for w in [lang_combo, lang_label]: w.setEnabled(False)
1895 expert_cb = QCheckBox(_('Expert mode'))
1896 expert_cb.setChecked(self.expert_mode)
1897 grid_ui.addWidget(expert_cb, 3, 0)
1898 hh = _('In expert mode, your client will:') + '\n' \
1899 + _(' - Show change addresses in the Receive tab') + '\n' \
1900 + _(' - Display the balance of each address') + '\n' \
1901 + _(' - Add freeze/prioritize actions to addresses.')
1902 grid_ui.addWidget(HelpButton(hh), 3, 2)
1903 grid_ui.setRowStretch(4,1)
1907 grid_wallet = QGridLayout(tab2)
1908 grid_wallet.setColumnStretch(0,1)
1909 tabs.addTab(tab2, _('Wallet') )
1911 fee_label = QLabel(_('Transaction fee'))
1912 grid_wallet.addWidget(fee_label, 0, 0)
1913 fee_e = AmountEdit(self.base_unit)
1914 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1915 grid_wallet.addWidget(fee_e, 0, 2)
1916 msg = _('Fee per kilobyte of transaction.') + ' ' \
1917 + _('Recommended value') + ': ' + self.format_amount(50000)
1918 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1919 if not self.config.is_modifiable('fee_per_kb'):
1920 for w in [fee_e, fee_label]: w.setEnabled(False)
1922 usechange_cb = QCheckBox(_('Use change addresses'))
1923 usechange_cb.setChecked(self.wallet.use_change)
1924 grid_wallet.addWidget(usechange_cb, 1, 0)
1925 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1926 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1928 units = ['BTC', 'mBTC']
1929 unit_label = QLabel(_('Base unit'))
1930 grid_wallet.addWidget(unit_label, 3, 0)
1931 unit_combo = QComboBox()
1932 unit_combo.addItems(units)
1933 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1934 grid_wallet.addWidget(unit_combo, 3, 2)
1935 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1936 + '\n1BTC=1000mBTC.\n' \
1937 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1938 grid_wallet.setRowStretch(4,1)
1941 run_hook('create_settings_tab', tabs)
1943 vbox.addLayout(ok_cancel_buttons(d))
1947 if not d.exec_(): return
1949 fee = unicode(fee_e.text())
1951 fee = self.read_amount(fee)
1953 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1956 self.wallet.set_fee(fee)
1958 nz = unicode(nz_e.text())
1963 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1966 if self.num_zeros != nz:
1968 self.config.set_key('num_zeros', nz, True)
1969 self.update_history_tab()
1970 self.update_receive_tab()
1972 usechange_result = usechange_cb.isChecked()
1973 if self.wallet.use_change != usechange_result:
1974 self.wallet.use_change = usechange_result
1975 self.config.set_key('use_change', self.wallet.use_change, True)
1977 unit_result = units[unit_combo.currentIndex()]
1978 if self.base_unit() != unit_result:
1979 self.decimal_point = 8 if unit_result == 'BTC' else 5
1980 self.config.set_key('decimal_point', self.decimal_point, True)
1981 self.update_history_tab()
1982 self.update_status()
1984 need_restart = False
1986 lang_request = languages.keys()[lang_combo.currentIndex()]
1987 if lang_request != self.config.get('language'):
1988 self.config.set_key("language", lang_request, True)
1992 run_hook('close_settings_dialog')
1995 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1997 self.receive_tab_set_mode(expert_cb.isChecked())
1999 def run_network_dialog(self):
2000 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2002 def closeEvent(self, event):
2004 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2005 self.save_column_widths()
2006 self.config.set_key("console-history", self.console.history[-50:], True)
2011 def plugins_dialog(self):
2012 from electrum.plugins import plugins
2015 d.setWindowTitle(_('Electrum Plugins'))
2018 vbox = QVBoxLayout(d)
2021 scroll = QScrollArea()
2022 scroll.setEnabled(True)
2023 scroll.setWidgetResizable(True)
2024 scroll.setMinimumSize(400,250)
2025 vbox.addWidget(scroll)
2029 w.setMinimumHeight(len(plugins)*35)
2031 grid = QGridLayout()
2032 grid.setColumnStretch(0,1)
2035 def mk_toggle(cb, p):
2036 return lambda: cb.setChecked(p.toggle())
2037 for i, p in enumerate(plugins):
2039 cb = QCheckBox(p.fullname())
2040 cb.setDisabled(not p.is_available())
2041 cb.setChecked(p.is_enabled())
2042 cb.clicked.connect(mk_toggle(cb,p))
2043 grid.addWidget(cb, i, 0)
2044 if p.requires_settings():
2045 grid.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2046 grid.addWidget(HelpButton(p.description()), i, 2)
2048 print_msg(_("Error: cannot display plugin"), p)
2049 traceback.print_exc(file=sys.stdout)
2050 grid.setRowStretch(i+1,1)
2052 vbox.addLayout(close_button(d))
2057 def show_account_details(self, k):
2059 d.setWindowTitle(_('Account Details'))
2062 vbox = QVBoxLayout(d)
2063 roots = self.wallet.get_roots(k)
2065 name = self.wallet.get_account_name(k)
2066 label = QLabel('Name: ' + name)
2067 vbox.addWidget(label)
2069 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2070 vbox.addWidget(QLabel('Type: ' + acctype))
2072 label = QLabel('Derivation: ' + k)
2073 vbox.addWidget(label)
2076 # mpk = self.wallet.master_public_keys[root]
2077 # text = QTextEdit()
2078 # text.setReadOnly(True)
2079 # text.setMaximumHeight(120)
2080 # text.setText(repr(mpk))
2081 # vbox.addWidget(text)
2083 vbox.addLayout(close_button(d))