3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
32 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
33 from electrum.plugins import run_hook
37 from electrum.wallet import format_satoshis
38 from electrum import Transaction
39 from electrum import mnemonic
40 from electrum import util, bitcoin, commands, Interface, Wallet
41 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
65 from electrum import ELECTRUM_VERSION
75 class StatusBarButton(QPushButton):
76 def __init__(self, icon, tooltip, func):
77 QPushButton.__init__(self, icon, '')
78 self.setToolTip(tooltip)
80 self.setMaximumWidth(25)
81 self.clicked.connect(func)
83 self.setIconSize(QSize(25,25))
85 def keyPressEvent(self, e):
86 if e.key() == QtCore.Qt.Key_Return:
98 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
100 class ElectrumWindow(QMainWindow):
101 def changeEvent(self, event):
102 flags = self.windowFlags();
103 if event and event.type() == QtCore.QEvent.WindowStateChange:
104 if self.windowState() & QtCore.Qt.WindowMinimized:
105 self.build_menu(True)
106 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
107 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
108 # Electrum from closing.
109 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
110 # self.setWindowFlags(flags & ~Qt.ToolTip)
111 elif event.oldState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(False)
113 #self.setWindowFlags(flags | Qt.ToolTip)
115 def build_menu(self, is_hidden = False):
117 if self.isMinimized():
118 m.addAction(_("Show"), self.showNormal)
120 m.addAction(_("Hide"), self.showMinimized)
123 m.addAction(_("Exit Electrum"), self.close)
124 self.tray.setContextMenu(m)
126 def tray_activated(self, reason):
127 if reason == QSystemTrayIcon.DoubleClick:
131 def __init__(self, config, network):
132 QMainWindow.__init__(self)
135 self.network = network
137 self._close_electrum = False
140 self.icon = QIcon(':icons/electrum.png')
141 self.tray = QSystemTrayIcon(self.icon, self)
142 self.tray.setToolTip('Electrum')
143 self.tray.activated.connect(self.tray_activated)
147 self.create_status_bar()
149 self.need_update = threading.Event()
151 self.decimal_point = config.get('decimal_point', 8)
152 self.num_zeros = int(config.get('num_zeros',0))
154 set_language(config.get('language'))
156 self.funds_error = False
157 self.completions = QStringListModel()
159 self.tabs = tabs = QTabWidget(self)
160 self.column_widths = self.config.get("column_widths_2", default_column_widths )
161 tabs.addTab(self.create_history_tab(), _('History') )
162 tabs.addTab(self.create_send_tab(), _('Send') )
163 tabs.addTab(self.create_receive_tab(), _('Receive') )
164 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
165 tabs.addTab(self.create_console_tab(), _('Console') )
166 tabs.setMinimumSize(600, 400)
167 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
168 self.setCentralWidget(tabs)
170 g = self.config.get("winpos-qt",[100, 100, 840, 400])
171 self.setGeometry(g[0], g[1], g[2], g[3])
175 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
176 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
177 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
178 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
179 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
181 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
182 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
183 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
185 self.history_list.setFocus(True)
188 self.network.register_callback('updated', lambda: self.need_update.set())
189 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
190 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
191 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
192 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
193 # set initial message
194 self.console.showMessage(self.network.banner)
196 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
197 if platform.system() == 'Windows':
198 n = 3 if self.wallet.seed else 2
199 tabs.setCurrentIndex (n)
200 tabs.setCurrentIndex (0)
207 self.config.set_key('lite_mode', False, True)
212 self.config.set_key('lite_mode', True, True)
219 if not self.check_qt_version():
220 if self.config.get('lite_mode') is True:
221 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
222 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
223 self.config.set_key('lite_mode', False, True)
228 actuator = lite_window.MiniActuator(self)
230 # Should probably not modify the current path but instead
231 # change the behaviour of rsrc(...)
232 old_path = QDir.currentPath()
233 actuator.load_theme()
235 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
237 driver = lite_window.MiniDriver(self, self.mini)
239 # Reset path back to original value now that loading the GUI
241 QDir.setCurrent(old_path)
243 if self.config.get('lite_mode') is True:
249 def check_qt_version(self):
250 qtVersion = qVersion()
251 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
254 def update_account_selector(self):
256 accounts = self.wallet.get_account_names()
257 self.account_selector.clear()
258 if len(accounts) > 1:
259 self.account_selector.addItems([_("All accounts")] + accounts.values())
260 self.account_selector.setCurrentIndex(0)
261 self.account_selector.show()
263 self.account_selector.hide()
266 def load_wallet(self, wallet):
269 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
270 self.current_account = self.wallet.storage.get("current_account", None)
271 self.pending_accounts = self.wallet.storage.get('pending_accounts',{})
273 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
274 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
275 self.setWindowTitle( title )
277 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
278 self.notify_transactions()
279 self.update_account_selector()
280 self.new_account.setEnabled(self.wallet.seed_version>4)
281 self.update_lock_icon()
282 self.update_buttons_on_seed()
283 self.update_console()
285 run_hook('load_wallet', wallet)
288 def select_wallet_file(self):
289 wallet_folder = self.wallet.storage.path
290 re.sub("(\/\w*.dat)$", "", wallet_folder)
291 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
295 def open_wallet(self):
297 filename = self.select_wallet_file()
301 storage = WalletStorage({'wallet_path': filename})
302 if not storage.file_exists:
303 self.show_message("file not found "+ filename)
306 self.wallet.stop_threads()
309 wallet = Wallet(storage)
310 wallet.start_threads(self.network)
312 self.load_wallet(wallet)
316 def backup_wallet(self):
318 path = self.wallet.storage.path
319 wallet_folder = os.path.dirname(path)
320 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
321 new_filename = unicode(new_filename)
322 if not ok or not new_filename:
325 new_path = os.path.join(wallet_folder, new_filename)
328 shutil.copy2(path, new_path)
329 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
330 except (IOError, os.error), reason:
331 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
334 def new_wallet(self):
337 wallet_folder = os.path.dirname(self.wallet.storage.path)
338 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
339 filename = unicode(filename)
340 if not ok or not filename:
342 filename = os.path.join(wallet_folder, filename)
344 storage = WalletStorage({'wallet_path': filename})
345 assert not storage.file_exists
347 wizard = installwizard.InstallWizard(self.config, self.network, storage)
348 wallet = wizard.run()
350 self.load_wallet(wallet)
354 def init_menubar(self):
357 file_menu = menubar.addMenu(_("&File"))
358 open_wallet_action = file_menu.addAction(_("&Open"))
359 open_wallet_action.triggered.connect(self.open_wallet)
361 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
362 new_wallet_action.triggered.connect(self.new_wallet)
364 wallet_backup = file_menu.addAction(_("&Copy"))
365 wallet_backup.triggered.connect(self.backup_wallet)
367 quit_item = file_menu.addAction(_("&Close"))
368 quit_item.triggered.connect(self.close)
370 wallet_menu = menubar.addMenu(_("&Wallet"))
372 new_contact = wallet_menu.addAction(_("&New contact"))
373 new_contact.triggered.connect(self.new_contact_dialog)
375 self.new_account = wallet_menu.addAction(_("&New account"))
376 self.new_account.triggered.connect(self.new_account_dialog)
378 wallet_menu.addSeparator()
380 pw = wallet_menu.addAction(_("&Password"))
381 pw.triggered.connect(self.change_password_dialog)
383 show_seed = wallet_menu.addAction(_("&Seed"))
384 show_seed.triggered.connect(self.show_seed_dialog)
386 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
387 show_mpk.triggered.connect(self.show_master_public_key)
389 wallet_menu.addSeparator()
391 labels_menu = wallet_menu.addMenu(_("&Labels"))
392 import_labels = labels_menu.addAction(_("&Import"))
393 import_labels.triggered.connect(self.do_import_labels)
394 export_labels = labels_menu.addAction(_("&Export"))
395 export_labels.triggered.connect(self.do_export_labels)
397 keys_menu = wallet_menu.addMenu(_("&Private keys"))
398 import_keys = keys_menu.addAction(_("&Import"))
399 import_keys.triggered.connect(self.do_import_privkey)
400 export_keys = keys_menu.addAction(_("&Export"))
401 export_keys.triggered.connect(self.do_export_privkeys)
403 ex_history = wallet_menu.addAction(_("&Export History"))
404 ex_history.triggered.connect(self.do_export_history)
408 tools_menu = menubar.addMenu(_("&Tools"))
410 # Settings / Preferences are all reserved keywords in OSX using this as work around
411 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
412 preferences_menu = tools_menu.addAction(preferences_name)
413 preferences_menu.triggered.connect(self.settings_dialog)
415 network = tools_menu.addAction(_("&Network"))
416 network.triggered.connect(self.run_network_dialog)
418 plugins_labels = tools_menu.addAction(_("&Plugins"))
419 plugins_labels.triggered.connect(self.plugins_dialog)
421 tools_menu.addSeparator()
423 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
425 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
426 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
428 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
429 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
431 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
433 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
434 raw_transaction_file.triggered.connect(self.do_process_from_file)
436 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
437 raw_transaction_text.triggered.connect(self.do_process_from_text)
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 if not self.network.is_connected():
467 print_error("Notifying GUI")
468 if len(self.network.interface.pending_transactions_for_notifications) > 0:
469 # Combine the transactions if there are more then three
470 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
473 for tx in self.network.interface.pending_transactions_for_notifications:
474 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
478 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
479 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
481 self.network.interface.pending_transactions_for_notifications = []
483 for tx in self.network.interface.pending_transactions_for_notifications:
485 self.network.interface.pending_transactions_for_notifications.remove(tx)
486 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
488 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
490 def notify(self, message):
491 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
495 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
496 def getOpenFileName(self, title, filter = ""):
497 directory = self.config.get('io_dir', os.path.expanduser('~'))
498 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
499 if fileName and directory != os.path.dirname(fileName):
500 self.config.set_key('io_dir', os.path.dirname(fileName), True)
503 def getSaveFileName(self, title, filename, filter = ""):
504 directory = self.config.get('io_dir', os.path.expanduser('~'))
505 path = os.path.join( directory, filename )
506 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
507 if fileName and directory != os.path.dirname(fileName):
508 self.config.set_key('io_dir', os.path.dirname(fileName), True)
512 QMainWindow.close(self)
513 run_hook('close_main_window')
515 def connect_slots(self, sender):
516 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
517 self.previous_payto_e=''
519 def timer_actions(self):
520 if self.need_update.is_set():
522 self.need_update.clear()
523 run_hook('timer_actions')
525 def format_amount(self, x, is_diff=False, whitespaces=False):
526 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
528 def read_amount(self, x):
529 if x in['.', '']: return None
530 p = pow(10, self.decimal_point)
531 return int( p * Decimal(x) )
534 assert self.decimal_point in [5,8]
535 return "BTC" if self.decimal_point == 8 else "mBTC"
538 def update_status(self):
539 if self.network.is_connected():
540 if not self.wallet.up_to_date:
541 text = _("Synchronizing...")
542 icon = QIcon(":icons/status_waiting.png")
543 elif self.network.server_lag > 1:
544 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
545 icon = QIcon(":icons/status_lagging.png")
547 c, u = self.wallet.get_account_balance(self.current_account)
548 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
549 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
552 run_hook('set_quote_text', c+u, r)
555 text += " (%s)"%quote
557 self.tray.setToolTip(text)
558 icon = QIcon(":icons/status_connected.png")
560 text = _("Not connected")
561 icon = QIcon(":icons/status_disconnected.png")
563 self.balance_label.setText(text)
564 self.status_button.setIcon( icon )
567 def update_wallet(self):
569 if self.wallet.up_to_date or not self.network.is_connected():
570 self.update_history_tab()
571 self.update_receive_tab()
572 self.update_contacts_tab()
573 self.update_completions()
576 def create_history_tab(self):
577 self.history_list = l = MyTreeWidget(self)
579 for i,width in enumerate(self.column_widths['history']):
580 l.setColumnWidth(i, width)
581 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
582 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
583 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
585 l.customContextMenuRequested.connect(self.create_history_menu)
589 def create_history_menu(self, position):
590 self.history_list.selectedIndexes()
591 item = self.history_list.currentItem()
593 tx_hash = str(item.data(0, Qt.UserRole).toString())
594 if not tx_hash: return
596 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
597 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
598 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
599 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
602 def show_transaction(self, tx):
603 import transaction_dialog
604 d = transaction_dialog.TxDialog(tx, self)
607 def tx_label_clicked(self, item, column):
608 if column==2 and item.isSelected():
610 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
611 self.history_list.editItem( item, column )
612 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
615 def tx_label_changed(self, item, column):
619 tx_hash = str(item.data(0, Qt.UserRole).toString())
620 tx = self.wallet.transactions.get(tx_hash)
621 text = unicode( item.text(2) )
622 self.wallet.set_label(tx_hash, text)
624 item.setForeground(2, QBrush(QColor('black')))
626 text = self.wallet.get_default_label(tx_hash)
627 item.setText(2, text)
628 item.setForeground(2, QBrush(QColor('gray')))
632 def edit_label(self, is_recv):
633 l = self.receive_list if is_recv else self.contacts_list
634 item = l.currentItem()
635 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
636 l.editItem( item, 1 )
637 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
641 def address_label_clicked(self, item, column, l, column_addr, column_label):
642 if column == column_label and item.isSelected():
643 is_editable = item.data(0, 32).toBool()
646 addr = unicode( item.text(column_addr) )
647 label = unicode( item.text(column_label) )
648 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
649 l.editItem( item, column )
650 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
653 def address_label_changed(self, item, column, l, column_addr, column_label):
654 if column == column_label:
655 addr = unicode( item.text(column_addr) )
656 text = unicode( item.text(column_label) )
657 is_editable = item.data(0, 32).toBool()
661 changed = self.wallet.set_label(addr, text)
663 self.update_history_tab()
664 self.update_completions()
666 self.current_item_changed(item)
668 run_hook('item_changed', item, column)
671 def current_item_changed(self, a):
672 run_hook('current_item_changed', a)
676 def update_history_tab(self):
678 self.history_list.clear()
679 for item in self.wallet.get_tx_history(self.current_account):
680 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
683 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
685 time_str = _("unknown")
688 time_str = 'unverified'
689 icon = QIcon(":icons/unconfirmed.png")
692 icon = QIcon(":icons/unconfirmed.png")
694 icon = QIcon(":icons/clock%d.png"%conf)
696 icon = QIcon(":icons/confirmed.png")
698 if value is not None:
699 v_str = self.format_amount(value, True, whitespaces=True)
703 balance_str = self.format_amount(balance, whitespaces=True)
706 label, is_default_label = self.wallet.get_label(tx_hash)
708 label = _('Pruned transaction outputs')
709 is_default_label = False
711 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
712 item.setFont(2, QFont(MONOSPACE_FONT))
713 item.setFont(3, QFont(MONOSPACE_FONT))
714 item.setFont(4, QFont(MONOSPACE_FONT))
716 item.setForeground(3, QBrush(QColor("#BC1E1E")))
718 item.setData(0, Qt.UserRole, tx_hash)
719 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
721 item.setForeground(2, QBrush(QColor('grey')))
723 item.setIcon(0, icon)
724 self.history_list.insertTopLevelItem(0,item)
727 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
730 def create_send_tab(self):
735 grid.setColumnMinimumWidth(3,300)
736 grid.setColumnStretch(5,1)
739 self.payto_e = QLineEdit()
740 grid.addWidget(QLabel(_('Pay to')), 1, 0)
741 grid.addWidget(self.payto_e, 1, 1, 1, 3)
743 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)
745 completer = QCompleter()
746 completer.setCaseSensitivity(False)
747 self.payto_e.setCompleter(completer)
748 completer.setModel(self.completions)
750 self.message_e = QLineEdit()
751 grid.addWidget(QLabel(_('Description')), 2, 0)
752 grid.addWidget(self.message_e, 2, 1, 1, 3)
753 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)
755 self.amount_e = AmountEdit(self.base_unit)
756 grid.addWidget(QLabel(_('Amount')), 3, 0)
757 grid.addWidget(self.amount_e, 3, 1, 1, 2)
758 grid.addWidget(HelpButton(
759 _('Amount to be sent.') + '\n\n' \
760 + _('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.') \
761 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
763 self.fee_e = AmountEdit(self.base_unit)
764 grid.addWidget(QLabel(_('Fee')), 4, 0)
765 grid.addWidget(self.fee_e, 4, 1, 1, 2)
766 grid.addWidget(HelpButton(
767 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
768 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
769 + _('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)
772 self.send_button = EnterButton(_("Send"), self.do_send)
773 grid.addWidget(self.send_button, 6, 1)
775 b = EnterButton(_("Clear"),self.do_clear)
776 grid.addWidget(b, 6, 2)
778 self.payto_sig = QLabel('')
779 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
781 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
782 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
791 def entry_changed( is_fee ):
792 self.funds_error = False
794 if self.amount_e.is_shortcut:
795 self.amount_e.is_shortcut = False
796 c, u = self.wallet.get_account_balance(self.current_account)
797 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
798 fee = self.wallet.estimated_fee(inputs)
800 self.amount_e.setText( self.format_amount(amount) )
801 self.fee_e.setText( self.format_amount( fee ) )
804 amount = self.read_amount(str(self.amount_e.text()))
805 fee = self.read_amount(str(self.fee_e.text()))
807 if not is_fee: fee = None
810 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
812 self.fee_e.setText( self.format_amount( fee ) )
815 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
819 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
820 self.funds_error = True
821 text = _( "Not enough funds" )
822 c, u = self.wallet.get_frozen_balance()
823 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
825 self.statusBar().showMessage(text)
826 self.amount_e.setPalette(palette)
827 self.fee_e.setPalette(palette)
829 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
830 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
832 run_hook('create_send_tab', grid)
836 def update_completions(self):
838 for addr,label in self.wallet.labels.items():
839 if addr in self.wallet.addressbook:
840 l.append( label + ' <' + addr + '>')
842 run_hook('update_completions', l)
843 self.completions.setStringList(l)
847 return lambda s, *args: s.do_protect(func, args)
852 label = unicode( self.message_e.text() )
853 r = unicode( self.payto_e.text() )
856 # label or alias, with address in brackets
857 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
858 to_address = m.group(2) if m else r
860 if not is_valid(to_address):
861 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
865 amount = self.read_amount(unicode( self.amount_e.text()))
867 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
870 fee = self.read_amount(unicode( self.fee_e.text()))
872 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
875 confirm_amount = self.config.get('confirm_amount', 100000000)
876 if amount >= confirm_amount:
877 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
880 self.send_tx(to_address, amount, fee, label)
884 def send_tx(self, to_address, amount, fee, label, password):
887 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
888 except BaseException, e:
889 traceback.print_exc(file=sys.stdout)
890 self.show_message(str(e))
893 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
894 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
898 self.wallet.set_label(tx.hash(), label)
901 h = self.wallet.send_tx(tx)
902 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
903 status, msg = self.wallet.receive_tx( h )
905 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
907 self.update_contacts_tab()
909 QMessageBox.warning(self, _('Error'), msg, _('OK'))
911 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
913 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
914 with open(fileName,'w') as f:
915 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
916 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
918 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
920 # add recipient to addressbook
921 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
922 self.wallet.addressbook.append(to_address)
927 def set_url(self, url):
928 address, amount, label, message, signature, identity, url = util.parse_url(url)
930 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
933 self.mini.set_payment_fields(address, amount)
935 if label and self.wallet.labels.get(address) != label:
936 if self.question('Give label "%s" to address %s ?'%(label,address)):
937 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
938 self.wallet.addressbook.append(address)
939 self.wallet.set_label(address, label)
941 run_hook('set_url', url, self.show_message, self.question)
943 self.tabs.setCurrentIndex(1)
944 label = self.wallet.labels.get(address)
945 m_addr = label + ' <'+ address +'>' if label else address
946 self.payto_e.setText(m_addr)
948 self.message_e.setText(message)
950 self.amount_e.setText(amount)
953 self.set_frozen(self.payto_e,True)
954 self.set_frozen(self.amount_e,True)
955 self.set_frozen(self.message_e,True)
956 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
958 self.payto_sig.setVisible(False)
961 self.payto_sig.setVisible(False)
962 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
964 self.set_frozen(e,False)
967 def set_frozen(self,entry,frozen):
969 entry.setReadOnly(True)
970 entry.setFrame(False)
972 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
973 entry.setPalette(palette)
975 entry.setReadOnly(False)
978 palette.setColor(entry.backgroundRole(), QColor('white'))
979 entry.setPalette(palette)
982 def toggle_freeze(self,addr):
984 if addr in self.wallet.frozen_addresses:
985 self.wallet.unfreeze(addr)
987 self.wallet.freeze(addr)
988 self.update_receive_tab()
990 def toggle_priority(self,addr):
992 if addr in self.wallet.prioritized_addresses:
993 self.wallet.unprioritize(addr)
995 self.wallet.prioritize(addr)
996 self.update_receive_tab()
999 def create_list_tab(self, headers):
1000 "generic tab creation method"
1001 l = MyTreeWidget(self)
1002 l.setColumnCount( len(headers) )
1003 l.setHeaderLabels( headers )
1006 vbox = QVBoxLayout()
1013 vbox.addWidget(buttons)
1015 hbox = QHBoxLayout()
1018 buttons.setLayout(hbox)
1023 def create_receive_tab(self):
1024 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1025 l.setContextMenuPolicy(Qt.CustomContextMenu)
1026 l.customContextMenuRequested.connect(self.create_receive_menu)
1027 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1028 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1029 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1030 self.receive_list = l
1031 self.receive_buttons_hbox = hbox
1038 def save_column_widths(self):
1039 self.column_widths["receive"] = []
1040 for i in range(self.receive_list.columnCount() -1):
1041 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1043 self.column_widths["history"] = []
1044 for i in range(self.history_list.columnCount() - 1):
1045 self.column_widths["history"].append(self.history_list.columnWidth(i))
1047 self.column_widths["contacts"] = []
1048 for i in range(self.contacts_list.columnCount() - 1):
1049 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1051 self.config.set_key("column_widths_2", self.column_widths, True)
1054 def create_contacts_tab(self):
1055 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1056 l.setContextMenuPolicy(Qt.CustomContextMenu)
1057 l.customContextMenuRequested.connect(self.create_contact_menu)
1058 for i,width in enumerate(self.column_widths['contacts']):
1059 l.setColumnWidth(i, width)
1061 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1062 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1063 self.contacts_list = l
1064 self.contacts_buttons_hbox = hbox
1069 def delete_imported_key(self, addr):
1070 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1071 self.wallet.delete_imported_key(addr)
1072 self.update_receive_tab()
1073 self.update_history_tab()
1075 def edit_account_label(self, k):
1076 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1078 label = unicode(text)
1079 self.wallet.set_label(k,label)
1080 self.update_receive_tab()
1082 def account_set_expanded(self, item, k, b):
1084 self.accounts_expanded[k] = b
1086 def create_account_menu(self, position, k, item):
1088 if item.isExpanded():
1089 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1091 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1092 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1093 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1094 if k in self.pending_accounts:
1095 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1096 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1098 def delete_pending_account(self, k):
1099 self.pending_accounts.pop(k)
1100 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1101 self.update_receive_tab()
1103 def create_receive_menu(self, position):
1104 # fixme: this function apparently has a side effect.
1105 # if it is not called the menu pops up several times
1106 #self.receive_list.selectedIndexes()
1108 item = self.receive_list.itemAt(position)
1111 addr = unicode(item.text(0))
1112 if not is_valid(addr):
1113 k = str(item.data(0,32).toString())
1115 self.create_account_menu(position, k, item)
1117 item.setExpanded(not item.isExpanded())
1121 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1122 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1123 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1124 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1125 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1126 if addr in self.wallet.imported_keys:
1127 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1129 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1130 menu.addAction(t, lambda: self.toggle_freeze(addr))
1131 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1132 menu.addAction(t, lambda: self.toggle_priority(addr))
1134 run_hook('receive_menu', menu)
1135 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1138 def payto(self, addr):
1140 label = self.wallet.labels.get(addr)
1141 m_addr = label + ' <' + addr + '>' if label else addr
1142 self.tabs.setCurrentIndex(1)
1143 self.payto_e.setText(m_addr)
1144 self.amount_e.setFocus()
1147 def delete_contact(self, x):
1148 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1149 self.wallet.delete_contact(x)
1150 self.wallet.set_label(x, None)
1151 self.update_history_tab()
1152 self.update_contacts_tab()
1153 self.update_completions()
1156 def create_contact_menu(self, position):
1157 item = self.contacts_list.itemAt(position)
1159 addr = unicode(item.text(0))
1160 label = unicode(item.text(1))
1161 is_editable = item.data(0,32).toBool()
1162 payto_addr = item.data(0,33).toString()
1164 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1165 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1166 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1168 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1169 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1171 run_hook('create_contact_menu', menu, item)
1172 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1175 def update_receive_item(self, item):
1176 item.setFont(0, QFont(MONOSPACE_FONT))
1177 address = str(item.data(0,0).toString())
1178 label = self.wallet.labels.get(address,'')
1179 item.setData(1,0,label)
1180 item.setData(0,32, True) # is editable
1182 run_hook('update_receive_item', address, item)
1184 if not self.wallet.is_mine(address): return
1186 c, u = self.wallet.get_addr_balance(address)
1187 balance = self.format_amount(c + u)
1188 item.setData(2,0,balance)
1190 if address in self.wallet.frozen_addresses:
1191 item.setBackgroundColor(0, QColor('lightblue'))
1192 elif address in self.wallet.prioritized_addresses:
1193 item.setBackgroundColor(0, QColor('lightgreen'))
1196 def update_receive_tab(self):
1197 l = self.receive_list
1200 l.setColumnHidden(2, False)
1201 l.setColumnHidden(3, False)
1202 for i,width in enumerate(self.column_widths['receive']):
1203 l.setColumnWidth(i, width)
1205 if self.current_account is None:
1206 account_items = self.wallet.accounts.items()
1207 elif self.current_account != -1:
1208 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1212 for k, account in account_items:
1213 name = self.wallet.get_account_name(k)
1214 c,u = self.wallet.get_account_balance(k)
1215 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1216 l.addTopLevelItem(account_item)
1217 account_item.setExpanded(self.accounts_expanded.get(k, True))
1218 account_item.setData(0, 32, k)
1220 if not self.wallet.is_seeded(k):
1221 icon = QIcon(":icons/key.png")
1222 account_item.setIcon(0, icon)
1224 for is_change in ([0,1]):
1225 name = _("Receiving") if not is_change else _("Change")
1226 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1227 account_item.addChild(seq_item)
1228 if not is_change: seq_item.setExpanded(True)
1233 for address in account.get_addresses(is_change):
1234 h = self.wallet.history.get(address,[])
1238 if gap > self.wallet.gap_limit:
1243 num_tx = '*' if h == ['*'] else "%d"%len(h)
1244 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1245 self.update_receive_item(item)
1247 item.setBackgroundColor(1, QColor('red'))
1248 seq_item.addChild(item)
1251 for k, addr in self.pending_accounts.items():
1252 if k in self.wallet.accounts:
1253 self.pending_accounts.pop(k)
1254 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1255 name = self.wallet.labels.get(k,'')
1256 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1257 self.update_receive_item(item)
1258 l.addTopLevelItem(account_item)
1259 account_item.setExpanded(True)
1260 account_item.setData(0, 32, k)
1261 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1262 account_item.addChild(item)
1263 self.update_receive_item(item)
1266 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1267 c,u = self.wallet.get_imported_balance()
1268 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1269 l.addTopLevelItem(account_item)
1270 account_item.setExpanded(True)
1271 for address in self.wallet.imported_keys.keys():
1272 item = QTreeWidgetItem( [ address, '', '', ''] )
1273 self.update_receive_item(item)
1274 account_item.addChild(item)
1277 # we use column 1 because column 0 may be hidden
1278 l.setCurrentItem(l.topLevelItem(0),1)
1281 def update_contacts_tab(self):
1282 l = self.contacts_list
1285 for address in self.wallet.addressbook:
1286 label = self.wallet.labels.get(address,'')
1287 n = self.wallet.get_num_tx(address)
1288 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1289 item.setFont(0, QFont(MONOSPACE_FONT))
1290 # 32 = label can be edited (bool)
1291 item.setData(0,32, True)
1293 item.setData(0,33, address)
1294 l.addTopLevelItem(item)
1296 run_hook('update_contacts_tab', l)
1297 l.setCurrentItem(l.topLevelItem(0))
1301 def create_console_tab(self):
1302 from console import Console
1303 self.console = console = Console()
1307 def update_console(self):
1308 console = self.console
1309 console.history = self.config.get("console-history",[])
1310 console.history_index = len(console.history)
1312 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1313 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1315 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1317 def mkfunc(f, method):
1318 return lambda *args: apply( f, (method, args, self.password_dialog ))
1320 if m[0]=='_' or m in ['network','wallet']: continue
1321 methods[m] = mkfunc(c._run, m)
1323 console.updateNamespace(methods)
1326 def change_account(self,s):
1327 if s == _("All accounts"):
1328 self.current_account = None
1330 accounts = self.wallet.get_account_names()
1331 for k, v in accounts.items():
1333 self.current_account = k
1334 self.update_history_tab()
1335 self.update_status()
1336 self.update_receive_tab()
1338 def create_status_bar(self):
1341 sb.setFixedHeight(35)
1342 qtVersion = qVersion()
1344 self.balance_label = QLabel("")
1345 sb.addWidget(self.balance_label)
1347 from version_getter import UpdateLabel
1348 self.updatelabel = UpdateLabel(self.config, sb)
1350 self.account_selector = QComboBox()
1351 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1352 sb.addPermanentWidget(self.account_selector)
1354 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1355 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1357 self.lock_icon = QIcon()
1358 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1359 sb.addPermanentWidget( self.password_button )
1361 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1362 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1363 sb.addPermanentWidget( self.seed_button )
1364 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1365 sb.addPermanentWidget( self.status_button )
1367 run_hook('create_status_bar', (sb,))
1369 self.setStatusBar(sb)
1372 def update_lock_icon(self):
1373 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1374 self.password_button.setIcon( icon )
1377 def update_buttons_on_seed(self):
1378 if not self.wallet.is_watching_only():
1379 self.seed_button.show()
1380 self.password_button.show()
1381 self.send_button.setText(_("Send"))
1383 self.password_button.hide()
1384 self.seed_button.hide()
1385 self.send_button.setText(_("Create unsigned transaction"))
1388 def change_password_dialog(self):
1389 from password_dialog import PasswordDialog
1390 d = PasswordDialog(self.wallet, self)
1392 self.update_lock_icon()
1395 def new_contact_dialog(self):
1398 vbox = QVBoxLayout(d)
1399 vbox.addWidget(QLabel(_('New Contact')+':'))
1401 grid = QGridLayout()
1404 grid.addWidget(QLabel(_("Address")), 1, 0)
1405 grid.addWidget(line1, 1, 1)
1406 grid.addWidget(QLabel(_("Name")), 2, 0)
1407 grid.addWidget(line2, 2, 1)
1409 vbox.addLayout(grid)
1410 vbox.addLayout(ok_cancel_buttons(d))
1415 address = str(line1.text())
1416 label = unicode(line2.text())
1418 if not is_valid(address):
1419 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1422 self.wallet.add_contact(address)
1424 self.wallet.set_label(address, label)
1426 self.update_contacts_tab()
1427 self.update_history_tab()
1428 self.update_completions()
1429 self.tabs.setCurrentIndex(3)
1432 def new_account_dialog(self):
1434 dialog = QDialog(self)
1436 dialog.setWindowTitle(_("New Account"))
1438 vbox = QVBoxLayout()
1439 vbox.addWidget(QLabel(_('Account name')+':'))
1442 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1443 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1448 vbox.addLayout(ok_cancel_buttons(dialog))
1449 dialog.setLayout(vbox)
1453 name = str(e.text())
1456 k, addr = self.wallet.new_account_address()
1457 self.wallet.set_label(k, name)
1458 self.pending_accounts[k] = addr
1459 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1460 self.update_receive_tab()
1461 self.tabs.setCurrentIndex(2)
1465 def show_master_public_key_old(self):
1466 dialog = QDialog(self)
1468 dialog.setWindowTitle(_("Master Public Key"))
1470 main_text = QTextEdit()
1471 main_text.setText(self.wallet.get_master_public_key())
1472 main_text.setReadOnly(True)
1473 main_text.setMaximumHeight(170)
1474 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1476 ok_button = QPushButton(_("OK"))
1477 ok_button.setDefault(True)
1478 ok_button.clicked.connect(dialog.accept)
1480 main_layout = QGridLayout()
1481 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1483 main_layout.addWidget(main_text, 1, 0)
1484 main_layout.addWidget(qrw, 1, 1 )
1486 vbox.addLayout(close_button(dialog))
1487 dialog.setLayout(vbox)
1491 def show_master_public_key(self):
1493 if self.wallet.seed_version == 4:
1494 self.show_master_public_keys_old()
1497 dialog = QDialog(self)
1499 dialog.setWindowTitle(_("Master Public Keys"))
1501 chain_text = QTextEdit()
1502 chain_text.setReadOnly(True)
1503 chain_text.setMaximumHeight(170)
1504 chain_qrw = QRCodeWidget()
1506 mpk_text = QTextEdit()
1507 mpk_text.setReadOnly(True)
1508 mpk_text.setMaximumHeight(170)
1509 mpk_qrw = QRCodeWidget()
1511 main_layout = QGridLayout()
1513 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1514 main_layout.addWidget(mpk_text, 1, 1)
1515 main_layout.addWidget(mpk_qrw, 1, 2)
1517 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1518 main_layout.addWidget(chain_text, 2, 1)
1519 main_layout.addWidget(chain_qrw, 2, 2)
1522 c, K, cK = self.wallet.master_public_keys[str(key)]
1523 chain_text.setText(c)
1524 chain_qrw.set_addr(c)
1525 chain_qrw.update_qr()
1530 key_selector = QComboBox()
1531 keys = sorted(self.wallet.master_public_keys.keys())
1532 key_selector.addItems(keys)
1534 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1535 main_layout.addWidget(key_selector, 0, 1)
1536 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1540 vbox = QVBoxLayout()
1541 vbox.addLayout(main_layout)
1542 vbox.addLayout(close_button(dialog))
1544 dialog.setLayout(vbox)
1549 def show_seed_dialog(self, password):
1550 if self.wallet.is_watching_only():
1551 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1554 if self.wallet.seed:
1556 seed = self.wallet.decode_seed(password)
1558 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1560 from seed_dialog import SeedDialog
1561 d = SeedDialog(self, seed, self.wallet.imported_keys)
1565 for k in self.wallet.master_private_keys.keys():
1566 pk = self.wallet.get_master_private_key(k, password)
1568 from seed_dialog import PrivateKeysDialog
1569 d = PrivateKeysDialog(self,l)
1576 def show_qrcode(self, data, title = _("QR code")):
1580 d.setWindowTitle(title)
1581 d.setMinimumSize(270, 300)
1582 vbox = QVBoxLayout()
1583 qrw = QRCodeWidget(data)
1584 vbox.addWidget(qrw, 1)
1585 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1586 hbox = QHBoxLayout()
1590 filename = "qrcode.bmp"
1591 bmp.save_qrcode(qrw.qr, filename)
1592 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1594 b = QPushButton(_("Save"))
1596 b.clicked.connect(print_qr)
1598 b = QPushButton(_("Close"))
1600 b.clicked.connect(d.accept)
1603 vbox.addLayout(hbox)
1608 def do_protect(self, func, args):
1609 if self.wallet.use_encryption:
1610 password = self.password_dialog()
1616 if args != (False,):
1617 args = (self,) + args + (password,)
1619 args = (self,password)
1624 def show_private_key(self, address, password):
1625 if not address: return
1627 pk_list = self.wallet.get_private_key(address, password)
1628 except BaseException, e:
1629 self.show_message(str(e))
1631 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1635 def do_sign(self, address, message, signature, password):
1636 message = unicode(message.toPlainText())
1637 message = message.encode('utf-8')
1639 sig = self.wallet.sign_message(str(address.text()), message, password)
1640 signature.setText(sig)
1641 except BaseException, e:
1642 self.show_message(str(e))
1644 def sign_message(self, address):
1645 if not address: return
1648 d.setWindowTitle(_('Sign Message'))
1649 d.setMinimumSize(410, 290)
1651 tab_widget = QTabWidget()
1653 layout = QGridLayout(tab)
1655 sign_address = QLineEdit()
1657 sign_address.setText(address)
1658 layout.addWidget(QLabel(_('Address')), 1, 0)
1659 layout.addWidget(sign_address, 1, 1)
1661 sign_message = QTextEdit()
1662 layout.addWidget(QLabel(_('Message')), 2, 0)
1663 layout.addWidget(sign_message, 2, 1)
1664 layout.setRowStretch(2,3)
1666 sign_signature = QTextEdit()
1667 layout.addWidget(QLabel(_('Signature')), 3, 0)
1668 layout.addWidget(sign_signature, 3, 1)
1669 layout.setRowStretch(3,1)
1672 hbox = QHBoxLayout()
1673 b = QPushButton(_("Sign"))
1675 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1676 b = QPushButton(_("Close"))
1677 b.clicked.connect(d.accept)
1679 layout.addLayout(hbox, 4, 1)
1680 tab_widget.addTab(tab, _("Sign"))
1684 layout = QGridLayout(tab)
1686 verify_address = QLineEdit()
1687 layout.addWidget(QLabel(_('Address')), 1, 0)
1688 layout.addWidget(verify_address, 1, 1)
1690 verify_message = QTextEdit()
1691 layout.addWidget(QLabel(_('Message')), 2, 0)
1692 layout.addWidget(verify_message, 2, 1)
1693 layout.setRowStretch(2,3)
1695 verify_signature = QTextEdit()
1696 layout.addWidget(QLabel(_('Signature')), 3, 0)
1697 layout.addWidget(verify_signature, 3, 1)
1698 layout.setRowStretch(3,1)
1701 message = unicode(verify_message.toPlainText())
1702 message = message.encode('utf-8')
1703 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1704 self.show_message(_("Signature verified"))
1706 self.show_message(_("Error: wrong signature"))
1708 hbox = QHBoxLayout()
1709 b = QPushButton(_("Verify"))
1710 b.clicked.connect(do_verify)
1712 b = QPushButton(_("Close"))
1713 b.clicked.connect(d.accept)
1715 layout.addLayout(hbox, 4, 1)
1716 tab_widget.addTab(tab, _("Verify"))
1718 vbox = QVBoxLayout()
1719 vbox.addWidget(tab_widget)
1726 def question(self, msg):
1727 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1729 def show_message(self, msg):
1730 QMessageBox.information(self, _('Message'), msg, _('OK'))
1732 def password_dialog(self ):
1739 vbox = QVBoxLayout()
1740 msg = _('Please enter your password')
1741 vbox.addWidget(QLabel(msg))
1743 grid = QGridLayout()
1745 grid.addWidget(QLabel(_('Password')), 1, 0)
1746 grid.addWidget(pw, 1, 1)
1747 vbox.addLayout(grid)
1749 vbox.addLayout(ok_cancel_buttons(d))
1752 run_hook('password_dialog', pw, grid, 1)
1753 if not d.exec_(): return
1754 return unicode(pw.text())
1763 def tx_from_text(self, txt):
1764 "json or raw hexadecimal"
1767 tx = Transaction(txt)
1773 tx_dict = json.loads(str(txt))
1774 assert "hex" in tx_dict.keys()
1775 assert "complete" in tx_dict.keys()
1776 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1777 if not tx_dict["complete"]:
1778 assert "input_info" in tx_dict.keys()
1779 input_info = json.loads(tx_dict['input_info'])
1780 tx.add_input_info(input_info)
1785 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1789 def read_tx_from_file(self):
1790 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1794 with open(fileName, "r") as f:
1795 file_content = f.read()
1796 except (ValueError, IOError, os.error), reason:
1797 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1799 return self.tx_from_text(file_content)
1803 def sign_raw_transaction(self, tx, input_info, password):
1804 self.wallet.signrawtransaction(tx, input_info, [], password)
1806 def do_process_from_text(self):
1807 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1810 tx = self.tx_from_text(text)
1812 self.show_transaction(tx)
1814 def do_process_from_file(self):
1815 tx = self.read_tx_from_file()
1817 self.show_transaction(tx)
1819 def do_process_from_csvReader(self, csvReader):
1822 for row in csvReader:
1824 amount = float(row[1])
1825 amount = int(100000000*amount)
1826 outputs.append((address, amount))
1827 except (ValueError, IOError, os.error), reason:
1828 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1832 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1833 except BaseException, e:
1834 self.show_message(str(e))
1837 self.show_transaction(tx)
1839 def do_process_from_csv_file(self):
1840 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1844 with open(fileName, "r") as f:
1845 csvReader = csv.reader(f)
1846 self.do_process_from_csvReader(csvReader)
1847 except (ValueError, IOError, os.error), reason:
1848 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1851 def do_process_from_csv_text(self):
1852 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1853 + _("Format: address, amount. One output per line"), _("Load CSV"))
1856 f = StringIO.StringIO(text)
1857 csvReader = csv.reader(f)
1858 self.do_process_from_csvReader(csvReader)
1863 def do_export_privkeys(self, password):
1864 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.")))
1867 select_export = _('Select file to export your private keys to')
1868 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1870 with open(fileName, "w+") as csvfile:
1871 transaction = csv.writer(csvfile)
1872 transaction.writerow(["address", "private_key"])
1874 addresses = self.wallet.addresses(True)
1876 for addr in addresses:
1877 pk = "".join(self.wallet.get_private_key(addr, password))
1878 transaction.writerow(["%34s"%addr,pk])
1880 self.show_message(_("Private keys exported."))
1882 except (IOError, os.error), reason:
1883 export_error_label = _("Electrum was unable to produce a private key-export.")
1884 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1886 except BaseException, e:
1887 self.show_message(str(e))
1891 def do_import_labels(self):
1892 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1893 if not labelsFile: return
1895 f = open(labelsFile, 'r')
1898 for key, value in json.loads(data).items():
1899 self.wallet.set_label(key, value)
1900 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1901 except (IOError, os.error), reason:
1902 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1905 def do_export_labels(self):
1906 labels = self.wallet.labels
1908 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1910 with open(fileName, 'w+') as f:
1911 json.dump(labels, f)
1912 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1913 except (IOError, os.error), reason:
1914 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1917 def do_export_history(self):
1918 from lite_window import csv_transaction
1919 csv_transaction(self.wallet)
1923 def do_import_privkey(self, password):
1924 if not self.wallet.imported_keys:
1925 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1926 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1927 + _('Are you sure you understand what you are doing?'), 3, 4)
1930 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1933 text = str(text).split()
1938 addr = self.wallet.import_key(key, password)
1939 except BaseException as e:
1945 addrlist.append(addr)
1947 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1949 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1950 self.update_receive_tab()
1951 self.update_history_tab()
1954 def settings_dialog(self):
1956 d.setWindowTitle(_('Electrum Settings'))
1958 vbox = QVBoxLayout()
1959 grid = QGridLayout()
1960 grid.setColumnStretch(0,1)
1962 nz_label = QLabel(_('Display zeros') + ':')
1963 grid.addWidget(nz_label, 0, 0)
1964 nz_e = AmountEdit(None,True)
1965 nz_e.setText("%d"% self.num_zeros)
1966 grid.addWidget(nz_e, 0, 1)
1967 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1968 grid.addWidget(HelpButton(msg), 0, 2)
1969 if not self.config.is_modifiable('num_zeros'):
1970 for w in [nz_e, nz_label]: w.setEnabled(False)
1972 lang_label=QLabel(_('Language') + ':')
1973 grid.addWidget(lang_label, 1, 0)
1974 lang_combo = QComboBox()
1975 from electrum.i18n import languages
1976 lang_combo.addItems(languages.values())
1978 index = languages.keys().index(self.config.get("language",''))
1981 lang_combo.setCurrentIndex(index)
1982 grid.addWidget(lang_combo, 1, 1)
1983 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1984 if not self.config.is_modifiable('language'):
1985 for w in [lang_combo, lang_label]: w.setEnabled(False)
1988 fee_label = QLabel(_('Transaction fee') + ':')
1989 grid.addWidget(fee_label, 2, 0)
1990 fee_e = AmountEdit(self.base_unit)
1991 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1992 grid.addWidget(fee_e, 2, 1)
1993 msg = _('Fee per kilobyte of transaction.') + ' ' \
1994 + _('Recommended value') + ': ' + self.format_amount(50000)
1995 grid.addWidget(HelpButton(msg), 2, 2)
1996 if not self.config.is_modifiable('fee_per_kb'):
1997 for w in [fee_e, fee_label]: w.setEnabled(False)
1999 units = ['BTC', 'mBTC']
2000 unit_label = QLabel(_('Base unit') + ':')
2001 grid.addWidget(unit_label, 3, 0)
2002 unit_combo = QComboBox()
2003 unit_combo.addItems(units)
2004 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2005 grid.addWidget(unit_combo, 3, 1)
2006 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2007 + '\n1BTC=1000mBTC.\n' \
2008 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2010 usechange_cb = QCheckBox(_('Use change addresses'))
2011 usechange_cb.setChecked(self.wallet.use_change)
2012 grid.addWidget(usechange_cb, 4, 0)
2013 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2014 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2016 grid.setRowStretch(5,1)
2018 vbox.addLayout(grid)
2019 vbox.addLayout(ok_cancel_buttons(d))
2023 if not d.exec_(): return
2025 fee = unicode(fee_e.text())
2027 fee = self.read_amount(fee)
2029 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2032 self.wallet.set_fee(fee)
2034 nz = unicode(nz_e.text())
2039 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2042 if self.num_zeros != nz:
2044 self.config.set_key('num_zeros', nz, True)
2045 self.update_history_tab()
2046 self.update_receive_tab()
2048 usechange_result = usechange_cb.isChecked()
2049 if self.wallet.use_change != usechange_result:
2050 self.wallet.use_change = usechange_result
2051 self.config.set_key('use_change', self.wallet.use_change, True)
2053 unit_result = units[unit_combo.currentIndex()]
2054 if self.base_unit() != unit_result:
2055 self.decimal_point = 8 if unit_result == 'BTC' else 5
2056 self.config.set_key('decimal_point', self.decimal_point, True)
2057 self.update_history_tab()
2058 self.update_status()
2060 need_restart = False
2062 lang_request = languages.keys()[lang_combo.currentIndex()]
2063 if lang_request != self.config.get('language'):
2064 self.config.set_key("language", lang_request, True)
2067 run_hook('close_settings_dialog')
2070 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2073 def run_network_dialog(self):
2074 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2076 def closeEvent(self, event):
2078 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2079 self.save_column_widths()
2080 self.config.set_key("console-history", self.console.history[-50:], True)
2081 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2086 def plugins_dialog(self):
2087 from electrum.plugins import plugins
2090 d.setWindowTitle(_('Electrum Plugins'))
2093 vbox = QVBoxLayout(d)
2096 scroll = QScrollArea()
2097 scroll.setEnabled(True)
2098 scroll.setWidgetResizable(True)
2099 scroll.setMinimumSize(400,250)
2100 vbox.addWidget(scroll)
2104 w.setMinimumHeight(len(plugins)*35)
2106 grid = QGridLayout()
2107 grid.setColumnStretch(0,1)
2110 def do_toggle(cb, p, w):
2113 if w: w.setEnabled(r)
2115 def mk_toggle(cb, p, w):
2116 return lambda: do_toggle(cb,p,w)
2118 for i, p in enumerate(plugins):
2120 cb = QCheckBox(p.fullname())
2121 cb.setDisabled(not p.is_available())
2122 cb.setChecked(p.is_enabled())
2123 grid.addWidget(cb, i, 0)
2124 if p.requires_settings():
2125 w = p.settings_widget(self)
2126 w.setEnabled( p.is_enabled() )
2127 grid.addWidget(w, i, 1)
2130 cb.clicked.connect(mk_toggle(cb,p,w))
2131 grid.addWidget(HelpButton(p.description()), i, 2)
2133 print_msg(_("Error: cannot display plugin"), p)
2134 traceback.print_exc(file=sys.stdout)
2135 grid.setRowStretch(i+1,1)
2137 vbox.addLayout(close_button(d))
2142 def show_account_details(self, k):
2144 d.setWindowTitle(_('Account Details'))
2147 vbox = QVBoxLayout(d)
2148 roots = self.wallet.get_roots(k)
2150 name = self.wallet.get_account_name(k)
2151 label = QLabel('Name: ' + name)
2152 vbox.addWidget(label)
2154 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2155 vbox.addWidget(QLabel('Type: ' + acctype))
2157 label = QLabel('Derivation: ' + k)
2158 vbox.addWidget(label)
2161 # mpk = self.wallet.master_public_keys[root]
2162 # text = QTextEdit()
2163 # text.setReadOnly(True)
2164 # text.setMaximumHeight(120)
2165 # text.setText(repr(mpk))
2166 # vbox.addWidget(text)
2168 vbox.addLayout(close_button(d))