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", 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
255 def load_wallet(self, wallet):
258 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
259 self.current_account = self.wallet.storage.get("current_account", None)
260 self.pending_accounts = self.wallet.storage.get('pending_accounts',{})
262 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
263 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
264 self.setWindowTitle( title )
266 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
267 self.notify_transactions()
270 accounts = self.wallet.get_account_names()
271 self.account_selector.clear()
272 if len(accounts) > 1:
273 self.account_selector.addItems([_("All accounts")] + accounts.values())
274 self.account_selector.setCurrentIndex(0)
275 self.account_selector.show()
277 self.account_selector.hide()
279 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( 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( 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", 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):
1396 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1397 address = unicode(text)
1399 if is_valid(address):
1400 self.wallet.add_contact(address)
1401 self.update_contacts_tab()
1402 self.update_history_tab()
1403 self.update_completions()
1405 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1408 def new_account_dialog(self):
1410 dialog = QDialog(self)
1412 dialog.setWindowTitle(_("New Account"))
1414 vbox = QVBoxLayout()
1415 vbox.addWidget(QLabel(_('Account name')+':'))
1418 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1419 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1424 vbox.addLayout(ok_cancel_buttons(dialog))
1425 dialog.setLayout(vbox)
1429 name = str(e.text())
1432 k, addr = self.wallet.new_account_address()
1433 self.wallet.set_label(k, name)
1434 self.pending_accounts[k] = addr
1435 self.wallet.storage.put('pending_accounts', self.pending_accounts)
1436 self.update_receive_tab()
1437 self.tabs.setCurrentIndex(2)
1441 def show_master_public_key_old(self):
1442 dialog = QDialog(self)
1444 dialog.setWindowTitle(_("Master Public Key"))
1446 main_text = QTextEdit()
1447 main_text.setText(self.wallet.get_master_public_key())
1448 main_text.setReadOnly(True)
1449 main_text.setMaximumHeight(170)
1450 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1452 ok_button = QPushButton(_("OK"))
1453 ok_button.setDefault(True)
1454 ok_button.clicked.connect(dialog.accept)
1456 main_layout = QGridLayout()
1457 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1459 main_layout.addWidget(main_text, 1, 0)
1460 main_layout.addWidget(qrw, 1, 1 )
1462 vbox.addLayout(close_button(dialog))
1463 dialog.setLayout(vbox)
1467 def show_master_public_key(self):
1469 if self.wallet.seed_version == 4:
1470 self.show_master_public_keys_old()
1473 dialog = QDialog(self)
1475 dialog.setWindowTitle(_("Master Public Keys"))
1477 chain_text = QTextEdit()
1478 chain_text.setReadOnly(True)
1479 chain_text.setMaximumHeight(170)
1480 chain_qrw = QRCodeWidget()
1482 mpk_text = QTextEdit()
1483 mpk_text.setReadOnly(True)
1484 mpk_text.setMaximumHeight(170)
1485 mpk_qrw = QRCodeWidget()
1487 main_layout = QGridLayout()
1489 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1490 main_layout.addWidget(mpk_text, 1, 1)
1491 main_layout.addWidget(mpk_qrw, 1, 2)
1493 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1494 main_layout.addWidget(chain_text, 2, 1)
1495 main_layout.addWidget(chain_qrw, 2, 2)
1498 c, K, cK = self.wallet.master_public_keys[str(key)]
1499 chain_text.setText(c)
1500 chain_qrw.set_addr(c)
1501 chain_qrw.update_qr()
1506 key_selector = QComboBox()
1507 keys = sorted(self.wallet.master_public_keys.keys())
1508 key_selector.addItems(keys)
1510 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1511 main_layout.addWidget(key_selector, 0, 1)
1512 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1516 vbox = QVBoxLayout()
1517 vbox.addLayout(main_layout)
1518 vbox.addLayout(close_button(dialog))
1520 dialog.setLayout(vbox)
1525 def show_seed_dialog(self, password):
1526 if self.wallet.is_watching_only():
1527 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1530 if self.wallet.seed:
1532 seed = self.wallet.decode_seed(password)
1534 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1536 from seed_dialog import SeedDialog
1537 d = SeedDialog(self, seed, self.wallet.imported_keys)
1541 for k in self.wallet.master_private_keys.keys():
1542 pk = self.wallet.get_master_private_key(k, password)
1544 from seed_dialog import PrivateKeysDialog
1545 d = PrivateKeysDialog(self,l)
1552 def show_qrcode(self, data, title = _("QR code")):
1556 d.setWindowTitle(title)
1557 d.setMinimumSize(270, 300)
1558 vbox = QVBoxLayout()
1559 qrw = QRCodeWidget(data)
1560 vbox.addWidget(qrw, 1)
1561 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1562 hbox = QHBoxLayout()
1566 filename = "qrcode.bmp"
1567 bmp.save_qrcode(qrw.qr, filename)
1568 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1570 b = QPushButton(_("Save"))
1572 b.clicked.connect(print_qr)
1574 b = QPushButton(_("Close"))
1576 b.clicked.connect(d.accept)
1579 vbox.addLayout(hbox)
1584 def do_protect(self, func, args):
1585 if self.wallet.use_encryption:
1586 password = self.password_dialog()
1592 if args != (False,):
1593 args = (self,) + args + (password,)
1595 args = (self,password)
1600 def show_private_key(self, address, password):
1601 if not address: return
1603 pk_list = self.wallet.get_private_key(address, password)
1604 except BaseException, e:
1605 self.show_message(str(e))
1607 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1611 def do_sign(self, address, message, signature, password):
1612 message = unicode(message.toPlainText())
1613 message = message.encode('utf-8')
1615 sig = self.wallet.sign_message(str(address.text()), message, password)
1616 signature.setText(sig)
1617 except BaseException, e:
1618 self.show_message(str(e))
1620 def sign_message(self, address):
1621 if not address: return
1624 d.setWindowTitle(_('Sign Message'))
1625 d.setMinimumSize(410, 290)
1627 tab_widget = QTabWidget()
1629 layout = QGridLayout(tab)
1631 sign_address = QLineEdit()
1633 sign_address.setText(address)
1634 layout.addWidget(QLabel(_('Address')), 1, 0)
1635 layout.addWidget(sign_address, 1, 1)
1637 sign_message = QTextEdit()
1638 layout.addWidget(QLabel(_('Message')), 2, 0)
1639 layout.addWidget(sign_message, 2, 1)
1640 layout.setRowStretch(2,3)
1642 sign_signature = QTextEdit()
1643 layout.addWidget(QLabel(_('Signature')), 3, 0)
1644 layout.addWidget(sign_signature, 3, 1)
1645 layout.setRowStretch(3,1)
1648 hbox = QHBoxLayout()
1649 b = QPushButton(_("Sign"))
1651 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1652 b = QPushButton(_("Close"))
1653 b.clicked.connect(d.accept)
1655 layout.addLayout(hbox, 4, 1)
1656 tab_widget.addTab(tab, _("Sign"))
1660 layout = QGridLayout(tab)
1662 verify_address = QLineEdit()
1663 layout.addWidget(QLabel(_('Address')), 1, 0)
1664 layout.addWidget(verify_address, 1, 1)
1666 verify_message = QTextEdit()
1667 layout.addWidget(QLabel(_('Message')), 2, 0)
1668 layout.addWidget(verify_message, 2, 1)
1669 layout.setRowStretch(2,3)
1671 verify_signature = QTextEdit()
1672 layout.addWidget(QLabel(_('Signature')), 3, 0)
1673 layout.addWidget(verify_signature, 3, 1)
1674 layout.setRowStretch(3,1)
1677 message = unicode(verify_message.toPlainText())
1678 message = message.encode('utf-8')
1679 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1680 self.show_message(_("Signature verified"))
1682 self.show_message(_("Error: wrong signature"))
1684 hbox = QHBoxLayout()
1685 b = QPushButton(_("Verify"))
1686 b.clicked.connect(do_verify)
1688 b = QPushButton(_("Close"))
1689 b.clicked.connect(d.accept)
1691 layout.addLayout(hbox, 4, 1)
1692 tab_widget.addTab(tab, _("Verify"))
1694 vbox = QVBoxLayout()
1695 vbox.addWidget(tab_widget)
1702 def question(self, msg):
1703 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1705 def show_message(self, msg):
1706 QMessageBox.information(self, _('Message'), msg, _('OK'))
1708 def password_dialog(self ):
1715 vbox = QVBoxLayout()
1716 msg = _('Please enter your password')
1717 vbox.addWidget(QLabel(msg))
1719 grid = QGridLayout()
1721 grid.addWidget(QLabel(_('Password')), 1, 0)
1722 grid.addWidget(pw, 1, 1)
1723 vbox.addLayout(grid)
1725 vbox.addLayout(ok_cancel_buttons(d))
1728 run_hook('password_dialog', pw, grid, 1)
1729 if not d.exec_(): return
1730 return unicode(pw.text())
1739 def tx_from_text(self, txt):
1740 "json or raw hexadecimal"
1743 tx = Transaction(txt)
1749 tx_dict = json.loads(str(txt))
1750 assert "hex" in tx_dict.keys()
1751 assert "complete" in tx_dict.keys()
1752 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1753 if not tx_dict["complete"]:
1754 assert "input_info" in tx_dict.keys()
1755 input_info = json.loads(tx_dict['input_info'])
1756 tx.add_input_info(input_info)
1761 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1765 def read_tx_from_file(self):
1766 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1770 with open(fileName, "r") as f:
1771 file_content = f.read()
1772 except (ValueError, IOError, os.error), reason:
1773 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1775 return self.tx_from_text(file_content)
1779 def sign_raw_transaction(self, tx, input_info, password):
1780 self.wallet.signrawtransaction(tx, input_info, [], password)
1782 def do_process_from_text(self):
1783 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1786 tx = self.tx_from_text(text)
1788 self.show_transaction(tx)
1790 def do_process_from_file(self):
1791 tx = self.read_tx_from_file()
1793 self.show_transaction(tx)
1795 def do_process_from_csvReader(self, csvReader):
1798 for row in csvReader:
1800 amount = float(row[1])
1801 amount = int(100000000*amount)
1802 outputs.append((address, amount))
1803 except (ValueError, IOError, os.error), reason:
1804 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1808 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1809 except BaseException, e:
1810 self.show_message(str(e))
1813 self.show_transaction(tx)
1815 def do_process_from_csv_file(self):
1816 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1820 with open(fileName, "r") as f:
1821 csvReader = csv.reader(f)
1822 self.do_process_from_csvReader(csvReader)
1823 except (ValueError, IOError, os.error), reason:
1824 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1827 def do_process_from_csv_text(self):
1828 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1829 + _("Format: address, amount. One output per line"), _("Load CSV"))
1832 f = StringIO.StringIO(text)
1833 csvReader = csv.reader(f)
1834 self.do_process_from_csvReader(csvReader)
1839 def do_export_privkeys(self, password):
1840 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.")))
1843 select_export = _('Select file to export your private keys to')
1844 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1846 with open(fileName, "w+") as csvfile:
1847 transaction = csv.writer(csvfile)
1848 transaction.writerow(["address", "private_key"])
1850 addresses = self.wallet.addresses(True)
1852 for addr in addresses:
1853 pk = "".join(self.wallet.get_private_key(addr, password))
1854 transaction.writerow(["%34s"%addr,pk])
1856 self.show_message(_("Private keys exported."))
1858 except (IOError, os.error), reason:
1859 export_error_label = _("Electrum was unable to produce a private key-export.")
1860 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1862 except BaseException, e:
1863 self.show_message(str(e))
1867 def do_import_labels(self):
1868 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1869 if not labelsFile: return
1871 f = open(labelsFile, 'r')
1874 for key, value in json.loads(data).items():
1875 self.wallet.set_label(key, value)
1876 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1877 except (IOError, os.error), reason:
1878 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1881 def do_export_labels(self):
1882 labels = self.wallet.labels
1884 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1886 with open(fileName, 'w+') as f:
1887 json.dump(labels, f)
1888 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1889 except (IOError, os.error), reason:
1890 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1893 def do_export_history(self):
1894 from lite_window import csv_transaction
1895 csv_transaction(self.wallet)
1899 def do_import_privkey(self, password):
1900 if not self.wallet.imported_keys:
1901 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1902 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1903 + _('Are you sure you understand what you are doing?'), 3, 4)
1906 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1909 text = str(text).split()
1914 addr = self.wallet.import_key(key, password)
1915 except BaseException as e:
1921 addrlist.append(addr)
1923 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1925 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1926 self.update_receive_tab()
1927 self.update_history_tab()
1930 def settings_dialog(self):
1932 d.setWindowTitle(_('Electrum Settings'))
1934 vbox = QVBoxLayout()
1935 grid = QGridLayout()
1936 grid.setColumnStretch(0,1)
1938 nz_label = QLabel(_('Display zeros') + ':')
1939 grid.addWidget(nz_label, 0, 0)
1940 nz_e = AmountEdit(None,True)
1941 nz_e.setText("%d"% self.num_zeros)
1942 grid.addWidget(nz_e, 0, 1)
1943 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1944 grid.addWidget(HelpButton(msg), 0, 2)
1945 if not self.config.is_modifiable('num_zeros'):
1946 for w in [nz_e, nz_label]: w.setEnabled(False)
1948 lang_label=QLabel(_('Language') + ':')
1949 grid.addWidget(lang_label, 1, 0)
1950 lang_combo = QComboBox()
1951 from electrum.i18n import languages
1952 lang_combo.addItems(languages.values())
1954 index = languages.keys().index(self.config.get("language",''))
1957 lang_combo.setCurrentIndex(index)
1958 grid.addWidget(lang_combo, 1, 1)
1959 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1960 if not self.config.is_modifiable('language'):
1961 for w in [lang_combo, lang_label]: w.setEnabled(False)
1964 fee_label = QLabel(_('Transaction fee') + ':')
1965 grid.addWidget(fee_label, 2, 0)
1966 fee_e = AmountEdit(self.base_unit)
1967 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1968 grid.addWidget(fee_e, 2, 1)
1969 msg = _('Fee per kilobyte of transaction.') + ' ' \
1970 + _('Recommended value') + ': ' + self.format_amount(50000)
1971 grid.addWidget(HelpButton(msg), 2, 2)
1972 if not self.config.is_modifiable('fee_per_kb'):
1973 for w in [fee_e, fee_label]: w.setEnabled(False)
1975 units = ['BTC', 'mBTC']
1976 unit_label = QLabel(_('Base unit') + ':')
1977 grid.addWidget(unit_label, 3, 0)
1978 unit_combo = QComboBox()
1979 unit_combo.addItems(units)
1980 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1981 grid.addWidget(unit_combo, 3, 1)
1982 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
1983 + '\n1BTC=1000mBTC.\n' \
1984 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
1986 usechange_cb = QCheckBox(_('Use change addresses'))
1987 usechange_cb.setChecked(self.wallet.use_change)
1988 grid.addWidget(usechange_cb, 4, 0)
1989 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
1990 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1992 grid.setRowStretch(5,1)
1994 vbox.addLayout(grid)
1995 vbox.addLayout(ok_cancel_buttons(d))
1999 if not d.exec_(): return
2001 fee = unicode(fee_e.text())
2003 fee = self.read_amount(fee)
2005 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2008 self.wallet.set_fee(fee)
2010 nz = unicode(nz_e.text())
2015 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2018 if self.num_zeros != nz:
2020 self.config.set_key('num_zeros', nz, True)
2021 self.update_history_tab()
2022 self.update_receive_tab()
2024 usechange_result = usechange_cb.isChecked()
2025 if self.wallet.use_change != usechange_result:
2026 self.wallet.use_change = usechange_result
2027 self.config.set_key('use_change', self.wallet.use_change, True)
2029 unit_result = units[unit_combo.currentIndex()]
2030 if self.base_unit() != unit_result:
2031 self.decimal_point = 8 if unit_result == 'BTC' else 5
2032 self.config.set_key('decimal_point', self.decimal_point, True)
2033 self.update_history_tab()
2034 self.update_status()
2036 need_restart = False
2038 lang_request = languages.keys()[lang_combo.currentIndex()]
2039 if lang_request != self.config.get('language'):
2040 self.config.set_key("language", lang_request, True)
2043 run_hook('close_settings_dialog')
2046 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2049 def run_network_dialog(self):
2050 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2052 def closeEvent(self, event):
2054 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2055 self.save_column_widths()
2056 self.config.set_key("console-history", self.console.history[-50:], True)
2057 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2062 def plugins_dialog(self):
2063 from electrum.plugins import plugins
2066 d.setWindowTitle(_('Electrum Plugins'))
2069 vbox = QVBoxLayout(d)
2072 scroll = QScrollArea()
2073 scroll.setEnabled(True)
2074 scroll.setWidgetResizable(True)
2075 scroll.setMinimumSize(400,250)
2076 vbox.addWidget(scroll)
2080 w.setMinimumHeight(len(plugins)*35)
2082 grid = QGridLayout()
2083 grid.setColumnStretch(0,1)
2086 def mk_toggle(cb, p):
2087 return lambda: cb.setChecked(p.toggle())
2088 for i, p in enumerate(plugins):
2090 cb = QCheckBox(p.fullname())
2091 cb.setDisabled(not p.is_available())
2092 cb.setChecked(p.is_enabled())
2093 cb.clicked.connect(mk_toggle(cb,p))
2094 grid.addWidget(cb, i, 0)
2095 if p.requires_settings():
2096 b = EnterButton(_('Settings'), p.settings_dialog)
2097 b.setEnabled( p.is_enabled() )
2098 grid.addWidget(b, i, 1)
2099 grid.addWidget(HelpButton(p.description()), i, 2)
2101 print_msg(_("Error: cannot display plugin"), p)
2102 traceback.print_exc(file=sys.stdout)
2103 grid.setRowStretch(i+1,1)
2105 vbox.addLayout(close_button(d))
2110 def show_account_details(self, k):
2112 d.setWindowTitle(_('Account Details'))
2115 vbox = QVBoxLayout(d)
2116 roots = self.wallet.get_roots(k)
2118 name = self.wallet.get_account_name(k)
2119 label = QLabel('Name: ' + name)
2120 vbox.addWidget(label)
2122 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2123 vbox.addWidget(QLabel('Type: ' + acctype))
2125 label = QLabel('Derivation: ' + k)
2126 vbox.addWidget(label)
2129 # mpk = self.wallet.master_public_keys[root]
2130 # text = QTextEdit()
2131 # text.setReadOnly(True)
2132 # text.setMaximumHeight(120)
2133 # text.setText(repr(mpk))
2134 # vbox.addWidget(text)
2136 vbox.addLayout(close_button(d))