3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
30 sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
32 from PyQt4.QtGui import *
33 from PyQt4.QtCore import *
34 import PyQt4.QtCore as QtCore
36 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
37 from electrum.plugins import run_hook
42 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
44 from electrum.wallet import format_satoshis
45 from electrum import Transaction
46 from electrum import mnemonic
47 from electrum import util, bitcoin, commands, Interface, Wallet
48 from electrum import SimpleConfig, Wallet, WalletStorage
51 from electrum import bmp, pyqrnative
53 from amountedit import AmountEdit
54 from network_dialog import NetworkDialog
55 from qrcodewidget import QRCodeWidget
57 from decimal import Decimal
65 if platform.system() == 'Windows':
66 MONOSPACE_FONT = 'Lucida Console'
67 elif platform.system() == 'Darwin':
68 MONOSPACE_FONT = 'Monaco'
70 MONOSPACE_FONT = 'monospace'
72 from electrum import ELECTRUM_VERSION
82 class StatusBarButton(QPushButton):
83 def __init__(self, icon, tooltip, func):
84 QPushButton.__init__(self, icon, '')
85 self.setToolTip(tooltip)
87 self.setMaximumWidth(25)
88 self.clicked.connect(func)
90 self.setIconSize(QSize(25,25))
92 def keyPressEvent(self, e):
93 if e.key() == QtCore.Qt.Key_Return:
105 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
107 class ElectrumWindow(QMainWindow):
108 def changeEvent(self, event):
109 flags = self.windowFlags();
110 if event and event.type() == QtCore.QEvent.WindowStateChange:
111 if self.windowState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(True)
113 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
114 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
115 # Electrum from closing.
116 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
117 # self.setWindowFlags(flags & ~Qt.ToolTip)
118 elif event.oldState() & QtCore.Qt.WindowMinimized:
119 self.build_menu(False)
120 #self.setWindowFlags(flags | Qt.ToolTip)
122 def build_menu(self, is_hidden = False):
124 if self.isMinimized():
125 m.addAction(_("Show"), self.showNormal)
127 m.addAction(_("Hide"), self.showMinimized)
130 m.addAction(_("Exit Electrum"), self.close)
131 self.tray.setContextMenu(m)
133 def tray_activated(self, reason):
134 if reason == QSystemTrayIcon.DoubleClick:
138 def __init__(self, config, network, go_lite):
139 QMainWindow.__init__(self)
142 self.network = network
143 self.go_lite = go_lite
145 self._close_electrum = False
147 self.current_account = self.config.get("current_account", None)
149 self.icon = QIcon(':icons/electrum.png')
150 self.tray = QSystemTrayIcon(self.icon, self)
151 self.tray.setToolTip('Electrum')
152 self.tray.activated.connect(self.tray_activated)
156 self.create_status_bar()
158 self.need_update = threading.Event()
160 self.expert_mode = config.get('classic_expert_mode', False)
161 self.decimal_point = config.get('decimal_point', 8)
162 self.num_zeros = int(config.get('num_zeros',0))
164 set_language(config.get('language'))
166 self.funds_error = False
167 self.completions = QStringListModel()
169 self.tabs = tabs = QTabWidget(self)
170 self.column_widths = self.config.get("column_widths", default_column_widths )
171 tabs.addTab(self.create_history_tab(), _('History') )
172 tabs.addTab(self.create_send_tab(), _('Send') )
173 tabs.addTab(self.create_receive_tab(), _('Receive') )
174 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
175 tabs.addTab(self.create_console_tab(), _('Console') )
176 tabs.setMinimumSize(600, 400)
177 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
178 self.setCentralWidget(tabs)
180 g = self.config.get("winpos-qt",[100, 100, 840, 400])
181 self.setGeometry(g[0], g[1], g[2], g[3])
185 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
186 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
187 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
188 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
189 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
191 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
192 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
193 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
195 self.history_list.setFocus(True)
197 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
198 if platform.system() == 'Windows':
199 n = 3 if self.wallet.seed else 2
200 tabs.setCurrentIndex (n)
201 tabs.setCurrentIndex (0)
207 def load_wallet(self, wallet):
211 self.network.register_callback('updated', lambda: self.need_update.set())
212 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
213 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
214 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
215 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
216 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
217 if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
218 self.setWindowTitle( title )
220 # set initial message
221 self.console.showMessage(self.network.banner)
222 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
223 self.notify_transactions()
226 accounts = self.wallet.get_account_names()
227 self.account_selector.clear()
228 if len(accounts) > 1:
229 self.account_selector.addItems([_("All accounts")] + accounts.values())
230 self.account_selector.setCurrentIndex(0)
231 self.account_selector.show()
233 self.account_selector.hide()
235 self.new_account.setEnabled(self.wallet.seed_version>4)
237 self.update_lock_icon()
238 self.update_buttons_on_seed()
239 self.update_console()
241 run_hook('load_wallet')
244 def select_wallet_file(self):
245 wallet_folder = self.wallet.storage.path
246 re.sub("(\/\w*.dat)$", "", wallet_folder)
247 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
251 def open_wallet(self):
253 filename = self.select_wallet_file()
257 storage = WalletStorage({'wallet_path': filename})
258 if not storage.file_exists:
259 self.show_message("file not found "+ filename)
262 self.wallet.stop_threads()
265 wallet = Wallet(storage)
266 wallet.start_threads(self.network)
268 self.load_wallet(wallet)
272 def backup_wallet(self):
274 path = self.wallet.storage.path
275 wallet_folder = os.path.dirname(path)
276 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
277 new_filename = unicode(new_filename)
278 if not ok or not new_filename:
281 new_path = os.path.join(wallet_folder, new_filename)
284 shutil.copy2(path, new_path)
285 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
286 except (IOError, os.error), reason:
287 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
290 def new_wallet(self):
293 wallet_folder = os.path.dirname(self.wallet.storage.path)
294 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
295 filename = unicode(filename)
296 if not ok or not filename:
298 filename = os.path.join(wallet_folder, filename)
300 storage = WalletStorage({'wallet_path': filename})
301 assert not storage.file_exists
303 wizard = installwizard.InstallWizard(self.config, self.network, storage)
304 wallet = wizard.run()
306 self.load_wallet(wallet)
310 def init_menubar(self):
313 file_menu = menubar.addMenu(_("&File"))
314 open_wallet_action = file_menu.addAction(_("&Open"))
315 open_wallet_action.triggered.connect(self.open_wallet)
317 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
318 new_wallet_action.triggered.connect(self.new_wallet)
320 wallet_backup = file_menu.addAction(_("&Copy"))
321 wallet_backup.triggered.connect(self.backup_wallet)
323 quit_item = file_menu.addAction(_("&Close"))
324 quit_item.triggered.connect(self.close)
326 wallet_menu = menubar.addMenu(_("&Wallet"))
328 new_contact = wallet_menu.addAction(_("&New contact"))
329 new_contact.triggered.connect(self.new_contact_dialog)
331 self.new_account = wallet_menu.addAction(_("&New account"))
332 self.new_account.triggered.connect(self.new_account_dialog)
334 wallet_menu.addSeparator()
336 #if self.wallet.seed:
337 show_seed = wallet_menu.addAction(_("&Seed"))
338 show_seed.triggered.connect(self.show_seed_dialog)
340 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
341 show_mpk.triggered.connect(self.show_master_public_key)
343 wallet_menu.addSeparator()
345 csv_transaction_menu = wallet_menu.addMenu(_("&Create transaction"))
347 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
348 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
350 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
351 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
353 raw_transaction_menu = wallet_menu.addMenu(_("&Load transaction"))
355 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
356 raw_transaction_file.triggered.connect(self.do_process_from_file)
358 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
359 raw_transaction_text.triggered.connect(self.do_process_from_text)
362 tools_menu = menubar.addMenu(_("&Tools"))
364 # Settings / Preferences are all reserved keywords in OSX using this as work around
365 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
366 preferences_menu = tools_menu.addAction(preferences_name)
367 preferences_menu.triggered.connect(self.settings_dialog)
369 plugins_labels = tools_menu.addAction(_("&Plugins"))
370 plugins_labels.triggered.connect(self.plugins_dialog)
372 wallet_menu.addSeparator()
374 labels_menu = tools_menu.addMenu(_("&Labels"))
375 import_labels = labels_menu.addAction(_("&Import"))
376 import_labels.triggered.connect(self.do_import_labels)
377 export_labels = labels_menu.addAction(_("&Export"))
378 export_labels.triggered.connect(self.do_export_labels)
380 keys_menu = tools_menu.addMenu(_("&Private keys"))
381 import_keys = keys_menu.addAction(_("&Import"))
382 import_keys.triggered.connect(self.do_import_privkey)
383 export_keys = keys_menu.addAction(_("&Export"))
384 export_keys.triggered.connect(self.do_export_privkeys)
386 ex_history = tools_menu.addAction(_("&Export History"))
387 ex_history.triggered.connect(self.do_export_history)
390 help_menu = menubar.addMenu(_("&Help"))
391 show_about = help_menu.addAction(_("&About"))
392 show_about.triggered.connect(self.show_about)
393 web_open = help_menu.addAction(_("&Official website"))
394 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
396 help_menu.addSeparator()
397 doc_open = help_menu.addAction(_("&Documentation"))
398 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
399 report_bug = help_menu.addAction(_("&Report Bug"))
400 report_bug.triggered.connect(self.show_report_bug)
402 self.setMenuBar(menubar)
404 def show_about(self):
405 QMessageBox.about(self, "Electrum",
406 _("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."))
408 def show_report_bug(self):
409 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
410 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
413 def notify_transactions(self):
414 print_error("Notifying GUI")
415 if len(self.network.interface.pending_transactions_for_notifications) > 0:
416 # Combine the transactions if there are more then three
417 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
420 for tx in self.network.interface.pending_transactions_for_notifications:
421 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
425 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
426 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
428 self.network.interface.pending_transactions_for_notifications = []
430 for tx in self.network.interface.pending_transactions_for_notifications:
432 self.network.interface.pending_transactions_for_notifications.remove(tx)
433 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
435 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
437 def notify(self, message):
438 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
442 def set_label(self, name, text = None):
444 old_text = self.wallet.labels.get(name)
447 self.wallet.labels[name] = text
448 self.wallet.storage.put('labels', self.wallet.labels)
452 self.wallet.labels.pop(name)
454 run_hook('set_label', name, text, changed)
458 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
459 def getOpenFileName(self, title, filter = ""):
460 directory = self.config.get('io_dir', os.path.expanduser('~'))
461 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
462 if fileName and directory != os.path.dirname(fileName):
463 self.config.set_key('io_dir', os.path.dirname(fileName), True)
466 def getSaveFileName(self, title, filename, filter = ""):
467 directory = self.config.get('io_dir', os.path.expanduser('~'))
468 path = os.path.join( directory, filename )
469 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
470 if fileName and directory != os.path.dirname(fileName):
471 self.config.set_key('io_dir', os.path.dirname(fileName), True)
475 QMainWindow.close(self)
476 run_hook('close_main_window')
478 def connect_slots(self, sender):
479 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
480 self.previous_payto_e=''
482 def timer_actions(self):
483 if self.need_update.is_set():
485 self.need_update.clear()
486 run_hook('timer_actions')
488 def format_amount(self, x, is_diff=False, whitespaces=False):
489 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
491 def read_amount(self, x):
492 if x in['.', '']: return None
493 p = pow(10, self.decimal_point)
494 return int( p * Decimal(x) )
497 assert self.decimal_point in [5,8]
498 return "BTC" if self.decimal_point == 8 else "mBTC"
500 def set_status_text(self, text):
501 self.balance_label.setText(text)
502 run_hook('set_status_text', text)
505 def update_status(self):
506 if self.network.interface and self.network.interface.is_connected:
507 if not self.wallet.up_to_date:
508 text = _("Synchronizing...")
509 icon = QIcon(":icons/status_waiting.png")
511 c, u = self.wallet.get_account_balance(self.current_account)
512 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
513 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
514 self.tray.setToolTip(text)
515 icon = QIcon(":icons/status_connected.png")
517 text = _("Not connected")
518 icon = QIcon(":icons/status_disconnected.png")
520 self.set_status_text(text)
521 self.status_button.setIcon( icon )
523 def update_wallet(self):
525 if self.wallet.up_to_date or not self.network.interface.is_connected:
526 self.update_history_tab()
527 self.update_receive_tab()
528 self.update_contacts_tab()
529 self.update_completions()
533 def create_history_tab(self):
534 self.history_list = l = MyTreeWidget(self)
536 for i,width in enumerate(self.column_widths['history']):
537 l.setColumnWidth(i, width)
538 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
539 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
540 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
542 l.customContextMenuRequested.connect(self.create_history_menu)
546 def create_history_menu(self, position):
547 self.history_list.selectedIndexes()
548 item = self.history_list.currentItem()
550 tx_hash = str(item.data(0, Qt.UserRole).toString())
551 if not tx_hash: return
553 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
554 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
555 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
556 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
559 def show_transaction(self, tx):
560 import transaction_dialog
561 d = transaction_dialog.TxDialog(tx, self)
564 def tx_label_clicked(self, item, column):
565 if column==2 and item.isSelected():
567 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568 self.history_list.editItem( item, column )
569 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
572 def tx_label_changed(self, item, column):
576 tx_hash = str(item.data(0, Qt.UserRole).toString())
577 tx = self.wallet.transactions.get(tx_hash)
578 text = unicode( item.text(2) )
579 self.set_label(tx_hash, text)
581 item.setForeground(2, QBrush(QColor('black')))
583 text = self.wallet.get_default_label(tx_hash)
584 item.setText(2, text)
585 item.setForeground(2, QBrush(QColor('gray')))
589 def edit_label(self, is_recv):
590 l = self.receive_list if is_recv else self.contacts_list
591 item = l.currentItem()
592 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
593 l.editItem( item, 1 )
594 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
598 def address_label_clicked(self, item, column, l, column_addr, column_label):
599 if column == column_label and item.isSelected():
600 is_editable = item.data(0, 32).toBool()
603 addr = unicode( item.text(column_addr) )
604 label = unicode( item.text(column_label) )
605 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
606 l.editItem( item, column )
607 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
610 def address_label_changed(self, item, column, l, column_addr, column_label):
611 if column == column_label:
612 addr = unicode( item.text(column_addr) )
613 text = unicode( item.text(column_label) )
614 is_editable = item.data(0, 32).toBool()
618 changed = self.set_label(addr, text)
620 self.update_history_tab()
621 self.update_completions()
623 self.current_item_changed(item)
625 run_hook('item_changed', item, column)
628 def current_item_changed(self, a):
629 run_hook('current_item_changed', a)
633 def update_history_tab(self):
635 self.history_list.clear()
636 for item in self.wallet.get_tx_history(self.current_account):
637 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
640 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
642 time_str = _("unknown")
645 time_str = 'unverified'
646 icon = QIcon(":icons/unconfirmed.png")
649 icon = QIcon(":icons/unconfirmed.png")
651 icon = QIcon(":icons/clock%d.png"%conf)
653 icon = QIcon(":icons/confirmed.png")
655 if value is not None:
656 v_str = self.format_amount(value, True, whitespaces=True)
660 balance_str = self.format_amount(balance, whitespaces=True)
663 label, is_default_label = self.wallet.get_label(tx_hash)
665 label = _('Pruned transaction outputs')
666 is_default_label = False
668 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
669 item.setFont(2, QFont(MONOSPACE_FONT))
670 item.setFont(3, QFont(MONOSPACE_FONT))
671 item.setFont(4, QFont(MONOSPACE_FONT))
673 item.setForeground(3, QBrush(QColor("#BC1E1E")))
675 item.setData(0, Qt.UserRole, tx_hash)
676 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
678 item.setForeground(2, QBrush(QColor('grey')))
680 item.setIcon(0, icon)
681 self.history_list.insertTopLevelItem(0,item)
684 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
687 def create_send_tab(self):
692 grid.setColumnMinimumWidth(3,300)
693 grid.setColumnStretch(5,1)
696 self.payto_e = QLineEdit()
697 grid.addWidget(QLabel(_('Pay to')), 1, 0)
698 grid.addWidget(self.payto_e, 1, 1, 1, 3)
700 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)
702 completer = QCompleter()
703 completer.setCaseSensitivity(False)
704 self.payto_e.setCompleter(completer)
705 completer.setModel(self.completions)
707 self.message_e = QLineEdit()
708 grid.addWidget(QLabel(_('Description')), 2, 0)
709 grid.addWidget(self.message_e, 2, 1, 1, 3)
710 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)
712 self.amount_e = AmountEdit(self.base_unit)
713 grid.addWidget(QLabel(_('Amount')), 3, 0)
714 grid.addWidget(self.amount_e, 3, 1, 1, 2)
715 grid.addWidget(HelpButton(
716 _('Amount to be sent.') + '\n\n' \
717 + _('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.') \
718 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
720 self.fee_e = AmountEdit(self.base_unit)
721 grid.addWidget(QLabel(_('Fee')), 4, 0)
722 grid.addWidget(self.fee_e, 4, 1, 1, 2)
723 grid.addWidget(HelpButton(
724 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
725 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
726 + _('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)
729 self.send_button = EnterButton(_("Send"), self.do_send)
730 grid.addWidget(self.send_button, 6, 1)
732 b = EnterButton(_("Clear"),self.do_clear)
733 grid.addWidget(b, 6, 2)
735 self.payto_sig = QLabel('')
736 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
738 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
739 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
748 def entry_changed( is_fee ):
749 self.funds_error = False
751 if self.amount_e.is_shortcut:
752 self.amount_e.is_shortcut = False
753 c, u = self.wallet.get_account_balance(self.current_account)
754 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
755 fee = self.wallet.estimated_fee(inputs)
757 self.amount_e.setText( self.format_amount(amount) )
758 self.fee_e.setText( self.format_amount( fee ) )
761 amount = self.read_amount(str(self.amount_e.text()))
762 fee = self.read_amount(str(self.fee_e.text()))
764 if not is_fee: fee = None
767 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
769 self.fee_e.setText( self.format_amount( fee ) )
772 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
776 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
777 self.funds_error = True
778 text = _( "Not enough funds" )
779 c, u = self.wallet.get_frozen_balance()
780 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
782 self.statusBar().showMessage(text)
783 self.amount_e.setPalette(palette)
784 self.fee_e.setPalette(palette)
786 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
787 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
789 run_hook('create_send_tab', grid)
793 def update_completions(self):
795 for addr,label in self.wallet.labels.items():
796 if addr in self.wallet.addressbook:
797 l.append( label + ' <' + addr + '>')
799 run_hook('update_completions', l)
800 self.completions.setStringList(l)
804 return lambda s, *args: s.do_protect(func, args)
809 label = unicode( self.message_e.text() )
810 r = unicode( self.payto_e.text() )
813 # label or alias, with address in brackets
814 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
815 to_address = m.group(2) if m else r
817 if not is_valid(to_address):
818 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
822 amount = self.read_amount(unicode( self.amount_e.text()))
824 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
827 fee = self.read_amount(unicode( self.fee_e.text()))
829 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
832 confirm_amount = self.config.get('confirm_amount', 100000000)
833 if amount >= confirm_amount:
834 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
837 self.send_tx(to_address, amount, fee, label)
841 def send_tx(self, to_address, amount, fee, label, password):
844 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
845 except BaseException, e:
846 traceback.print_exc(file=sys.stdout)
847 self.show_message(str(e))
850 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
851 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
854 run_hook('send_tx', tx)
857 self.set_label(tx.hash(), label)
860 h = self.wallet.send_tx(tx)
861 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
862 status, msg = self.wallet.receive_tx( h )
864 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
866 self.update_contacts_tab()
868 QMessageBox.warning(self, _('Error'), msg, _('OK'))
870 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
872 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
873 with open(fileName,'w') as f:
874 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
875 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
877 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
879 # add recipient to addressbook
880 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
881 self.wallet.addressbook.append(to_address)
886 def set_url(self, url):
887 address, amount, label, message, signature, identity, url = util.parse_url(url)
888 if self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
890 if label and self.wallet.labels.get(address) != label:
891 if self.question('Give label "%s" to address %s ?'%(label,address)):
892 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
893 self.wallet.addressbook.append(address)
894 self.set_label(address, label)
896 run_hook('set_url', url, self.show_message, self.question)
898 self.tabs.setCurrentIndex(1)
899 label = self.wallet.labels.get(address)
900 m_addr = label + ' <'+ address +'>' if label else address
901 self.payto_e.setText(m_addr)
903 self.message_e.setText(message)
904 self.amount_e.setText(amount)
906 self.set_frozen(self.payto_e,True)
907 self.set_frozen(self.amount_e,True)
908 self.set_frozen(self.message_e,True)
909 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
911 self.payto_sig.setVisible(False)
914 self.payto_sig.setVisible(False)
915 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
917 self.set_frozen(e,False)
920 def set_frozen(self,entry,frozen):
922 entry.setReadOnly(True)
923 entry.setFrame(False)
925 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
926 entry.setPalette(palette)
928 entry.setReadOnly(False)
931 palette.setColor(entry.backgroundRole(), QColor('white'))
932 entry.setPalette(palette)
935 def toggle_freeze(self,addr):
937 if addr in self.wallet.frozen_addresses:
938 self.wallet.unfreeze(addr)
940 self.wallet.freeze(addr)
941 self.update_receive_tab()
943 def toggle_priority(self,addr):
945 if addr in self.wallet.prioritized_addresses:
946 self.wallet.unprioritize(addr)
948 self.wallet.prioritize(addr)
949 self.update_receive_tab()
952 def create_list_tab(self, headers):
953 "generic tab creation method"
954 l = MyTreeWidget(self)
955 l.setColumnCount( len(headers) )
956 l.setHeaderLabels( headers )
966 vbox.addWidget(buttons)
971 buttons.setLayout(hbox)
976 def create_receive_tab(self):
977 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
978 l.setContextMenuPolicy(Qt.CustomContextMenu)
979 l.customContextMenuRequested.connect(self.create_receive_menu)
980 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
981 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
982 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
983 self.receive_list = l
984 self.receive_buttons_hbox = hbox
989 def receive_tab_set_mode(self, i):
990 self.save_column_widths()
991 self.expert_mode = (i == 1)
992 self.config.set_key('classic_expert_mode', self.expert_mode, True)
993 self.update_receive_tab()
996 def save_column_widths(self):
997 if not self.expert_mode:
998 widths = [ self.receive_list.columnWidth(0) ]
1001 for i in range(self.receive_list.columnCount() -1):
1002 widths.append(self.receive_list.columnWidth(i))
1003 self.column_widths["receive"][self.expert_mode] = widths
1005 self.column_widths["history"] = []
1006 for i in range(self.history_list.columnCount() - 1):
1007 self.column_widths["history"].append(self.history_list.columnWidth(i))
1009 self.column_widths["contacts"] = []
1010 for i in range(self.contacts_list.columnCount() - 1):
1011 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1013 self.config.set_key("column_widths", self.column_widths, True)
1016 def create_contacts_tab(self):
1017 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1018 l.setContextMenuPolicy(Qt.CustomContextMenu)
1019 l.customContextMenuRequested.connect(self.create_contact_menu)
1020 for i,width in enumerate(self.column_widths['contacts']):
1021 l.setColumnWidth(i, width)
1023 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1024 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1025 self.contacts_list = l
1026 self.contacts_buttons_hbox = hbox
1031 def delete_imported_key(self, addr):
1032 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1033 self.wallet.delete_imported_key(addr)
1034 self.update_receive_tab()
1035 self.update_history_tab()
1038 def create_receive_menu(self, position):
1039 # fixme: this function apparently has a side effect.
1040 # if it is not called the menu pops up several times
1041 #self.receive_list.selectedIndexes()
1043 item = self.receive_list.itemAt(position)
1045 addr = unicode(item.text(0))
1046 if not is_valid(addr):
1047 item.setExpanded(not item.isExpanded())
1050 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1051 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1052 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1053 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1054 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1055 if addr in self.wallet.imported_keys:
1056 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1058 if self.expert_mode:
1059 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1060 menu.addAction(t, lambda: self.toggle_freeze(addr))
1061 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1062 menu.addAction(t, lambda: self.toggle_priority(addr))
1064 run_hook('receive_menu', menu)
1065 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1068 def payto(self, addr):
1070 label = self.wallet.labels.get(addr)
1071 m_addr = label + ' <' + addr + '>' if label else addr
1072 self.tabs.setCurrentIndex(1)
1073 self.payto_e.setText(m_addr)
1074 self.amount_e.setFocus()
1077 def delete_contact(self, x):
1078 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1079 self.wallet.delete_contact(x)
1080 self.set_label(x, None)
1081 self.update_history_tab()
1082 self.update_contacts_tab()
1083 self.update_completions()
1086 def create_contact_menu(self, position):
1087 item = self.contacts_list.itemAt(position)
1089 addr = unicode(item.text(0))
1090 label = unicode(item.text(1))
1091 is_editable = item.data(0,32).toBool()
1092 payto_addr = item.data(0,33).toString()
1094 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1095 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1096 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1098 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1099 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1101 run_hook('create_contact_menu', menu, item)
1102 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1105 def update_receive_item(self, item):
1106 item.setFont(0, QFont(MONOSPACE_FONT))
1107 address = str(item.data(0,0).toString())
1108 label = self.wallet.labels.get(address,'')
1109 item.setData(1,0,label)
1110 item.setData(0,32, True) # is editable
1112 run_hook('update_receive_item', address, item)
1114 c, u = self.wallet.get_addr_balance(address)
1115 balance = self.format_amount(c + u)
1116 item.setData(2,0,balance)
1118 if self.expert_mode:
1119 if address in self.wallet.frozen_addresses:
1120 item.setBackgroundColor(0, QColor('lightblue'))
1121 elif address in self.wallet.prioritized_addresses:
1122 item.setBackgroundColor(0, QColor('lightgreen'))
1125 def update_receive_tab(self):
1126 l = self.receive_list
1129 l.setColumnHidden(2, not self.expert_mode)
1130 l.setColumnHidden(3, not self.expert_mode)
1131 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1132 l.setColumnWidth(i, width)
1134 if self.current_account is None:
1135 account_items = self.wallet.accounts.items()
1136 elif self.current_account != -1:
1137 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1141 for k, account in account_items:
1142 name = self.wallet.get_account_name(k)
1143 c,u = self.wallet.get_account_balance(k)
1144 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1145 l.addTopLevelItem(account_item)
1146 account_item.setExpanded(True)
1148 for is_change in ([0,1] if self.expert_mode else [0]):
1149 if self.expert_mode:
1150 name = _("Receiving") if not is_change else _("Change")
1151 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1152 account_item.addChild(seq_item)
1153 if not is_change: seq_item.setExpanded(True)
1155 seq_item = account_item
1159 for address in account.get_addresses(is_change):
1160 h = self.wallet.history.get(address,[])
1164 if gap > self.wallet.gap_limit:
1169 num_tx = '*' if h == ['*'] else "%d"%len(h)
1170 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1171 self.update_receive_item(item)
1173 item.setBackgroundColor(1, QColor('red'))
1174 seq_item.addChild(item)
1177 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1178 c,u = self.wallet.get_imported_balance()
1179 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1180 l.addTopLevelItem(account_item)
1181 account_item.setExpanded(True)
1182 for address in self.wallet.imported_keys.keys():
1183 item = QTreeWidgetItem( [ address, '', '', ''] )
1184 self.update_receive_item(item)
1185 account_item.addChild(item)
1188 # we use column 1 because column 0 may be hidden
1189 l.setCurrentItem(l.topLevelItem(0),1)
1192 def update_contacts_tab(self):
1193 l = self.contacts_list
1196 for address in self.wallet.addressbook:
1197 label = self.wallet.labels.get(address,'')
1198 n = self.wallet.get_num_tx(address)
1199 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1200 item.setFont(0, QFont(MONOSPACE_FONT))
1201 # 32 = label can be edited (bool)
1202 item.setData(0,32, True)
1204 item.setData(0,33, address)
1205 l.addTopLevelItem(item)
1207 run_hook('update_contacts_tab', l)
1208 l.setCurrentItem(l.topLevelItem(0))
1212 def create_console_tab(self):
1213 from console import Console
1214 self.console = console = Console()
1218 def update_console(self):
1219 console = self.console
1220 console.history = self.config.get("console-history",[])
1221 console.history_index = len(console.history)
1223 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1224 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1226 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1228 def mkfunc(f, method):
1229 return lambda *args: apply( f, (method, args, self.password_dialog ))
1231 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1232 methods[m] = mkfunc(c._run, m)
1234 console.updateNamespace(methods)
1237 def change_account(self,s):
1238 if s == _("All accounts"):
1239 self.current_account = None
1241 accounts = self.wallet.get_account_names()
1242 for k, v in accounts.items():
1244 self.current_account = k
1245 self.update_history_tab()
1246 self.update_status()
1247 self.update_receive_tab()
1249 def create_status_bar(self):
1252 sb.setFixedHeight(35)
1253 qtVersion = qVersion()
1255 self.balance_label = QLabel("")
1256 sb.addWidget(self.balance_label)
1258 from version_getter import UpdateLabel
1259 self.updatelabel = UpdateLabel(self.config, sb)
1261 self.account_selector = QComboBox()
1262 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1263 sb.addPermanentWidget(self.account_selector)
1265 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1266 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1268 self.lock_icon = QIcon()
1269 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1270 sb.addPermanentWidget( self.password_button )
1272 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1273 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1274 sb.addPermanentWidget( self.seed_button )
1275 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1276 sb.addPermanentWidget( self.status_button )
1278 run_hook('create_status_bar', (sb,))
1280 self.setStatusBar(sb)
1283 def update_lock_icon(self):
1284 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1285 self.password_button.setIcon( icon )
1288 def update_buttons_on_seed(self):
1289 if self.wallet.seed:
1290 self.seed_button.show()
1291 self.password_button.show()
1292 self.send_button.setText(_("Send"))
1294 self.password_button.hide()
1295 self.seed_button.hide()
1296 self.send_button.setText(_("Create unsigned transaction"))
1299 def change_password_dialog(self):
1300 from password_dialog import PasswordDialog
1301 d = PasswordDialog(self.wallet, self)
1303 self.update_lock_icon()
1306 def new_contact_dialog(self):
1307 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1308 address = unicode(text)
1310 if is_valid(address):
1311 self.wallet.add_contact(address)
1312 self.update_contacts_tab()
1313 self.update_history_tab()
1314 self.update_completions()
1316 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1319 def new_account_dialog(self):
1321 dialog = QDialog(self)
1323 dialog.setWindowTitle(_("New Account"))
1325 addr = self.wallet.new_account_address()
1326 vbox = QVBoxLayout()
1327 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1328 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1329 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1330 vbox.addWidget(QLabel(msg))
1331 vbox.addWidget(QLabel(_('Address')+':'))
1336 vbox.addLayout(ok_cancel_buttons(dialog))
1337 dialog.setLayout(vbox)
1344 def show_master_public_key(self):
1345 dialog = QDialog(self)
1347 dialog.setWindowTitle(_("Master Public Key"))
1349 main_text = QTextEdit()
1350 main_text.setText(self.wallet.get_master_public_key())
1351 main_text.setReadOnly(True)
1352 main_text.setMaximumHeight(170)
1353 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1355 ok_button = QPushButton(_("OK"))
1356 ok_button.setDefault(True)
1357 ok_button.clicked.connect(dialog.accept)
1359 main_layout = QGridLayout()
1360 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1362 main_layout.addWidget(main_text, 1, 0)
1363 main_layout.addWidget(qrw, 1, 1 )
1365 vbox = QVBoxLayout()
1366 vbox.addLayout(main_layout)
1367 hbox = QHBoxLayout()
1369 hbox.addWidget(ok_button)
1370 vbox.addLayout(hbox)
1372 dialog.setLayout(vbox)
1377 def show_seed_dialog(self, password):
1378 if not self.wallet.seed:
1379 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1382 seed = self.wallet.decode_seed(password)
1384 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1387 from seed_dialog import SeedDialog
1388 d = SeedDialog(self)
1389 d.show_seed(seed, self.wallet.imported_keys)
1393 def show_qrcode(self, data, title = _("QR code")):
1397 d.setWindowTitle(title)
1398 d.setMinimumSize(270, 300)
1399 vbox = QVBoxLayout()
1400 qrw = QRCodeWidget(data)
1401 vbox.addWidget(qrw, 1)
1402 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1403 hbox = QHBoxLayout()
1407 filename = "qrcode.bmp"
1408 bmp.save_qrcode(qrw.qr, filename)
1409 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1411 b = QPushButton(_("Save"))
1413 b.clicked.connect(print_qr)
1415 b = QPushButton(_("Close"))
1417 b.clicked.connect(d.accept)
1420 vbox.addLayout(hbox)
1425 def do_protect(self, func, args):
1426 if self.wallet.use_encryption:
1427 password = self.password_dialog()
1433 if args != (False,):
1434 args = (self,) + args + (password,)
1436 args = (self,password)
1441 def show_private_key(self, address, password):
1442 if not address: return
1444 pk_list = self.wallet.get_private_key(address, password)
1445 except BaseException, e:
1446 self.show_message(str(e))
1448 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1452 def do_sign(self, address, message, signature, password):
1453 message = unicode(message.toPlainText())
1454 message = message.encode('utf-8')
1456 sig = self.wallet.sign_message(str(address.text()), message, password)
1457 signature.setText(sig)
1458 except BaseException, e:
1459 self.show_message(str(e))
1461 def sign_message(self, address):
1462 if not address: return
1465 d.setWindowTitle(_('Sign Message'))
1466 d.setMinimumSize(410, 290)
1468 tab_widget = QTabWidget()
1470 layout = QGridLayout(tab)
1472 sign_address = QLineEdit()
1474 sign_address.setText(address)
1475 layout.addWidget(QLabel(_('Address')), 1, 0)
1476 layout.addWidget(sign_address, 1, 1)
1478 sign_message = QTextEdit()
1479 layout.addWidget(QLabel(_('Message')), 2, 0)
1480 layout.addWidget(sign_message, 2, 1)
1481 layout.setRowStretch(2,3)
1483 sign_signature = QTextEdit()
1484 layout.addWidget(QLabel(_('Signature')), 3, 0)
1485 layout.addWidget(sign_signature, 3, 1)
1486 layout.setRowStretch(3,1)
1489 hbox = QHBoxLayout()
1490 b = QPushButton(_("Sign"))
1492 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1493 b = QPushButton(_("Close"))
1494 b.clicked.connect(d.accept)
1496 layout.addLayout(hbox, 4, 1)
1497 tab_widget.addTab(tab, _("Sign"))
1501 layout = QGridLayout(tab)
1503 verify_address = QLineEdit()
1504 layout.addWidget(QLabel(_('Address')), 1, 0)
1505 layout.addWidget(verify_address, 1, 1)
1507 verify_message = QTextEdit()
1508 layout.addWidget(QLabel(_('Message')), 2, 0)
1509 layout.addWidget(verify_message, 2, 1)
1510 layout.setRowStretch(2,3)
1512 verify_signature = QTextEdit()
1513 layout.addWidget(QLabel(_('Signature')), 3, 0)
1514 layout.addWidget(verify_signature, 3, 1)
1515 layout.setRowStretch(3,1)
1518 message = unicode(verify_message.toPlainText())
1519 message = message.encode('utf-8')
1520 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1521 self.show_message(_("Signature verified"))
1523 self.show_message(_("Error: wrong signature"))
1525 hbox = QHBoxLayout()
1526 b = QPushButton(_("Verify"))
1527 b.clicked.connect(do_verify)
1529 b = QPushButton(_("Close"))
1530 b.clicked.connect(d.accept)
1532 layout.addLayout(hbox, 4, 1)
1533 tab_widget.addTab(tab, _("Verify"))
1535 vbox = QVBoxLayout()
1536 vbox.addWidget(tab_widget)
1543 def question(self, msg):
1544 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1546 def show_message(self, msg):
1547 QMessageBox.information(self, _('Message'), msg, _('OK'))
1549 def password_dialog(self ):
1556 vbox = QVBoxLayout()
1557 msg = _('Please enter your password')
1558 vbox.addWidget(QLabel(msg))
1560 grid = QGridLayout()
1562 grid.addWidget(QLabel(_('Password')), 1, 0)
1563 grid.addWidget(pw, 1, 1)
1564 vbox.addLayout(grid)
1566 vbox.addLayout(ok_cancel_buttons(d))
1569 run_hook('password_dialog', pw, grid, 1)
1570 if not d.exec_(): return
1571 return unicode(pw.text())
1580 def tx_from_text(self, txt):
1581 "json or raw hexadecimal"
1584 tx = Transaction(txt)
1590 tx_dict = json.loads(str(txt))
1591 assert "hex" in tx_dict.keys()
1592 assert "complete" in tx_dict.keys()
1593 if not tx_dict["complete"]:
1594 assert "input_info" in tx_dict.keys()
1595 tx = Transaction(tx_dict["hex"])
1600 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1604 def read_tx_from_file(self):
1605 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1609 with open(fileName, "r") as f:
1610 file_content = f.read()
1611 except (ValueError, IOError, os.error), reason:
1612 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1614 return self.tx_from_text(file_content)
1618 def sign_raw_transaction(self, tx, input_info, password):
1619 self.wallet.signrawtransaction(tx, input_info, [], password)
1621 def do_process_from_text(self):
1622 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1625 tx = self.tx_from_text(text)
1627 self.show_transaction(tx)
1629 def do_process_from_file(self):
1630 tx = self.read_tx_from_file()
1632 self.show_transaction(tx)
1634 def do_process_from_csvReader(self, csvReader):
1637 for row in csvReader:
1639 amount = float(row[1])
1640 amount = int(100000000*amount)
1641 outputs.append((address, amount))
1642 except (ValueError, IOError, os.error), reason:
1643 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1647 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1648 except BaseException, e:
1649 self.show_message(str(e))
1652 self.show_transaction(tx)
1654 def do_process_from_csv_file(self):
1655 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1659 with open(fileName, "r") as f:
1660 csvReader = csv.reader(f)
1661 self.do_process_from_csvReader(csvReader)
1662 except (ValueError, IOError, os.error), reason:
1663 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1666 def do_process_from_csv_text(self):
1667 text = text_dialog(self, _('Input CSV'), _("CSV:"), _("Load CSV"))
1670 f = StringIO.StringIO(text)
1671 csvReader = csv.reader(f)
1672 self.do_process_from_csvReader(csvReader)
1677 def do_export_privkeys(self, password):
1678 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.")))
1681 select_export = _('Select file to export your private keys to')
1682 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1684 with open(fileName, "w+") as csvfile:
1685 transaction = csv.writer(csvfile)
1686 transaction.writerow(["address", "private_key"])
1688 addresses = self.wallet.addresses(True)
1690 for addr in addresses:
1691 pk = "".join(self.wallet.get_private_key(addr, password))
1692 transaction.writerow(["%34s"%addr,pk])
1694 self.show_message(_("Private keys exported."))
1696 except (IOError, os.error), reason:
1697 export_error_label = _("Electrum was unable to produce a private key-export.")
1698 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1700 except BaseException, e:
1701 self.show_message(str(e))
1705 def do_import_labels(self):
1706 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1707 if not labelsFile: return
1709 f = open(labelsFile, 'r')
1712 for key, value in json.loads(data).items():
1713 self.wallet.set_label(key, value)
1714 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1715 except (IOError, os.error), reason:
1716 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1719 def do_export_labels(self):
1720 labels = self.wallet.labels
1722 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1724 with open(fileName, 'w+') as f:
1725 json.dump(labels, f)
1726 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1727 except (IOError, os.error), reason:
1728 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1731 def do_export_history(self):
1732 from lite_window import csv_transaction
1733 csv_transaction(self.wallet)
1737 def do_import_privkey(self, password):
1738 if not self.wallet.imported_keys:
1739 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1740 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1741 + _('Are you sure you understand what you are doing?'), 3, 4)
1744 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1747 text = str(text).split()
1752 addr = self.wallet.import_key(key, password)
1753 except BaseException as e:
1759 addrlist.append(addr)
1761 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1763 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1764 self.update_receive_tab()
1765 self.update_history_tab()
1768 def settings_dialog(self):
1770 d.setWindowTitle(_('Electrum Settings'))
1772 vbox = QVBoxLayout()
1774 tabs = QTabWidget(self)
1775 self.settings_tab = tabs
1776 vbox.addWidget(tabs)
1779 grid_ui = QGridLayout(tab1)
1780 grid_ui.setColumnStretch(0,1)
1781 tabs.addTab(tab1, _('Display') )
1783 nz_label = QLabel(_('Display zeros'))
1784 grid_ui.addWidget(nz_label, 0, 0)
1785 nz_e = AmountEdit(None,True)
1786 nz_e.setText("%d"% self.num_zeros)
1787 grid_ui.addWidget(nz_e, 0, 1)
1788 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1789 grid_ui.addWidget(HelpButton(msg), 0, 2)
1790 if not self.config.is_modifiable('num_zeros'):
1791 for w in [nz_e, nz_label]: w.setEnabled(False)
1793 lang_label=QLabel(_('Language') + ':')
1794 grid_ui.addWidget(lang_label, 1, 0)
1795 lang_combo = QComboBox()
1796 from electrum.i18n import languages
1797 lang_combo.addItems(languages.values())
1799 index = languages.keys().index(self.config.get("language",''))
1802 lang_combo.setCurrentIndex(index)
1803 grid_ui.addWidget(lang_combo, 1, 1)
1804 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1805 if not self.config.is_modifiable('language'):
1806 for w in [lang_combo, lang_label]: w.setEnabled(False)
1808 expert_cb = QCheckBox(_('Expert mode'))
1809 expert_cb.setChecked(self.expert_mode)
1810 grid_ui.addWidget(expert_cb, 3, 0)
1811 hh = _('In expert mode, your client will:') + '\n' \
1812 + _(' - Show change addresses in the Receive tab') + '\n' \
1813 + _(' - Display the balance of each address') + '\n' \
1814 + _(' - Add freeze/prioritize actions to addresses.')
1815 grid_ui.addWidget(HelpButton(hh), 3, 2)
1816 grid_ui.setRowStretch(4,1)
1820 grid_wallet = QGridLayout(tab2)
1821 grid_wallet.setColumnStretch(0,1)
1822 tabs.addTab(tab2, _('Wallet') )
1824 fee_label = QLabel(_('Transaction fee'))
1825 grid_wallet.addWidget(fee_label, 0, 0)
1826 fee_e = AmountEdit(self.base_unit)
1827 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1828 grid_wallet.addWidget(fee_e, 0, 2)
1829 msg = _('Fee per kilobyte of transaction.') + ' ' \
1830 + _('Recommended value') + ': ' + self.format_amount(50000)
1831 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1832 if not self.config.is_modifiable('fee_per_kb'):
1833 for w in [fee_e, fee_label]: w.setEnabled(False)
1835 usechange_cb = QCheckBox(_('Use change addresses'))
1836 usechange_cb.setChecked(self.wallet.use_change)
1837 grid_wallet.addWidget(usechange_cb, 1, 0)
1838 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1839 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1841 units = ['BTC', 'mBTC']
1842 unit_label = QLabel(_('Base unit'))
1843 grid_wallet.addWidget(unit_label, 3, 0)
1844 unit_combo = QComboBox()
1845 unit_combo.addItems(units)
1846 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1847 grid_wallet.addWidget(unit_combo, 3, 2)
1848 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1849 + '\n1BTC=1000mBTC.\n' \
1850 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1851 grid_wallet.setRowStretch(4,1)
1854 run_hook('create_settings_tab', tabs)
1856 vbox.addLayout(ok_cancel_buttons(d))
1860 if not d.exec_(): return
1862 fee = unicode(fee_e.text())
1864 fee = self.read_amount(fee)
1866 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1869 self.wallet.set_fee(fee)
1871 nz = unicode(nz_e.text())
1876 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1879 if self.num_zeros != nz:
1881 self.config.set_key('num_zeros', nz, True)
1882 self.update_history_tab()
1883 self.update_receive_tab()
1885 usechange_result = usechange_cb.isChecked()
1886 if self.wallet.use_change != usechange_result:
1887 self.wallet.use_change = usechange_result
1888 self.config.set_key('use_change', self.wallet.use_change, True)
1890 unit_result = units[unit_combo.currentIndex()]
1891 if self.base_unit() != unit_result:
1892 self.decimal_point = 8 if unit_result == 'BTC' else 5
1893 self.config.set_key('decimal_point', self.decimal_point, True)
1894 self.update_history_tab()
1895 self.update_status()
1897 need_restart = False
1899 lang_request = languages.keys()[lang_combo.currentIndex()]
1900 if lang_request != self.config.get('language'):
1901 self.config.set_key("language", lang_request, True)
1905 run_hook('close_settings_dialog')
1908 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1910 self.receive_tab_set_mode(expert_cb.isChecked())
1912 def run_network_dialog(self):
1913 NetworkDialog(self.wallet.network, self.config, self).do_exec()
1915 def closeEvent(self, event):
1917 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1918 self.save_column_widths()
1919 self.config.set_key("console-history", self.console.history[-50:], True)
1924 def plugins_dialog(self):
1925 from electrum.plugins import plugins
1928 d.setWindowTitle(_('Electrum Plugins'))
1931 vbox = QVBoxLayout(d)
1934 scroll = QScrollArea()
1935 scroll.setEnabled(True)
1936 scroll.setWidgetResizable(True)
1937 scroll.setMinimumSize(400,250)
1938 vbox.addWidget(scroll)
1942 w.setMinimumHeight(len(plugins)*35)
1944 grid = QGridLayout()
1945 grid.setColumnStretch(0,1)
1948 def mk_toggle(cb, p):
1949 return lambda: cb.setChecked(p.toggle())
1950 for i, p in enumerate(plugins):
1952 cb = QCheckBox(p.fullname())
1953 cb.setDisabled(not p.is_available())
1954 cb.setChecked(p.is_enabled())
1955 cb.clicked.connect(mk_toggle(cb,p))
1956 grid.addWidget(cb, i, 0)
1957 if p.requires_settings():
1958 grid.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
1959 grid.addWidget(HelpButton(p.description()), i, 2)
1961 print_msg(_("Error: cannot display plugin"), p)
1962 traceback.print_exc(file=sys.stdout)
1963 grid.setRowStretch(i+1,1)
1965 vbox.addLayout(close_button(d))