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
41 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
43 from electrum.wallet import format_satoshis
44 from electrum import Transaction
45 from electrum import mnemonic
46 from electrum import util, bitcoin, commands, Interface, Wallet
47 from electrum import SimpleConfig, Wallet, WalletStorage
50 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
78 class MyTreeWidget(QTreeWidget):
79 def __init__(self, parent):
80 QTreeWidget.__init__(self, parent)
83 for i in range(0,self.viewport().height()/5):
84 if self.itemAt(QPoint(0,i*5)) == item:
89 if self.itemAt(QPoint(0,i*5 + j)) != item:
91 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
93 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
98 class StatusBarButton(QPushButton):
99 def __init__(self, icon, tooltip, func):
100 QPushButton.__init__(self, icon, '')
101 self.setToolTip(tooltip)
103 self.setMaximumWidth(25)
104 self.clicked.connect(func)
106 self.setIconSize(QSize(25,25))
108 def keyPressEvent(self, e):
109 if e.key() == QtCore.Qt.Key_Return:
121 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
123 class ElectrumWindow(QMainWindow):
124 def changeEvent(self, event):
125 flags = self.windowFlags();
126 if event and event.type() == QtCore.QEvent.WindowStateChange:
127 if self.windowState() & QtCore.Qt.WindowMinimized:
128 self.build_menu(True)
129 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
130 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
131 # Electrum from closing.
132 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
133 # self.setWindowFlags(flags & ~Qt.ToolTip)
134 elif event.oldState() & QtCore.Qt.WindowMinimized:
135 self.build_menu(False)
136 #self.setWindowFlags(flags | Qt.ToolTip)
138 def build_menu(self, is_hidden = False):
140 if self.isMinimized():
141 m.addAction(_("Show"), self.showNormal)
143 m.addAction(_("Hide"), self.showMinimized)
146 m.addAction(_("Exit Electrum"), self.close)
147 self.tray.setContextMenu(m)
149 def tray_activated(self, reason):
150 if reason == QSystemTrayIcon.DoubleClick:
154 def __init__(self, config, network, go_lite):
155 QMainWindow.__init__(self)
158 self.network = network
159 self.go_lite = go_lite
162 self._close_electrum = False
164 self.current_account = self.config.get("current_account", None)
166 self.icon = QIcon(':icons/electrum.png')
167 self.tray = QSystemTrayIcon(self.icon, self)
168 self.tray.setToolTip('Electrum')
169 self.tray.activated.connect(self.tray_activated)
173 self.create_status_bar()
175 self.need_update = threading.Event()
177 self.expert_mode = config.get('classic_expert_mode', False)
178 self.decimal_point = config.get('decimal_point', 8)
179 self.num_zeros = int(config.get('num_zeros',0))
181 set_language(config.get('language'))
183 self.funds_error = False
184 self.completions = QStringListModel()
186 self.tabs = tabs = QTabWidget(self)
187 self.column_widths = self.config.get("column_widths", default_column_widths )
188 tabs.addTab(self.create_history_tab(), _('History') )
189 tabs.addTab(self.create_send_tab(), _('Send') )
190 tabs.addTab(self.create_receive_tab(), _('Receive') )
191 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
192 tabs.addTab(self.create_console_tab(), _('Console') )
193 tabs.setMinimumSize(600, 400)
194 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
195 self.setCentralWidget(tabs)
197 g = self.config.get("winpos-qt",[100, 100, 840, 400])
198 self.setGeometry(g[0], g[1], g[2], g[3])
202 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
203 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
204 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
205 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
206 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
208 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
209 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
210 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
212 self.history_list.setFocus(True)
214 self.exchanger = exchange_rate.Exchanger(self)
215 self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
217 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
218 if platform.system() == 'Windows':
219 n = 3 if self.wallet.seed else 2
220 tabs.setCurrentIndex (n)
221 tabs.setCurrentIndex (0)
223 # plugins that need to change the GUI do it here
224 self.run_hook('init')
229 def load_wallet(self, wallet):
233 self.network.register_callback('updated', lambda: self.need_update.set())
234 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
235 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
236 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
237 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
238 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
239 if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
240 self.setWindowTitle( title )
242 # set initial message
243 self.console.showMessage(self.network.banner)
244 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
245 self.notify_transactions()
248 accounts = self.wallet.get_account_names()
249 self.account_selector.clear()
250 if len(accounts) > 1:
251 self.account_selector.addItems([_("All accounts")] + accounts.values())
252 self.account_selector.setCurrentIndex(0)
253 self.account_selector.show()
255 self.account_selector.hide()
257 self.new_account.setEnabled(self.wallet.seed_version>4)
259 self.update_lock_icon()
260 self.update_buttons_on_seed()
261 self.update_console()
263 self.run_hook('load_wallet')
266 def select_wallet_file(self):
267 wallet_folder = self.wallet.storage.path
268 re.sub("(\/\w*.dat)$", "", wallet_folder)
269 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
273 def open_wallet(self):
275 filename = self.select_wallet_file()
279 storage = WalletStorage({'wallet_path': filename})
280 if not storage.file_exists:
281 self.show_message("file not found "+ filename)
284 self.wallet.stop_threads()
287 wallet = Wallet(storage)
288 wallet.start_threads(self.network)
290 self.load_wallet(wallet)
294 def backup_wallet(self):
296 path = self.wallet.storage.path
297 wallet_folder = os.path.dirname(path)
298 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
299 new_filename = unicode(new_filename)
300 if not ok or not new_filename:
303 new_path = os.path.join(wallet_folder, new_filename)
306 shutil.copy2(path, new_path)
307 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
308 except (IOError, os.error), reason:
309 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
312 def new_wallet(self):
315 wallet_folder = os.path.dirname(self.wallet.storage.path)
316 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
317 filename = unicode(filename)
318 if not ok or not filename:
320 filename = os.path.join(wallet_folder, filename)
322 storage = WalletStorage({'wallet_path': filename})
323 assert not storage.file_exists
325 wizard = installwizard.InstallWizard(self.config, self.network, storage)
326 wallet = wizard.run()
328 self.load_wallet(wallet)
332 def init_menubar(self):
335 file_menu = menubar.addMenu(_("&File"))
336 open_wallet_action = file_menu.addAction(_("&Open"))
337 open_wallet_action.triggered.connect(self.open_wallet)
339 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
340 new_wallet_action.triggered.connect(self.new_wallet)
342 wallet_backup = file_menu.addAction(_("&Copy"))
343 wallet_backup.triggered.connect(self.backup_wallet)
345 quit_item = file_menu.addAction(_("&Close"))
346 quit_item.triggered.connect(self.close)
348 wallet_menu = menubar.addMenu(_("&Wallet"))
350 # Settings / Preferences are all reserved keywords in OSX using this as work around
351 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
352 preferences_menu = wallet_menu.addAction(preferences_name)
353 preferences_menu.triggered.connect(self.settings_dialog)
355 wallet_menu.addSeparator()
357 raw_transaction_menu = wallet_menu.addMenu(_("&Load raw transaction"))
359 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
360 raw_transaction_file.triggered.connect(self.do_process_from_file)
362 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
363 raw_transaction_text.triggered.connect(self.do_process_from_text)
365 csv_transaction_menu = wallet_menu.addMenu(_("&Create transaction"))
367 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
368 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
370 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
371 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
373 wallet_menu.addSeparator()
375 show_menu = wallet_menu.addMenu(_("Show"))
377 #if self.wallet.seed:
378 show_seed = show_menu.addAction(_("&Seed"))
379 show_seed.triggered.connect(self.show_seed_dialog)
381 show_mpk = show_menu.addAction(_("&Master Public Key"))
382 show_mpk.triggered.connect(self.show_master_public_key)
384 wallet_menu.addSeparator()
385 new_contact = wallet_menu.addAction(_("&New contact"))
386 new_contact.triggered.connect(self.new_contact_dialog)
388 self.new_account = wallet_menu.addAction(_("&New account"))
389 self.new_account.triggered.connect(self.new_account_dialog)
391 import_menu = menubar.addMenu(_("&Import"))
392 in_labels = import_menu.addAction(_("&Labels"))
393 in_labels.triggered.connect(self.do_import_labels)
395 in_private_keys = import_menu.addAction(_("&Private keys"))
396 in_private_keys.triggered.connect(self.do_import_privkey)
398 export_menu = menubar.addMenu(_("&Export"))
399 ex_private_keys = export_menu.addAction(_("&Private keys"))
400 ex_private_keys.triggered.connect(self.do_export_privkeys)
402 ex_history = export_menu.addAction(_("&History"))
403 ex_history.triggered.connect(self.do_export_history)
405 ex_labels = export_menu.addAction(_("&Labels"))
406 ex_labels.triggered.connect(self.do_export_labels)
408 help_menu = menubar.addMenu(_("&Help"))
409 show_about = help_menu.addAction(_("&About"))
410 show_about.triggered.connect(self.show_about)
411 web_open = help_menu.addAction(_("&Official website"))
412 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
414 help_menu.addSeparator()
415 doc_open = help_menu.addAction(_("&Documentation"))
416 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
417 report_bug = help_menu.addAction(_("&Report Bug"))
418 report_bug.triggered.connect(self.show_report_bug)
420 self.setMenuBar(menubar)
422 def show_about(self):
423 QMessageBox.about(self, "Electrum",
424 _("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."))
426 def show_report_bug(self):
427 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
428 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
431 def notify_transactions(self):
432 print_error("Notifying GUI")
433 if len(self.network.interface.pending_transactions_for_notifications) > 0:
434 # Combine the transactions if there are more then three
435 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
438 for tx in self.network.interface.pending_transactions_for_notifications:
439 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
443 self.notify("%s new transactions received. Total amount received in the new transactions %s %s" \
444 % (tx_amount, self.format_amount(total_amount), self.base_unit()))
446 self.network.interface.pending_transactions_for_notifications = []
448 for tx in self.network.interface.pending_transactions_for_notifications:
450 self.network.interface.pending_transactions_for_notifications.remove(tx)
451 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
453 self.notify("New transaction received. %s %s" % (self.format_amount(v), self.base_unit()))
455 def notify(self, message):
456 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
459 def init_plugins(self):
460 import imp, pkgutil, __builtin__
461 if __builtin__.use_local_modules:
462 fp, pathname, description = imp.find_module('plugins')
463 plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])]
464 plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names)
465 imp.load_module('electrum_plugins', fp, pathname, description)
466 plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names)
468 import electrum_plugins
469 plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)]
470 plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names]
473 for name, p in zip(plugin_names, plugins):
475 self.plugins.append( p.Plugin(self, name) )
477 print_msg("Error:cannot initialize plugin",p)
478 traceback.print_exc(file=sys.stdout)
481 def run_hook(self, name, *args):
482 for p in self.plugins:
483 if not p.is_enabled():
492 print_error("Plugin error")
493 traceback.print_exc(file=sys.stdout)
499 def set_label(self, name, text = None):
501 old_text = self.wallet.labels.get(name)
504 self.wallet.labels[name] = text
505 self.wallet.storage.put('labels', self.wallet.labels)
509 self.wallet.labels.pop(name)
511 self.run_hook('set_label', name, text, changed)
515 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
516 def getOpenFileName(self, title, filter = ""):
517 directory = self.config.get('io_dir', os.path.expanduser('~'))
518 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
519 if fileName and directory != os.path.dirname(fileName):
520 self.config.set_key('io_dir', os.path.dirname(fileName), True)
523 def getSaveFileName(self, title, filename, filter = ""):
524 directory = self.config.get('io_dir', os.path.expanduser('~'))
525 path = os.path.join( directory, filename )
526 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
527 if fileName and directory != os.path.dirname(fileName):
528 self.config.set_key('io_dir', os.path.dirname(fileName), True)
532 QMainWindow.close(self)
533 self.run_hook('close_main_window')
535 def connect_slots(self, sender):
536 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
537 self.previous_payto_e=''
539 def timer_actions(self):
540 if self.need_update.is_set():
542 self.need_update.clear()
543 self.run_hook('timer_actions')
545 def format_amount(self, x, is_diff=False, whitespaces=False):
546 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
548 def read_amount(self, x):
549 if x in['.', '']: return None
550 p = pow(10, self.decimal_point)
551 return int( p * Decimal(x) )
554 assert self.decimal_point in [5,8]
555 return "BTC" if self.decimal_point == 8 else "mBTC"
557 def update_status(self):
558 if self.network.interface and self.network.interface.is_connected:
559 if not self.wallet.up_to_date:
560 text = _("Synchronizing...")
561 icon = QIcon(":icons/status_waiting.png")
563 c, u = self.wallet.get_account_balance(self.current_account)
564 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
565 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
566 text += self.create_quote_text(Decimal(c+u)/100000000)
567 self.tray.setToolTip(text)
568 icon = QIcon(":icons/status_connected.png")
570 text = _("Not connected")
571 icon = QIcon(":icons/status_disconnected.png")
573 self.balance_label.setText(text)
574 self.status_button.setIcon( icon )
576 def update_wallet(self):
578 if self.wallet.up_to_date or not self.network.interface.is_connected:
579 self.update_history_tab()
580 self.update_receive_tab()
581 self.update_contacts_tab()
582 self.update_completions()
585 def create_quote_text(self, btc_balance):
586 quote_currency = self.config.get("currency", "None")
587 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
588 if quote_balance is None:
591 quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
594 def create_history_tab(self):
595 self.history_list = l = MyTreeWidget(self)
597 for i,width in enumerate(self.column_widths['history']):
598 l.setColumnWidth(i, width)
599 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
600 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
601 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
603 l.setContextMenuPolicy(Qt.CustomContextMenu)
604 l.customContextMenuRequested.connect(self.create_history_menu)
608 def create_history_menu(self, position):
609 self.history_list.selectedIndexes()
610 item = self.history_list.currentItem()
612 tx_hash = str(item.data(0, Qt.UserRole).toString())
613 if not tx_hash: return
615 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
616 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
617 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
618 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
621 def show_transaction(self, tx):
622 import transaction_dialog
623 d = transaction_dialog.TxDialog(tx, self)
626 def tx_label_clicked(self, item, column):
627 if column==2 and item.isSelected():
629 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
630 self.history_list.editItem( item, column )
631 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
634 def tx_label_changed(self, item, column):
638 tx_hash = str(item.data(0, Qt.UserRole).toString())
639 tx = self.wallet.transactions.get(tx_hash)
640 text = unicode( item.text(2) )
641 self.set_label(tx_hash, text)
643 item.setForeground(2, QBrush(QColor('black')))
645 text = self.wallet.get_default_label(tx_hash)
646 item.setText(2, text)
647 item.setForeground(2, QBrush(QColor('gray')))
651 def edit_label(self, is_recv):
652 l = self.receive_list if is_recv else self.contacts_list
653 item = l.currentItem()
654 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
655 l.editItem( item, 1 )
656 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
660 def address_label_clicked(self, item, column, l, column_addr, column_label):
661 if column == column_label and item.isSelected():
662 is_editable = item.data(0, 32).toBool()
665 addr = unicode( item.text(column_addr) )
666 label = unicode( item.text(column_label) )
667 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
668 l.editItem( item, column )
669 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
672 def address_label_changed(self, item, column, l, column_addr, column_label):
673 if column == column_label:
674 addr = unicode( item.text(column_addr) )
675 text = unicode( item.text(column_label) )
676 is_editable = item.data(0, 32).toBool()
680 changed = self.set_label(addr, text)
682 self.update_history_tab()
683 self.update_completions()
685 self.current_item_changed(item)
687 self.run_hook('item_changed', item, column)
690 def current_item_changed(self, a):
691 self.run_hook('current_item_changed', a)
695 def update_history_tab(self):
697 self.history_list.clear()
698 for item in self.wallet.get_tx_history(self.current_account):
699 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
702 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
707 time_str = 'unverified'
708 icon = QIcon(":icons/unconfirmed.png")
711 icon = QIcon(":icons/unconfirmed.png")
713 icon = QIcon(":icons/clock%d.png"%conf)
715 icon = QIcon(":icons/confirmed.png")
717 if value is not None:
718 v_str = self.format_amount(value, True, whitespaces=True)
722 balance_str = self.format_amount(balance, whitespaces=True)
725 label, is_default_label = self.wallet.get_label(tx_hash)
727 label = _('Pruned transaction outputs')
728 is_default_label = False
730 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
731 item.setFont(2, QFont(MONOSPACE_FONT))
732 item.setFont(3, QFont(MONOSPACE_FONT))
733 item.setFont(4, QFont(MONOSPACE_FONT))
735 item.setForeground(3, QBrush(QColor("#BC1E1E")))
737 item.setData(0, Qt.UserRole, tx_hash)
738 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
740 item.setForeground(2, QBrush(QColor('grey')))
742 item.setIcon(0, icon)
743 self.history_list.insertTopLevelItem(0,item)
746 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
749 def create_send_tab(self):
754 grid.setColumnMinimumWidth(3,300)
755 grid.setColumnStretch(5,1)
758 self.payto_e = QLineEdit()
759 grid.addWidget(QLabel(_('Pay to')), 1, 0)
760 grid.addWidget(self.payto_e, 1, 1, 1, 3)
762 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)
764 completer = QCompleter()
765 completer.setCaseSensitivity(False)
766 self.payto_e.setCompleter(completer)
767 completer.setModel(self.completions)
769 self.message_e = QLineEdit()
770 grid.addWidget(QLabel(_('Description')), 2, 0)
771 grid.addWidget(self.message_e, 2, 1, 1, 3)
772 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)
774 self.amount_e = AmountEdit(self.base_unit)
775 grid.addWidget(QLabel(_('Amount')), 3, 0)
776 grid.addWidget(self.amount_e, 3, 1, 1, 2)
777 grid.addWidget(HelpButton(
778 _('Amount to be sent.') + '\n\n' \
779 + _('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.') \
780 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
782 self.fee_e = AmountEdit(self.base_unit)
783 grid.addWidget(QLabel(_('Fee')), 4, 0)
784 grid.addWidget(self.fee_e, 4, 1, 1, 2)
785 grid.addWidget(HelpButton(
786 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
787 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
788 + _('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)
791 self.send_button = EnterButton(_("Send"), self.do_send)
792 grid.addWidget(self.send_button, 6, 1)
794 b = EnterButton(_("Clear"),self.do_clear)
795 grid.addWidget(b, 6, 2)
797 self.payto_sig = QLabel('')
798 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
800 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
801 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
810 def entry_changed( is_fee ):
811 self.funds_error = False
813 if self.amount_e.is_shortcut:
814 self.amount_e.is_shortcut = False
815 c, u = self.wallet.get_account_balance(self.current_account)
816 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
817 fee = self.wallet.estimated_fee(inputs)
819 self.amount_e.setText( self.format_amount(amount) )
820 self.fee_e.setText( self.format_amount( fee ) )
823 amount = self.read_amount(str(self.amount_e.text()))
824 fee = self.read_amount(str(self.fee_e.text()))
826 if not is_fee: fee = None
829 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
831 self.fee_e.setText( self.format_amount( fee ) )
834 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
838 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
839 self.funds_error = True
840 text = _( "Not enough funds" )
841 c, u = self.wallet.get_frozen_balance()
842 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
844 self.statusBar().showMessage(text)
845 self.amount_e.setPalette(palette)
846 self.fee_e.setPalette(palette)
848 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
849 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
851 self.run_hook('create_send_tab', grid)
855 def update_completions(self):
857 for addr,label in self.wallet.labels.items():
858 if addr in self.wallet.addressbook:
859 l.append( label + ' <' + addr + '>')
861 self.run_hook('update_completions', l)
862 self.completions.setStringList(l)
866 return lambda s, *args: s.do_protect(func, args)
871 label = unicode( self.message_e.text() )
872 r = unicode( self.payto_e.text() )
875 # label or alias, with address in brackets
876 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
877 to_address = m.group(2) if m else r
879 if not is_valid(to_address):
880 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
884 amount = self.read_amount(unicode( self.amount_e.text()))
886 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
889 fee = self.read_amount(unicode( self.fee_e.text()))
891 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
894 confirm_amount = self.config.get('confirm_amount', 100000000)
895 if amount >= confirm_amount:
896 if not self.question("send %s to %s?"%(self.format_amount(amount) + ' '+ self.base_unit(), to_address)):
899 self.send_tx(to_address, amount, fee, label)
903 def send_tx(self, to_address, amount, fee, label, password):
906 tx = self.wallet.mktx( [(to_address, amount)], password, fee, account=self.current_account)
907 except BaseException, e:
908 traceback.print_exc(file=sys.stdout)
909 self.show_message(str(e))
912 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
913 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
916 self.run_hook('send_tx', tx)
919 self.set_label(tx.hash(), label)
922 h = self.wallet.send_tx(tx)
923 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
924 status, msg = self.wallet.receive_tx( h )
926 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
928 self.update_contacts_tab()
930 QMessageBox.warning(self, _('Error'), msg, _('OK'))
932 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
934 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
935 with open(fileName,'w') as f:
936 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
937 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
939 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
941 # add recipient to addressbook
942 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
943 self.wallet.addressbook.append(to_address)
948 def set_url(self, url):
949 address, amount, label, message, signature, identity, url = util.parse_url(url)
950 if self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
952 if label and self.wallet.labels.get(address) != label:
953 if self.question('Give label "%s" to address %s ?'%(label,address)):
954 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
955 self.wallet.addressbook.append(address)
956 self.set_label(address, label)
958 self.run_hook('set_url', url, self.show_message, self.question)
960 self.tabs.setCurrentIndex(1)
961 label = self.wallet.labels.get(address)
962 m_addr = label + ' <'+ address +'>' if label else address
963 self.payto_e.setText(m_addr)
965 self.message_e.setText(message)
966 self.amount_e.setText(amount)
968 self.set_frozen(self.payto_e,True)
969 self.set_frozen(self.amount_e,True)
970 self.set_frozen(self.message_e,True)
971 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
973 self.payto_sig.setVisible(False)
976 self.payto_sig.setVisible(False)
977 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
979 self.set_frozen(e,False)
982 def set_frozen(self,entry,frozen):
984 entry.setReadOnly(True)
985 entry.setFrame(False)
987 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
988 entry.setPalette(palette)
990 entry.setReadOnly(False)
993 palette.setColor(entry.backgroundRole(), QColor('white'))
994 entry.setPalette(palette)
997 def toggle_freeze(self,addr):
999 if addr in self.wallet.frozen_addresses:
1000 self.wallet.unfreeze(addr)
1002 self.wallet.freeze(addr)
1003 self.update_receive_tab()
1005 def toggle_priority(self,addr):
1007 if addr in self.wallet.prioritized_addresses:
1008 self.wallet.unprioritize(addr)
1010 self.wallet.prioritize(addr)
1011 self.update_receive_tab()
1014 def create_list_tab(self, headers):
1015 "generic tab creation method"
1016 l = MyTreeWidget(self)
1017 l.setColumnCount( len(headers) )
1018 l.setHeaderLabels( headers )
1021 vbox = QVBoxLayout()
1028 vbox.addWidget(buttons)
1030 hbox = QHBoxLayout()
1033 buttons.setLayout(hbox)
1038 def create_receive_tab(self):
1039 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1040 l.setContextMenuPolicy(Qt.CustomContextMenu)
1041 l.customContextMenuRequested.connect(self.create_receive_menu)
1042 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1043 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1044 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1045 self.receive_list = l
1046 self.receive_buttons_hbox = hbox
1051 def receive_tab_set_mode(self, i):
1052 self.save_column_widths()
1053 self.expert_mode = (i == 1)
1054 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1055 self.update_receive_tab()
1058 def save_column_widths(self):
1059 if not self.expert_mode:
1060 widths = [ self.receive_list.columnWidth(0) ]
1063 for i in range(self.receive_list.columnCount() -1):
1064 widths.append(self.receive_list.columnWidth(i))
1065 self.column_widths["receive"][self.expert_mode] = widths
1067 self.column_widths["history"] = []
1068 for i in range(self.history_list.columnCount() - 1):
1069 self.column_widths["history"].append(self.history_list.columnWidth(i))
1071 self.column_widths["contacts"] = []
1072 for i in range(self.contacts_list.columnCount() - 1):
1073 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1075 self.config.set_key("column_widths", self.column_widths, True)
1078 def create_contacts_tab(self):
1079 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1080 l.setContextMenuPolicy(Qt.CustomContextMenu)
1081 l.customContextMenuRequested.connect(self.create_contact_menu)
1082 for i,width in enumerate(self.column_widths['contacts']):
1083 l.setColumnWidth(i, width)
1085 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1086 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1087 self.contacts_list = l
1088 self.contacts_buttons_hbox = hbox
1093 def delete_imported_key(self, addr):
1094 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1095 self.wallet.delete_imported_key(addr)
1096 self.update_receive_tab()
1097 self.update_history_tab()
1100 def create_receive_menu(self, position):
1101 # fixme: this function apparently has a side effect.
1102 # if it is not called the menu pops up several times
1103 #self.receive_list.selectedIndexes()
1105 item = self.receive_list.itemAt(position)
1107 addr = unicode(item.text(0))
1108 if not is_valid(addr):
1109 item.setExpanded(not item.isExpanded())
1112 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1113 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1114 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1115 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1116 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1117 if addr in self.wallet.imported_keys:
1118 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1120 if self.expert_mode:
1121 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1122 menu.addAction(t, lambda: self.toggle_freeze(addr))
1123 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1124 menu.addAction(t, lambda: self.toggle_priority(addr))
1126 self.run_hook('receive_menu', menu)
1127 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1130 def payto(self, addr):
1132 label = self.wallet.labels.get(addr)
1133 m_addr = label + ' <' + addr + '>' if label else addr
1134 self.tabs.setCurrentIndex(1)
1135 self.payto_e.setText(m_addr)
1136 self.amount_e.setFocus()
1139 def delete_contact(self, x):
1140 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1141 self.wallet.delete_contact(x)
1142 self.set_label(x, None)
1143 self.update_history_tab()
1144 self.update_contacts_tab()
1145 self.update_completions()
1148 def create_contact_menu(self, position):
1149 item = self.contacts_list.itemAt(position)
1151 addr = unicode(item.text(0))
1152 label = unicode(item.text(1))
1153 is_editable = item.data(0,32).toBool()
1154 payto_addr = item.data(0,33).toString()
1156 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1157 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1158 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1160 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1161 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1163 self.run_hook('create_contact_menu', menu, item)
1164 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1167 def update_receive_item(self, item):
1168 item.setFont(0, QFont(MONOSPACE_FONT))
1169 address = str(item.data(0,0).toString())
1170 label = self.wallet.labels.get(address,'')
1171 item.setData(1,0,label)
1172 item.setData(0,32, True) # is editable
1174 self.run_hook('update_receive_item', address, item)
1176 c, u = self.wallet.get_addr_balance(address)
1177 balance = self.format_amount(c + u)
1178 item.setData(2,0,balance)
1180 if self.expert_mode:
1181 if address in self.wallet.frozen_addresses:
1182 item.setBackgroundColor(0, QColor('lightblue'))
1183 elif address in self.wallet.prioritized_addresses:
1184 item.setBackgroundColor(0, QColor('lightgreen'))
1187 def update_receive_tab(self):
1188 l = self.receive_list
1191 l.setColumnHidden(2, not self.expert_mode)
1192 l.setColumnHidden(3, not self.expert_mode)
1193 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1194 l.setColumnWidth(i, width)
1196 if self.current_account is None:
1197 account_items = self.wallet.accounts.items()
1198 elif self.current_account != -1:
1199 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1203 for k, account in account_items:
1204 name = self.wallet.get_account_name(k)
1205 c,u = self.wallet.get_account_balance(k)
1206 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1207 l.addTopLevelItem(account_item)
1208 account_item.setExpanded(True)
1210 for is_change in ([0,1] if self.expert_mode else [0]):
1211 if self.expert_mode:
1212 name = "Receiving" if not is_change else "Change"
1213 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1214 account_item.addChild(seq_item)
1215 if not is_change: seq_item.setExpanded(True)
1217 seq_item = account_item
1221 for address in account.get_addresses(is_change):
1222 h = self.wallet.history.get(address,[])
1226 if gap > self.wallet.gap_limit:
1231 num_tx = '*' if h == ['*'] else "%d"%len(h)
1232 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1233 self.update_receive_item(item)
1235 item.setBackgroundColor(1, QColor('red'))
1236 seq_item.addChild(item)
1239 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1240 c,u = self.wallet.get_imported_balance()
1241 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1242 l.addTopLevelItem(account_item)
1243 account_item.setExpanded(True)
1244 for address in self.wallet.imported_keys.keys():
1245 item = QTreeWidgetItem( [ address, '', '', ''] )
1246 self.update_receive_item(item)
1247 account_item.addChild(item)
1250 # we use column 1 because column 0 may be hidden
1251 l.setCurrentItem(l.topLevelItem(0),1)
1254 def update_contacts_tab(self):
1255 l = self.contacts_list
1258 for address in self.wallet.addressbook:
1259 label = self.wallet.labels.get(address,'')
1260 n = self.wallet.get_num_tx(address)
1261 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1262 item.setFont(0, QFont(MONOSPACE_FONT))
1263 # 32 = label can be edited (bool)
1264 item.setData(0,32, True)
1266 item.setData(0,33, address)
1267 l.addTopLevelItem(item)
1269 self.run_hook('update_contacts_tab', l)
1270 l.setCurrentItem(l.topLevelItem(0))
1274 def create_console_tab(self):
1275 from qt_console import Console
1276 self.console = console = Console()
1280 def update_console(self):
1281 console = self.console
1282 console.history = self.config.get("console-history",[])
1283 console.history_index = len(console.history)
1285 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1286 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1288 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1290 def mkfunc(f, method):
1291 return lambda *args: apply( f, (method, args, self.password_dialog ))
1293 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1294 methods[m] = mkfunc(c._run, m)
1296 console.updateNamespace(methods)
1299 def change_account(self,s):
1300 if s == _("All accounts"):
1301 self.current_account = None
1303 accounts = self.wallet.get_account_names()
1304 for k, v in accounts.items():
1306 self.current_account = k
1307 self.update_history_tab()
1308 self.update_status()
1309 self.update_receive_tab()
1311 def create_status_bar(self):
1314 sb.setFixedHeight(35)
1315 qtVersion = qVersion()
1317 self.balance_label = QLabel("")
1318 sb.addWidget(self.balance_label)
1320 from version_getter import UpdateLabel
1321 self.updatelabel = UpdateLabel(self.config, sb)
1323 self.account_selector = QComboBox()
1324 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1325 sb.addPermanentWidget(self.account_selector)
1327 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1328 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1330 self.lock_icon = QIcon()
1331 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1332 sb.addPermanentWidget( self.password_button )
1334 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1335 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1336 sb.addPermanentWidget( self.seed_button )
1337 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1338 sb.addPermanentWidget( self.status_button )
1340 self.run_hook('create_status_bar', (sb,))
1342 self.setStatusBar(sb)
1345 def update_lock_icon(self):
1346 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1347 self.password_button.setIcon( icon )
1350 def update_buttons_on_seed(self):
1351 if self.wallet.seed:
1352 self.seed_button.show()
1353 self.password_button.show()
1354 self.send_button.setText(_("Send"))
1356 self.password_button.hide()
1357 self.seed_button.hide()
1358 self.send_button.setText(_("Create unsigned transaction"))
1361 def change_password_dialog(self):
1362 from password_dialog import PasswordDialog
1363 d = PasswordDialog(self.wallet, self)
1365 self.update_lock_icon()
1368 def new_contact_dialog(self):
1369 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1370 address = unicode(text)
1372 if is_valid(address):
1373 self.wallet.add_contact(address)
1374 self.update_contacts_tab()
1375 self.update_history_tab()
1376 self.update_completions()
1378 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1381 def new_account_dialog(self):
1383 dialog = QDialog(self)
1385 dialog.setWindowTitle(_("New Account"))
1387 addr = self.wallet.new_account_address()
1388 vbox = QVBoxLayout()
1389 vbox.addWidget(QLabel(_("To create a new account, please send coins to the first address of that account:")))
1394 ok_button = QPushButton(_("OK"))
1395 ok_button.setDefault(True)
1396 ok_button.clicked.connect(dialog.accept)
1398 hbox = QHBoxLayout()
1400 hbox.addWidget(ok_button)
1401 vbox.addLayout(hbox)
1403 dialog.setLayout(vbox)
1408 def show_master_public_key(self):
1409 dialog = QDialog(self)
1411 dialog.setWindowTitle(_("Master Public Key"))
1413 main_text = QTextEdit()
1414 main_text.setText(self.wallet.get_master_public_key())
1415 main_text.setReadOnly(True)
1416 main_text.setMaximumHeight(170)
1417 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1419 ok_button = QPushButton(_("OK"))
1420 ok_button.setDefault(True)
1421 ok_button.clicked.connect(dialog.accept)
1423 main_layout = QGridLayout()
1424 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1426 main_layout.addWidget(main_text, 1, 0)
1427 main_layout.addWidget(qrw, 1, 1 )
1429 vbox = QVBoxLayout()
1430 vbox.addLayout(main_layout)
1431 hbox = QHBoxLayout()
1433 hbox.addWidget(ok_button)
1434 vbox.addLayout(hbox)
1436 dialog.setLayout(vbox)
1441 def show_seed_dialog(self, password):
1442 if not self.wallet.seed:
1443 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1446 seed = self.wallet.decode_seed(password)
1448 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1451 from seed_dialog import SeedDialog
1452 d = SeedDialog(self)
1453 d.show_seed(seed, self.wallet.imported_keys)
1457 def show_qrcode(self, data, title = "QR code"):
1461 d.setWindowTitle(title)
1462 d.setMinimumSize(270, 300)
1463 vbox = QVBoxLayout()
1464 qrw = QRCodeWidget(data)
1465 vbox.addWidget(qrw, 1)
1466 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1467 hbox = QHBoxLayout()
1471 filename = "qrcode.bmp"
1472 bmp.save_qrcode(qrw.qr, filename)
1473 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1475 b = QPushButton(_("Save"))
1477 b.clicked.connect(print_qr)
1479 b = QPushButton(_("Close"))
1481 b.clicked.connect(d.accept)
1484 vbox.addLayout(hbox)
1489 def do_protect(self, func, args):
1490 if self.wallet.use_encryption:
1491 password = self.password_dialog()
1497 if args != (False,):
1498 args = (self,) + args + (password,)
1500 args = (self,password)
1505 def show_private_key(self, address, password):
1506 if not address: return
1508 pk_list = self.wallet.get_private_key(address, password)
1509 except BaseException, e:
1510 self.show_message(str(e))
1512 QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1516 def do_sign(self, address, message, signature, password):
1518 sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1519 signature.setText(sig)
1520 except BaseException, e:
1521 self.show_message(str(e))
1523 def sign_message(self, address):
1524 if not address: return
1527 d.setWindowTitle(_('Sign Message'))
1528 d.setMinimumSize(410, 290)
1530 tab_widget = QTabWidget()
1532 layout = QGridLayout(tab)
1534 sign_address = QLineEdit()
1536 sign_address.setText(address)
1537 layout.addWidget(QLabel(_('Address')), 1, 0)
1538 layout.addWidget(sign_address, 1, 1)
1540 sign_message = QTextEdit()
1541 layout.addWidget(QLabel(_('Message')), 2, 0)
1542 layout.addWidget(sign_message, 2, 1)
1543 layout.setRowStretch(2,3)
1545 sign_signature = QTextEdit()
1546 layout.addWidget(QLabel(_('Signature')), 3, 0)
1547 layout.addWidget(sign_signature, 3, 1)
1548 layout.setRowStretch(3,1)
1551 hbox = QHBoxLayout()
1552 b = QPushButton(_("Sign"))
1554 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1555 b = QPushButton(_("Close"))
1556 b.clicked.connect(d.accept)
1558 layout.addLayout(hbox, 4, 1)
1559 tab_widget.addTab(tab, _("Sign"))
1563 layout = QGridLayout(tab)
1565 verify_address = QLineEdit()
1566 layout.addWidget(QLabel(_('Address')), 1, 0)
1567 layout.addWidget(verify_address, 1, 1)
1569 verify_message = QTextEdit()
1570 layout.addWidget(QLabel(_('Message')), 2, 0)
1571 layout.addWidget(verify_message, 2, 1)
1572 layout.setRowStretch(2,3)
1574 verify_signature = QTextEdit()
1575 layout.addWidget(QLabel(_('Signature')), 3, 0)
1576 layout.addWidget(verify_signature, 3, 1)
1577 layout.setRowStretch(3,1)
1580 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText())):
1581 self.show_message(_("Signature verified"))
1583 self.show_message(_("Error: wrong signature"))
1585 hbox = QHBoxLayout()
1586 b = QPushButton(_("Verify"))
1587 b.clicked.connect(do_verify)
1589 b = QPushButton(_("Close"))
1590 b.clicked.connect(d.accept)
1592 layout.addLayout(hbox, 4, 1)
1593 tab_widget.addTab(tab, _("Verify"))
1595 vbox = QVBoxLayout()
1596 vbox.addWidget(tab_widget)
1603 def question(self, msg):
1604 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1606 def show_message(self, msg):
1607 QMessageBox.information(self, _('Message'), msg, _('OK'))
1609 def password_dialog(self ):
1616 vbox = QVBoxLayout()
1617 msg = _('Please enter your password')
1618 vbox.addWidget(QLabel(msg))
1620 grid = QGridLayout()
1622 grid.addWidget(QLabel(_('Password')), 1, 0)
1623 grid.addWidget(pw, 1, 1)
1624 vbox.addLayout(grid)
1626 vbox.addLayout(ok_cancel_buttons(d))
1629 self.run_hook('password_dialog', pw, grid, 1)
1630 if not d.exec_(): return
1631 return unicode(pw.text())
1640 def tx_dict_from_text(self, txt):
1642 tx_dict = json.loads(str(txt))
1643 assert "hex" in tx_dict.keys()
1644 assert "complete" in tx_dict.keys()
1645 if not tx_dict["complete"]:
1646 assert "input_info" in tx_dict.keys()
1648 QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1653 def read_tx_from_file(self):
1654 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1658 with open(fileName, "r") as f:
1659 file_content = f.read()
1660 except (ValueError, IOError, os.error), reason:
1661 QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1663 return self.tx_dict_from_text(file_content)
1667 def sign_raw_transaction(self, tx, input_info, password):
1668 self.wallet.signrawtransaction(tx, input_info, [], password)
1670 def do_process_from_text(self):
1671 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1674 tx_dict = self.tx_dict_from_text(text)
1676 self.create_process_transaction_window(tx_dict)
1678 def do_process_from_file(self):
1679 tx_dict = self.read_tx_from_file()
1681 self.create_process_transaction_window(tx_dict)
1683 def do_process_from_csvReader(self, csvReader):
1686 for row in csvReader:
1688 amount = float(row[1])
1689 amount = int(100000000*amount)
1690 outputs.append((address, amount))
1691 except (ValueError, IOError, os.error), reason:
1692 QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1696 tx = self.wallet.make_unsigned_transaction(outputs, None, None, account=self.current_account)
1697 except BaseException, e:
1698 self.show_message(str(e))
1701 self.show_transaction(tx)
1702 #tx_dict = tx.as_dict()
1703 #self.create_process_transaction_window(tx_dict)
1705 def do_process_from_csv_file(self):
1706 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1710 with open(fileName, "r") as f:
1711 csvReader = csv.reader(f)
1712 self.do_process_from_csvReader(csvReader)
1713 except (ValueError, IOError, os.error), reason:
1714 QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1717 def do_process_from_csv_text(self):
1718 text = text_dialog(self, _('Input CSV'), _("CSV:"), _("Load CSV"))
1721 f = StringIO.StringIO(text)
1722 csvReader = csv.reader(f)
1723 self.do_process_from_csvReader(csvReader)
1726 def create_process_transaction_window(self, tx_dict):
1727 tx = Transaction(tx_dict["hex"])
1728 self.show_transaction(tx)
1732 def do_export_privkeys(self, password):
1733 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.")))
1736 select_export = _('Select file to export your private keys to')
1737 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1739 with open(fileName, "w+") as csvfile:
1740 transaction = csv.writer(csvfile)
1741 transaction.writerow(["address", "private_key"])
1743 addresses = self.wallet.addresses(True)
1745 for addr in addresses:
1746 pk = "".join(self.wallet.get_private_key(addr, password))
1747 transaction.writerow(["%34s"%addr,pk])
1749 self.show_message(_("Private keys exported."))
1751 except (IOError, os.error), reason:
1752 export_error_label = _("Electrum was unable to produce a private key-export.")
1753 QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1755 except BaseException, e:
1756 self.show_message(str(e))
1760 def do_import_labels(self):
1761 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1762 if not labelsFile: return
1764 f = open(labelsFile, 'r')
1767 for key, value in json.loads(data).items():
1768 self.wallet.set_label(key, value)
1769 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1770 except (IOError, os.error), reason:
1771 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1774 def do_export_labels(self):
1775 labels = self.wallet.labels
1777 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1779 with open(fileName, 'w+') as f:
1780 json.dump(labels, f)
1781 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1782 except (IOError, os.error), reason:
1783 QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1786 def do_export_history(self):
1787 from lite_window import csv_transaction
1788 csv_transaction(self.wallet)
1792 def do_import_privkey(self, password):
1793 if not self.wallet.imported_keys:
1794 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1795 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1796 + _('Are you sure you understand what you are doing?'), 3, 4)
1799 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1802 text = str(text).split()
1807 addr = self.wallet.import_key(key, password)
1808 except BaseException as e:
1814 addrlist.append(addr)
1816 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1818 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1819 self.update_receive_tab()
1820 self.update_history_tab()
1823 def settings_dialog(self):
1825 d.setWindowTitle(_('Electrum Settings'))
1827 vbox = QVBoxLayout()
1829 tabs = QTabWidget(self)
1830 self.settings_tab = tabs
1831 vbox.addWidget(tabs)
1834 grid_ui = QGridLayout(tab1)
1835 grid_ui.setColumnStretch(0,1)
1836 tabs.addTab(tab1, _('Display') )
1838 nz_label = QLabel(_('Display zeros'))
1839 grid_ui.addWidget(nz_label, 0, 0)
1840 nz_e = AmountEdit(None,True)
1841 nz_e.setText("%d"% self.num_zeros)
1842 grid_ui.addWidget(nz_e, 0, 1)
1843 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1844 grid_ui.addWidget(HelpButton(msg), 0, 2)
1845 if not self.config.is_modifiable('num_zeros'):
1846 for w in [nz_e, nz_label]: w.setEnabled(False)
1848 lang_label=QLabel(_('Language') + ':')
1849 grid_ui.addWidget(lang_label, 1, 0)
1850 lang_combo = QComboBox()
1851 from electrum.i18n import languages
1852 lang_combo.addItems(languages.values())
1854 index = languages.keys().index(self.config.get("language",''))
1857 lang_combo.setCurrentIndex(index)
1858 grid_ui.addWidget(lang_combo, 1, 1)
1859 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1860 if not self.config.is_modifiable('language'):
1861 for w in [lang_combo, lang_label]: w.setEnabled(False)
1863 currencies = self.exchanger.get_currencies()
1864 currencies.insert(0, "None")
1866 cur_label=QLabel(_('Currency') + ':')
1867 grid_ui.addWidget(cur_label , 2, 0)
1868 cur_combo = QComboBox()
1869 cur_combo.addItems(currencies)
1871 index = currencies.index(self.config.get('currency', "None"))
1874 cur_combo.setCurrentIndex(index)
1875 grid_ui.addWidget(cur_combo, 2, 1)
1876 grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1878 expert_cb = QCheckBox(_('Expert mode'))
1879 expert_cb.setChecked(self.expert_mode)
1880 grid_ui.addWidget(expert_cb, 3, 0)
1881 hh = _('In expert mode, your client will:') + '\n' \
1882 + _(' - Show change addresses in the Receive tab') + '\n' \
1883 + _(' - Display the balance of each address') + '\n' \
1884 + _(' - Add freeze/prioritize actions to addresses.')
1885 grid_ui.addWidget(HelpButton(hh), 3, 2)
1886 grid_ui.setRowStretch(4,1)
1890 grid_wallet = QGridLayout(tab2)
1891 grid_wallet.setColumnStretch(0,1)
1892 tabs.addTab(tab2, _('Wallet') )
1894 fee_label = QLabel(_('Transaction fee'))
1895 grid_wallet.addWidget(fee_label, 0, 0)
1896 fee_e = AmountEdit(self.base_unit)
1897 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1898 grid_wallet.addWidget(fee_e, 0, 2)
1899 msg = _('Fee per kilobyte of transaction.') + ' ' \
1900 + _('Recommended value') + ': ' + self.format_amount(50000)
1901 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1902 if not self.config.is_modifiable('fee_per_kb'):
1903 for w in [fee_e, fee_label]: w.setEnabled(False)
1905 usechange_cb = QCheckBox(_('Use change addresses'))
1906 usechange_cb.setChecked(self.wallet.use_change)
1907 grid_wallet.addWidget(usechange_cb, 1, 0)
1908 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1909 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1911 units = ['BTC', 'mBTC']
1912 unit_label = QLabel(_('Base unit'))
1913 grid_wallet.addWidget(unit_label, 3, 0)
1914 unit_combo = QComboBox()
1915 unit_combo.addItems(units)
1916 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1917 grid_wallet.addWidget(unit_combo, 3, 2)
1918 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1919 + '\n1BTC=1000mBTC.\n' \
1920 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1921 grid_wallet.setRowStretch(4,1)
1925 tab5 = QScrollArea()
1926 tab5.setEnabled(True)
1927 tab5.setWidgetResizable(True)
1929 grid_plugins = QGridLayout()
1930 grid_plugins.setColumnStretch(0,1)
1933 w.setLayout(grid_plugins)
1936 w.setMinimumHeight(len(self.plugins)*35)
1938 tabs.addTab(tab5, _('Plugins') )
1939 def mk_toggle(cb, p):
1940 return lambda: cb.setChecked(p.toggle())
1941 for i, p in enumerate(self.plugins):
1943 cb = QCheckBox(p.fullname())
1944 cb.setDisabled(not p.is_available())
1945 cb.setChecked(p.is_enabled())
1946 cb.clicked.connect(mk_toggle(cb,p))
1947 grid_plugins.addWidget(cb, i, 0)
1948 if p.requires_settings():
1949 grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
1950 grid_plugins.addWidget(HelpButton(p.description()), i, 2)
1952 print_msg("Error: cannot display plugin", p)
1953 traceback.print_exc(file=sys.stdout)
1954 grid_plugins.setRowStretch(i+1,1)
1956 self.run_hook('create_settings_tab', tabs)
1958 vbox.addLayout(ok_cancel_buttons(d))
1962 if not d.exec_(): return
1964 fee = unicode(fee_e.text())
1966 fee = self.read_amount(fee)
1968 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1971 self.wallet.set_fee(fee)
1973 nz = unicode(nz_e.text())
1978 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1981 if self.num_zeros != nz:
1983 self.config.set_key('num_zeros', nz, True)
1984 self.update_history_tab()
1985 self.update_receive_tab()
1987 usechange_result = usechange_cb.isChecked()
1988 if self.wallet.use_change != usechange_result:
1989 self.wallet.use_change = usechange_result
1990 self.config.set_key('use_change', self.wallet.use_change, True)
1992 unit_result = units[unit_combo.currentIndex()]
1993 if self.base_unit() != unit_result:
1994 self.decimal_point = 8 if unit_result == 'BTC' else 5
1995 self.config.set_key('decimal_point', self.decimal_point, True)
1996 self.update_history_tab()
1997 self.update_status()
1999 need_restart = False
2001 lang_request = languages.keys()[lang_combo.currentIndex()]
2002 if lang_request != self.config.get('language'):
2003 self.config.set_key("language", lang_request, True)
2006 cur_request = str(currencies[cur_combo.currentIndex()])
2007 if cur_request != self.config.get('currency', "None"):
2008 self.config.set_key('currency', cur_request, True)
2009 self.update_wallet()
2011 self.run_hook('close_settings_dialog')
2014 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2016 self.receive_tab_set_mode(expert_cb.isChecked())
2018 def run_network_dialog(self):
2019 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2021 def closeEvent(self, event):
2023 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2024 self.save_column_widths()
2025 self.config.set_key("console-history", self.console.history[-50:], True)