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_nvc.i18n import _, set_language
21 from electrum_nvc.util import print_error, print_msg
22 import os.path, json, ast, traceback
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
33 from electrum_nvc.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum_nvc.plugins import run_hook
38 from electrum_nvc.util import format_satoshis
39 from electrum_nvc import Transaction
40 from electrum_nvc import mnemonic
41 from electrum_nvc import util, bitcoin, commands, Interface, Wallet
42 from electrum_nvc import SimpleConfig, Wallet, WalletStorage
43 from electrum_nvc import Imported_Wallet
45 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
46 from network_dialog import NetworkDialog
47 from qrcodewidget import QRCodeWidget, QRDialog
48 from qrtextedit import QRTextEdit
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
67 # status of payment requests
70 PR_SENT = 2 # sent but not propagated
71 PR_PAID = 3 # send and propagated
72 PR_ERROR = 4 # could not parse
75 from electrum_nvc import ELECTRUM_VERSION
78 from util import MyTreeWidget, HelpButton, EnterButton, line_dialog, text_dialog, ok_cancel_buttons, close_button, WaitingDialog
90 class StatusBarButton(QPushButton):
91 def __init__(self, icon, tooltip, func):
92 QPushButton.__init__(self, icon, '')
93 self.setToolTip(tooltip)
95 self.setMaximumWidth(25)
96 self.clicked.connect(func)
98 self.setIconSize(QSize(25,25))
100 def keyPressEvent(self, e):
101 if e.key() == QtCore.Qt.Key_Return:
113 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
115 class ElectrumWindow(QMainWindow):
119 def __init__(self, config, network, gui_object):
120 QMainWindow.__init__(self)
123 self.network = network
124 self.gui_object = gui_object
125 self.tray = gui_object.tray
126 self.go_lite = gui_object.go_lite
129 self.create_status_bar()
130 self.need_update = threading.Event()
132 self.decimal_point = config.get('decimal_point', 6)
133 self.num_zeros = int(config.get('num_zeros',0))
136 set_language(config.get('language'))
138 self.completions = QStringListModel()
140 self.tabs = tabs = QTabWidget(self)
141 self.column_widths = self.config.get("column_widths_2", default_column_widths )
142 tabs.addTab(self.create_history_tab(), _('History') )
143 tabs.addTab(self.create_send_tab(), _('Send') )
144 tabs.addTab(self.create_receive_tab(), _('Receive') )
145 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
146 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
147 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
148 tabs.addTab(self.create_console_tab(), _('Console') )
149 tabs.setMinimumSize(600, 400)
150 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
151 self.setCentralWidget(tabs)
153 g = self.config.get("winpos-qt",[100, 100, 840, 400])
154 self.setGeometry(g[0], g[1], g[2], g[3])
155 if self.config.get("is_maximized"):
158 self.setWindowIcon(QIcon(":icons/electrum.png"))
161 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
163 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
164 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
165 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
167 for i in range(tabs.count()):
168 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
170 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
171 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
172 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
173 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
174 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
176 self.history_list.setFocus(True)
180 self.network.register_callback('updated', lambda: self.need_update.set())
181 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
182 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
184 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
186 # set initial message
187 self.console.showMessage(self.network.banner)
190 self.payment_request = None
192 def update_account_selector(self):
194 accounts = self.wallet.get_account_names()
195 self.account_selector.clear()
196 if len(accounts) > 1:
197 self.account_selector.addItems([_("All accounts")] + accounts.values())
198 self.account_selector.setCurrentIndex(0)
199 self.account_selector.show()
201 self.account_selector.hide()
204 def load_wallet(self, wallet):
208 self.update_wallet_format()
210 self.invoices = self.wallet.storage.get('invoices', {})
211 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
212 self.current_account = self.wallet.storage.get("current_account", None)
213 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
214 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
215 self.setWindowTitle( title )
217 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
218 self.notify_transactions()
219 self.update_account_selector()
221 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
222 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
223 self.password_menu.setEnabled(not self.wallet.is_watching_only())
224 self.seed_menu.setEnabled(self.wallet.has_seed())
225 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
226 self.import_menu.setEnabled(self.wallet.can_import())
228 self.update_lock_icon()
229 self.update_buttons_on_seed()
230 self.update_console()
232 self.clear_receive_tab()
233 self.update_receive_tab()
234 run_hook('load_wallet', wallet)
237 def update_wallet_format(self):
238 # convert old-format imported keys
239 if self.wallet.imported_keys:
240 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
242 self.wallet.convert_imported_keys(password)
244 self.show_message("error")
247 def open_wallet(self):
248 wallet_folder = self.wallet.storage.path
249 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
253 storage = WalletStorage({'wallet_path': filename})
254 if not storage.file_exists:
255 self.show_message("file not found "+ filename)
258 self.wallet.stop_threads()
261 wallet = Wallet(storage)
262 wallet.start_threads(self.network)
264 self.load_wallet(wallet)
268 def backup_wallet(self):
270 path = self.wallet.storage.path
271 wallet_folder = os.path.dirname(path)
272 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
276 new_path = os.path.join(wallet_folder, filename)
279 shutil.copy2(path, new_path)
280 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
281 except (IOError, os.error), reason:
282 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
285 def new_wallet(self):
288 wallet_folder = os.path.dirname(self.wallet.storage.path)
291 filename = "wallet_%d"%i
292 if filename in os.listdir(wallet_folder):
297 filename = line_dialog(self, _('New Wallet'), _('Enter file name') + ':', _('OK'), filename)
301 full_path = os.path.join(wallet_folder, filename)
302 storage = WalletStorage({'wallet_path': full_path})
303 if storage.file_exists:
304 QMessageBox.critical(None, "Error", _("File exists"))
307 wizard = installwizard.InstallWizard(self.config, self.network, storage)
308 wallet = wizard.run('new')
310 self.load_wallet(wallet)
314 def init_menubar(self):
317 file_menu = menubar.addMenu(_("&File"))
318 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
319 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
320 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
321 file_menu.addAction(_("&Quit"), self.close)
323 wallet_menu = menubar.addMenu(_("&Wallet"))
324 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
325 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
327 wallet_menu.addSeparator()
329 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
330 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
331 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
333 wallet_menu.addSeparator()
334 labels_menu = wallet_menu.addMenu(_("&Labels"))
335 labels_menu.addAction(_("&Import"), self.do_import_labels)
336 labels_menu.addAction(_("&Export"), self.do_export_labels)
338 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
339 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
340 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
341 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
342 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
344 tools_menu = menubar.addMenu(_("&Tools"))
346 # Settings / Preferences are all reserved keywords in OSX using this as work around
347 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
348 tools_menu.addAction(_("&Network"), self.run_network_dialog)
349 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
350 tools_menu.addSeparator()
351 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
352 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
353 tools_menu.addSeparator()
355 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
356 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
357 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
359 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
360 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
361 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
362 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
363 raw_transaction_menu.addAction(_("&From QR code"), self.read_tx_from_qrcode)
364 self.raw_transaction_menu = raw_transaction_menu
366 help_menu = menubar.addMenu(_("&Help"))
367 help_menu.addAction(_("&About"), self.show_about)
368 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
369 help_menu.addSeparator()
370 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
371 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
373 self.setMenuBar(menubar)
375 def show_about(self):
376 QMessageBox.about(self, "Electrum-NVC",
377 _("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 Novacoin system."))
379 def show_report_bug(self):
380 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
381 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
384 def notify_transactions(self):
385 if not self.network or not self.network.is_connected():
388 print_error("Notifying GUI")
389 if len(self.network.pending_transactions_for_notifications) > 0:
390 # Combine the transactions if there are more then three
391 tx_amount = len(self.network.pending_transactions_for_notifications)
394 for tx in self.network.pending_transactions_for_notifications:
395 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
399 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
400 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
402 self.network.pending_transactions_for_notifications = []
404 for tx in self.network.pending_transactions_for_notifications:
406 self.network.pending_transactions_for_notifications.remove(tx)
407 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
409 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
411 def notify(self, message):
412 self.tray.showMessage("Electrum-NVC", message, QSystemTrayIcon.Information, 20000)
416 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
417 def getOpenFileName(self, title, filter = ""):
418 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
419 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
420 if fileName and directory != os.path.dirname(fileName):
421 self.config.set_key('io_dir', os.path.dirname(fileName), True)
424 def getSaveFileName(self, title, filename, filter = ""):
425 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
426 path = os.path.join( directory, filename )
427 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
428 if fileName and directory != os.path.dirname(fileName):
429 self.config.set_key('io_dir', os.path.dirname(fileName), True)
433 QMainWindow.close(self)
434 run_hook('close_main_window')
436 def connect_slots(self, sender):
437 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
438 self.previous_payto_e=''
440 def timer_actions(self):
441 if self.need_update.is_set():
443 self.need_update.clear()
445 run_hook('timer_actions')
447 def format_amount(self, x, is_diff=False, whitespaces=False):
448 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
451 def get_decimal_point(self):
452 return self.decimal_point
456 assert self.decimal_point in [2, 3, 6]
457 if self.decimal_point == 2:
459 if self.decimal_point == 3:
461 if self.decimal_point == 6:
463 raise Exception('Unknown base unit')
465 def update_status(self):
466 if self.network is None or not self.network.is_running():
468 icon = QIcon(":icons/status_disconnected.png")
470 elif self.network.is_connected():
471 if not self.wallet.up_to_date:
472 text = _("Synchronizing...")
473 icon = QIcon(":icons/status_waiting.png")
474 elif self.network.server_lag > 1:
475 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
476 icon = QIcon(":icons/status_lagging.png")
478 c, u = self.wallet.get_account_balance(self.current_account)
479 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
480 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
482 # append fiat balance and price from exchange rate plugin
484 run_hook('get_fiat_status_text', c+u, r)
489 self.tray.setToolTip(text)
490 icon = QIcon(":icons/status_connected.png")
492 text = _("Not connected")
493 icon = QIcon(":icons/status_disconnected.png")
495 self.balance_label.setText(text)
496 self.status_button.setIcon( icon )
499 def update_wallet(self):
501 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
502 self.update_history_tab()
503 self.update_receive_tab()
504 self.update_address_tab()
505 self.update_contacts_tab()
506 self.update_completions()
507 self.update_invoices_tab()
510 def create_history_tab(self):
511 self.history_list = l = MyTreeWidget(self)
513 for i,width in enumerate(self.column_widths['history']):
514 l.setColumnWidth(i, width)
515 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
516 l.itemDoubleClicked.connect(self.tx_label_clicked)
517 l.itemChanged.connect(self.tx_label_changed)
518 l.customContextMenuRequested.connect(self.create_history_menu)
522 def create_history_menu(self, position):
523 self.history_list.selectedIndexes()
524 item = self.history_list.currentItem()
525 be = self.config.get('block_explorer', 'explorer.novaco.in')
526 if be == 'explorer.novaco.in':
527 block_explorer = 'https://explorer.novaco.in/tx/'
528 elif be == 'novacoin.su':
529 block_explorer = 'http://novacoin.su/tx/'
532 tx_hash = str(item.data(0, Qt.UserRole).toString())
533 if not tx_hash: return
535 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
536 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
537 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
538 menu.addAction(_("View on explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
539 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
542 def show_transaction(self, tx):
543 import transaction_dialog
544 d = transaction_dialog.TxDialog(tx, self)
547 def tx_label_clicked(self, item, column):
548 if column==2 and item.isSelected():
550 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
551 self.history_list.editItem( item, column )
552 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
555 def tx_label_changed(self, item, column):
559 tx_hash = str(item.data(0, Qt.UserRole).toString())
560 tx = self.wallet.transactions.get(tx_hash)
561 text = unicode( item.text(2) )
562 self.wallet.set_label(tx_hash, text)
564 item.setForeground(2, QBrush(QColor('black')))
566 text = self.wallet.get_default_label(tx_hash)
567 item.setText(2, text)
568 item.setForeground(2, QBrush(QColor('gray')))
572 def edit_label(self, is_recv):
573 l = self.address_list if is_recv else self.contacts_list
574 item = l.currentItem()
575 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
576 l.editItem( item, 1 )
577 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
581 def address_label_clicked(self, item, column, l, column_addr, column_label):
582 if column == column_label and item.isSelected():
583 is_editable = item.data(0, 32).toBool()
586 addr = unicode( item.text(column_addr) )
587 label = unicode( item.text(column_label) )
588 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
589 l.editItem( item, column )
590 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
593 def address_label_changed(self, item, column, l, column_addr, column_label):
594 if column == column_label:
595 addr = unicode( item.text(column_addr) )
596 text = unicode( item.text(column_label) )
597 is_editable = item.data(0, 32).toBool()
601 changed = self.wallet.set_label(addr, text)
603 self.update_history_tab()
604 self.update_completions()
606 self.current_item_changed(item)
608 run_hook('item_changed', item, column)
611 def current_item_changed(self, a):
612 run_hook('current_item_changed', a)
616 def update_history_tab(self):
618 self.history_list.clear()
619 for item in self.wallet.get_tx_history(self.current_account):
620 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
621 time_str = _("unknown")
624 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
626 time_str = _("error")
629 time_str = 'unverified'
630 icon = QIcon(":icons/unconfirmed.png")
633 icon = QIcon(":icons/unconfirmed.png")
635 icon = QIcon(":icons/clock%d.png"%conf)
637 icon = QIcon(":icons/confirmed.png")
639 if value is not None:
640 v_str = self.format_amount(value, True, whitespaces=True)
644 balance_str = self.format_amount(balance, whitespaces=True)
647 label, is_default_label = self.wallet.get_label(tx_hash)
649 label = _('Pruned transaction outputs')
650 is_default_label = False
652 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
653 item.setFont(2, QFont(MONOSPACE_FONT))
654 item.setFont(3, QFont(MONOSPACE_FONT))
655 item.setFont(4, QFont(MONOSPACE_FONT))
657 item.setForeground(3, QBrush(QColor("#BC1E1E")))
659 item.setData(0, Qt.UserRole, tx_hash)
660 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
662 item.setForeground(2, QBrush(QColor('grey')))
664 item.setIcon(0, icon)
665 self.history_list.insertTopLevelItem(0,item)
668 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
669 run_hook('history_tab_update')
672 def create_receive_tab(self):
674 grid = QGridLayout(w)
675 grid.setColumnMinimumWidth(3, 300)
676 grid.setColumnStretch(5, 1)
678 self.receive_address_e = QLineEdit()
679 self.receive_address_e.setReadOnly(True)
680 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
681 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
682 self.receive_address_e.textChanged.connect(self.update_receive_qr)
684 self.receive_message_e = QLineEdit()
685 grid.addWidget(QLabel(_('Message')), 1, 0)
686 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
687 self.receive_message_e.textChanged.connect(self.update_receive_qr)
689 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
690 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
691 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
692 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
694 self.save_request_button = QPushButton(_('Save'))
695 self.save_request_button.clicked.connect(self.save_payment_request)
696 grid.addWidget(self.save_request_button, 3, 1)
697 clear_button = QPushButton(_('New'))
698 clear_button.clicked.connect(self.new_receive_address)
699 grid.addWidget(clear_button, 3, 2)
700 grid.setRowStretch(4, 1)
702 self.receive_qr = QRCodeWidget(fixedSize=200)
703 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
705 grid.setRowStretch(5, 1)
707 self.receive_requests_label = QLabel(_('Saved Requests'))
708 self.receive_list = MyTreeWidget(self)
709 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
710 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
711 self.receive_list.itemClicked.connect(self.receive_item_changed)
712 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
713 self.receive_list.setColumnWidth(0, 340)
714 h = self.receive_list.header()
715 h.setStretchLastSection(False)
716 h.setResizeMode(1, QHeaderView.Stretch)
718 grid.addWidget(self.receive_requests_label, 6, 0)
719 grid.addWidget(self.receive_list, 7, 0, 1, 6)
722 def receive_item_changed(self, item):
725 addr = str(item.text(0))
726 amount, message = self.receive_requests[addr]
727 self.receive_address_e.setText(addr)
728 self.receive_message_e.setText(message)
729 self.receive_amount_e.setAmount(amount)
732 def receive_list_delete(self, item):
733 addr = str(item.text(0))
734 self.receive_requests.pop(addr)
735 self.update_receive_tab()
736 self.clear_receive_tab()
738 def receive_list_menu(self, position):
739 item = self.receive_list.itemAt(position)
741 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
742 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
744 def save_payment_request(self):
745 addr = str(self.receive_address_e.text())
746 amount = self.receive_amount_e.get_amount()
747 message = str(self.receive_message_e.text())
748 if not message and not amount:
749 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
751 self.receive_requests = self.wallet.storage.get('receive_requests',{})
752 self.receive_requests[addr] = (amount, message)
753 self.wallet.storage.put('receive_requests', self.receive_requests)
754 self.update_receive_tab()
756 def new_receive_address(self):
757 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
759 if not self.wallet.history.get(addr) and addr not in self.receive_requests.keys():
762 if isinstance(self.wallet, Imported_Wallet):
763 self.show_message(_('No more addresses in your wallet.'))
765 if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
767 addr = self.wallet.create_new_address(self.current_account, False)
768 self.receive_address_e.setText(addr)
769 self.receive_message_e.setText('')
770 self.receive_amount_e.setAmount(None)
772 def clear_receive_tab(self):
773 self.receive_requests = self.wallet.storage.get('receive_requests',{})
774 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
776 if not self.wallet.history.get(addr) and addr not in self.receive_requests.keys():
780 self.receive_address_e.setText(addr)
781 self.receive_message_e.setText('')
782 self.receive_amount_e.setAmount(None)
784 def receive_at(self, addr):
785 if not bitcoin.is_address(addr):
787 self.tabs.setCurrentIndex(2)
788 self.receive_address_e.setText(addr)
790 def update_receive_tab(self):
791 self.receive_requests = self.wallet.storage.get('receive_requests',{})
792 b = len(self.receive_requests) > 0
793 self.receive_list.setVisible(b)
794 self.receive_requests_label.setVisible(b)
796 self.receive_list.clear()
797 for address, v in self.receive_requests.items():
799 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
800 item.setFont(0, QFont(MONOSPACE_FONT))
801 self.receive_list.addTopLevelItem(item)
804 def update_receive_qr(self):
805 import urlparse, urllib
806 addr = str(self.receive_address_e.text())
807 amount = self.receive_amount_e.get_amount()
808 message = unicode(self.receive_message_e.text()).encode('utf8')
809 self.save_request_button.setEnabled((amount is not None) or (message != ""))
813 query.append('amount=%s'%format_satoshis(amount))
815 query.append('message=%s'%urllib.quote(message))
816 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
817 url = urlparse.urlunparse(p)
820 self.receive_qr.setData(url)
821 run_hook('update_receive_qr', addr, amount, message, url)
824 def create_send_tab(self):
827 self.send_grid = grid = QGridLayout(w)
829 grid.setColumnMinimumWidth(3,300)
830 grid.setColumnStretch(5,1)
831 grid.setRowStretch(8, 1)
833 from paytoedit import PayToEdit
834 self.amount_e = BTCAmountEdit(self.get_decimal_point)
835 self.payto_e = PayToEdit(self)
836 self.payto_help = HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Novacoin 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 Novacoin address)'))
837 grid.addWidget(QLabel(_('Pay to')), 1, 0)
838 grid.addWidget(self.payto_e, 1, 1, 1, 3)
839 grid.addWidget(self.payto_help, 1, 4)
841 completer = QCompleter()
842 completer.setCaseSensitivity(False)
843 self.payto_e.setCompleter(completer)
844 completer.setModel(self.completions)
846 self.message_e = MyLineEdit()
847 self.message_help = 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.'))
848 grid.addWidget(QLabel(_('Description')), 2, 0)
849 grid.addWidget(self.message_e, 2, 1, 1, 3)
850 grid.addWidget(self.message_help, 2, 4)
852 self.from_label = QLabel(_('From'))
853 grid.addWidget(self.from_label, 3, 0)
854 self.from_list = MyTreeWidget(self)
855 self.from_list.setColumnCount(2)
856 self.from_list.setColumnWidth(0, 350)
857 self.from_list.setColumnWidth(1, 50)
858 self.from_list.setHeaderHidden(True)
859 self.from_list.setMaximumHeight(80)
860 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
861 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
862 grid.addWidget(self.from_list, 3, 1, 1, 3)
863 self.set_pay_from([])
865 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
866 + _('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.') \
867 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
868 grid.addWidget(QLabel(_('Amount')), 4, 0)
869 grid.addWidget(self.amount_e, 4, 1, 1, 2)
870 grid.addWidget(self.amount_help, 4, 3)
872 self.fee_e = BTCAmountEdit(self.get_decimal_point)
873 grid.addWidget(QLabel(_('Fee')), 5, 0)
874 grid.addWidget(self.fee_e, 5, 1, 1, 2)
875 grid.addWidget(HelpButton(
876 _('Novacoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
877 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
878 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
880 self.send_button = EnterButton(_("Send"), self.do_send)
881 grid.addWidget(self.send_button, 6, 1)
883 b = EnterButton(_("Clear"), self.do_clear)
884 grid.addWidget(b, 6, 2)
886 self.payto_sig = QLabel('')
887 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
889 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
890 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
893 def entry_changed( is_fee ):
895 if self.amount_e.is_shortcut:
896 self.amount_e.is_shortcut = False
897 sendable = self.get_sendable_balance()
898 # there is only one output because we are completely spending inputs
899 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
900 fee = self.wallet.estimated_fee(inputs, 1)
902 self.amount_e.setAmount(amount)
903 self.amount_e.textEdited.emit("")
904 self.fee_e.setAmount(fee)
907 amount = self.amount_e.get_amount()
908 fee = self.fee_e.get_amount()
909 outputs = self.payto_e.get_outputs()
915 self.fee_e.setAmount(None)
916 not_enough_funds = False
918 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, len(outputs), coins = self.get_coins())
919 not_enough_funds = len(inputs) == 0
921 self.fee_e.setAmount(fee)
923 if not not_enough_funds:
925 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
929 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
930 text = _( "Not enough funds" )
931 c, u = self.wallet.get_frozen_balance()
932 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
934 self.statusBar().showMessage(text)
935 self.amount_e.setPalette(palette)
936 self.fee_e.setPalette(palette)
938 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
939 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
941 run_hook('create_send_tab', grid)
944 def from_list_delete(self, item):
945 i = self.from_list.indexOfTopLevelItem(item)
947 self.redraw_from_list()
949 def from_list_menu(self, position):
950 item = self.from_list.itemAt(position)
952 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
953 menu.exec_(self.from_list.viewport().mapToGlobal(position))
955 def set_pay_from(self, domain = None):
956 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
957 self.redraw_from_list()
959 def redraw_from_list(self):
960 self.from_list.clear()
961 self.from_label.setHidden(len(self.pay_from) == 0)
962 self.from_list.setHidden(len(self.pay_from) == 0)
965 h = x.get('prevout_hash')
966 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
968 for item in self.pay_from:
969 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
971 def update_completions(self):
973 for addr,label in self.wallet.labels.items():
974 if addr in self.wallet.addressbook:
975 l.append( label + ' <' + addr + '>')
977 run_hook('update_completions', l)
978 self.completions.setStringList(l)
982 return lambda s, *args: s.do_protect(func, args)
985 def read_send_tab(self):
987 if self.payment_request and self.payment_request.has_expired():
988 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
991 label = unicode( self.message_e.text() )
993 if self.payment_request:
994 outputs = self.payment_request.get_outputs()
996 outputs = self.payto_e.get_outputs()
999 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
1002 for type, addr, amount in outputs:
1004 QMessageBox.warning(self, _('Error'), _('Novacoin Address is None'), _('OK'))
1006 if type == 'op_return':
1008 if type == 'address' and not bitcoin.is_address(addr):
1009 QMessageBox.warning(self, _('Error'), _('Invalid Novacoin Address'), _('OK'))
1012 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
1015 amount = sum(map(lambda x:x[2], outputs))
1017 fee = self.fee_e.get_amount()
1019 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
1022 confirm_amount = self.config.get('confirm_amount', 10000)
1023 if amount >= confirm_amount:
1024 o = '\n'.join(map(lambda x:x[1], outputs))
1025 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
1028 confirm_fee = self.config.get('confirm_fee', 10000)
1029 if fee >= confirm_fee:
1030 if not self.question(_("The fee for this transaction seems unusually high.\nAre you really sure you want to pay %(fee)s in fees?")%{ 'fee' : self.format_amount(fee) + ' '+ self.base_unit()}):
1033 coins = self.get_coins()
1034 return outputs, fee, label, coins
1038 r = self.read_send_tab()
1041 outputs, fee, label, coins = r
1042 self.send_tx(outputs, fee, label, coins)
1046 def send_tx(self, outputs, fee, label, coins, password):
1047 self.send_button.setDisabled(True)
1049 # first, create an unsigned tx
1051 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1053 except Exception as e:
1054 traceback.print_exc(file=sys.stdout)
1055 self.show_message(str(e))
1056 self.send_button.setDisabled(False)
1059 # call hook to see if plugin needs gui interaction
1060 run_hook('send_tx', tx)
1064 if self.wallet.is_watching_only():
1068 self.wallet.add_keypairs(tx, keypairs, password)
1069 self.wallet.sign_transaction(tx, keypairs, password)
1070 except Exception as e:
1071 traceback.print_exc(file=sys.stdout)
1077 self.show_message(tx.error)
1078 self.send_button.setDisabled(False)
1080 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1081 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1082 self.send_button.setDisabled(False)
1085 self.wallet.set_label(tx.hash(), label)
1087 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1088 self.show_transaction(tx)
1090 self.send_button.setDisabled(False)
1093 self.broadcast_transaction(tx)
1095 # keep a reference to WaitingDialog or the gui might crash
1096 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1097 self.waiting_dialog.start()
1101 def broadcast_transaction(self, tx):
1103 def broadcast_thread():
1104 pr = self.payment_request
1106 return self.wallet.sendtx(tx)
1108 if pr.has_expired():
1109 self.payment_request = None
1110 return False, _("Payment request has expired")
1112 status, msg = self.wallet.sendtx(tx)
1116 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1117 self.wallet.storage.put('invoices', self.invoices)
1118 self.update_invoices_tab()
1119 self.payment_request = None
1120 refund_address = self.wallet.addresses()[0]
1121 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1127 def broadcast_done(status, msg):
1129 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1132 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1133 self.send_button.setDisabled(False)
1135 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1136 self.waiting_dialog.start()
1140 def prepare_for_payment_request(self):
1141 self.tabs.setCurrentIndex(1)
1142 self.payto_e.is_pr = True
1143 for e in [self.payto_e, self.amount_e, self.message_e]:
1145 for h in [self.payto_help, self.amount_help, self.message_help]:
1147 self.payto_e.setText(_("please wait..."))
1150 def payment_request_ok(self):
1151 pr = self.payment_request
1153 if pr_id not in self.invoices:
1154 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1155 self.wallet.storage.put('invoices', self.invoices)
1156 self.update_invoices_tab()
1158 print_error('invoice already in list')
1160 status = self.invoices[pr_id][4]
1161 if status == PR_PAID:
1163 self.show_message("invoice already paid")
1164 self.payment_request = None
1167 self.payto_help.show()
1168 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1170 if not pr.has_expired():
1171 self.payto_e.setGreen()
1173 self.payto_e.setExpired()
1175 self.payto_e.setText(pr.domain)
1176 self.amount_e.setText(self.format_amount(pr.get_amount()))
1177 self.message_e.setText(pr.get_memo())
1179 def payment_request_error(self):
1181 self.show_message(self.payment_request.error)
1182 self.payment_request = None
1184 def pay_from_URI(self,URI):
1187 address, amount, label, message, request_url = util.parse_URI(URI)
1189 address, amount, label, message, request_url = util.parse_URI(URI)
1190 except Exception as e:
1191 QMessageBox.warning(self, _('Error'), _('Invalid novacoin URI:') + '\n' + str(e), _('OK'))
1194 self.tabs.setCurrentIndex(1)
1198 if self.wallet.labels.get(address) != label:
1199 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1200 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1201 self.wallet.addressbook.append(address)
1202 self.wallet.set_label(address, label)
1204 label = self.wallet.labels.get(address)
1206 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1208 self.message_e.setText(message)
1210 self.amount_e.setAmount(amount)
1213 from electrum_nvc import paymentrequest
1214 def payment_request():
1215 self.payment_request = paymentrequest.PaymentRequest(self.config)
1216 self.payment_request.read(request_url)
1217 if self.payment_request.verify():
1218 self.emit(SIGNAL('payment_request_ok'))
1220 self.emit(SIGNAL('payment_request_error'))
1222 self.pr_thread = threading.Thread(target=payment_request).start()
1223 self.prepare_for_payment_request()
1228 self.payto_e.is_pr = False
1229 self.payto_sig.setVisible(False)
1230 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1234 for h in [self.payto_help, self.amount_help, self.message_help]:
1237 self.payto_help.set_alt(None)
1238 self.set_pay_from([])
1239 self.update_status()
1243 def set_addrs_frozen(self,addrs,freeze):
1245 if not addr: continue
1246 if addr in self.wallet.frozen_addresses and not freeze:
1247 self.wallet.unfreeze(addr)
1248 elif addr not in self.wallet.frozen_addresses and freeze:
1249 self.wallet.freeze(addr)
1250 self.update_address_tab()
1254 def create_list_tab(self, headers):
1255 "generic tab creation method"
1256 l = MyTreeWidget(self)
1257 l.setColumnCount( len(headers) )
1258 l.setHeaderLabels( headers )
1261 vbox = QVBoxLayout()
1268 vbox.addWidget(buttons)
1273 def create_addresses_tab(self):
1274 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1275 for i,width in enumerate(self.column_widths['receive']):
1276 l.setColumnWidth(i, width)
1277 l.setContextMenuPolicy(Qt.CustomContextMenu)
1278 l.customContextMenuRequested.connect(self.create_receive_menu)
1279 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1280 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1281 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1282 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1283 self.address_list = l
1289 def save_column_widths(self):
1290 self.column_widths["receive"] = []
1291 for i in range(self.address_list.columnCount() -1):
1292 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1294 self.column_widths["history"] = []
1295 for i in range(self.history_list.columnCount() - 1):
1296 self.column_widths["history"].append(self.history_list.columnWidth(i))
1298 self.column_widths["contacts"] = []
1299 for i in range(self.contacts_list.columnCount() - 1):
1300 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1302 self.config.set_key("column_widths_2", self.column_widths, True)
1305 def create_contacts_tab(self):
1306 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1307 l.setContextMenuPolicy(Qt.CustomContextMenu)
1308 l.customContextMenuRequested.connect(self.create_contact_menu)
1309 for i,width in enumerate(self.column_widths['contacts']):
1310 l.setColumnWidth(i, width)
1311 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1312 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1313 self.contacts_list = l
1317 def create_invoices_tab(self):
1318 l, w = self.create_list_tab([_('Requestor'), _('Memo'), _('Date'), _('Amount'), _('Status')])
1319 l.setColumnWidth(0, 150)
1320 l.setColumnWidth(2, 150)
1321 l.setColumnWidth(3, 150)
1323 h.setStretchLastSection(False)
1324 h.setResizeMode(1, QHeaderView.Stretch)
1325 l.setContextMenuPolicy(Qt.CustomContextMenu)
1326 l.customContextMenuRequested.connect(self.create_invoice_menu)
1327 self.invoices_list = l
1330 def update_invoices_tab(self):
1331 invoices = self.wallet.storage.get('invoices', {})
1332 l = self.invoices_list
1334 for key, value in sorted(invoices.items(), key=lambda x: -x[1][3]):
1335 domain, memo, amount, expiration_date, status, tx_hash = value
1336 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1338 date_str = datetime.datetime.fromtimestamp(expiration_date).isoformat(' ')[:-3]
1339 item = QTreeWidgetItem( [ domain, memo, date_str, self.format_amount(amount, whitespaces=True), format_status(status)] )
1340 item.setData(0, 32, key)
1341 item.setFont(0, QFont(MONOSPACE_FONT))
1342 item.setFont(3, QFont(MONOSPACE_FONT))
1343 l.addTopLevelItem(item)
1344 l.setCurrentItem(l.topLevelItem(0))
1346 def delete_imported_key(self, addr):
1347 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1348 self.wallet.delete_imported_key(addr)
1349 self.update_address_tab()
1350 self.update_history_tab()
1352 def edit_account_label(self, k):
1353 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1355 label = unicode(text)
1356 self.wallet.set_label(k,label)
1357 self.update_address_tab()
1359 def account_set_expanded(self, item, k, b):
1361 self.accounts_expanded[k] = b
1363 def create_account_menu(self, position, k, item):
1365 if item.isExpanded():
1366 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1368 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1369 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1370 if self.wallet.seed_version > 4:
1371 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1372 if self.wallet.account_is_pending(k):
1373 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1374 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1376 def delete_pending_account(self, k):
1377 self.wallet.delete_pending_account(k)
1378 self.update_address_tab()
1380 def create_receive_menu(self, position):
1381 # fixme: this function apparently has a side effect.
1382 # if it is not called the menu pops up several times
1383 #self.address_list.selectedIndexes()
1385 selected = self.address_list.selectedItems()
1386 multi_select = len(selected) > 1
1387 addrs = [unicode(item.text(0)) for item in selected]
1388 if not multi_select:
1389 item = self.address_list.itemAt(position)
1393 if not is_valid(addr):
1394 k = str(item.data(0,32).toString())
1396 self.create_account_menu(position, k, item)
1398 item.setExpanded(not item.isExpanded())
1402 if not multi_select:
1403 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1404 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1405 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1406 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1407 if not self.wallet.is_watching_only():
1408 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1409 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1410 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1411 if self.wallet.is_imported(addr):
1412 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1414 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1415 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1416 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1417 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1420 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1421 if any(can_send(addr) for addr in addrs):
1422 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1424 run_hook('receive_menu', menu, addrs)
1425 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1428 def get_sendable_balance(self):
1429 return sum(map(lambda x:x['value'], self.get_coins()))
1432 def get_coins(self):
1434 return self.pay_from
1436 domain = self.wallet.get_account_addresses(self.current_account)
1437 for i in self.wallet.frozen_addresses:
1438 if i in domain: domain.remove(i)
1439 return self.wallet.get_unspent_coins(domain)
1442 def send_from_addresses(self, addrs):
1443 self.set_pay_from( addrs )
1444 self.tabs.setCurrentIndex(1)
1447 def payto(self, addr):
1449 label = self.wallet.labels.get(addr)
1450 m_addr = label + ' <' + addr + '>' if label else addr
1451 self.tabs.setCurrentIndex(1)
1452 self.payto_e.setText(m_addr)
1453 self.amount_e.setFocus()
1456 def delete_contact(self, x):
1457 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1458 self.wallet.delete_contact(x)
1459 self.wallet.set_label(x, None)
1460 self.update_history_tab()
1461 self.update_contacts_tab()
1462 self.update_completions()
1465 def create_contact_menu(self, position):
1466 item = self.contacts_list.itemAt(position)
1469 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1471 addr = unicode(item.text(0))
1472 label = unicode(item.text(1))
1473 is_editable = item.data(0,32).toBool()
1474 payto_addr = item.data(0,33).toString()
1475 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1476 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1477 menu.addAction(_("QR code"), lambda: self.show_qrcode("novacoin:" + addr, _("Address")))
1479 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1480 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1482 run_hook('create_contact_menu', menu, item)
1483 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1485 def delete_invoice(self, key):
1486 self.invoices.pop(key)
1487 self.wallet.storage.put('invoices', self.invoices)
1488 self.update_invoices_tab()
1490 def show_invoice(self, key):
1491 from electrum_nvc.paymentrequest import PaymentRequest
1492 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1493 pr = PaymentRequest(self.config)
1497 self.show_pr_details(pr, tx_hash)
1499 def show_pr_details(self, pr, tx_hash=None):
1500 msg = 'Domain: ' + pr.domain
1501 msg += '\nStatus: ' + pr.get_status()
1502 msg += '\nMemo: ' + pr.get_memo()
1503 msg += '\nPayment URL: ' + pr.payment_url
1504 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[1] + ' ' + self.format_amount(x[2])+ self.base_unit(), pr.get_outputs()))
1506 msg += '\n\nTransaction ID: ' + tx_hash
1507 QMessageBox.information(self, 'Invoice', msg , 'OK')
1509 def do_pay_invoice(self, key):
1510 from electrum_nvc.paymentrequest import PaymentRequest
1511 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1512 pr = PaymentRequest(self.config)
1515 self.payment_request = pr
1516 self.prepare_for_payment_request()
1518 self.payment_request_ok()
1520 self.payment_request_error()
1523 def create_invoice_menu(self, position):
1524 item = self.invoices_list.itemAt(position)
1527 key = str(item.data(0, 32).toString())
1528 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1530 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1531 if status == PR_UNPAID:
1532 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1533 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1534 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1538 def update_address_tab(self):
1539 l = self.address_list
1540 # extend the syntax for consistency
1541 l.addChild = l.addTopLevelItem
1542 l.insertChild = l.insertTopLevelItem
1546 accounts = self.wallet.get_accounts()
1547 if self.current_account is None:
1548 account_items = sorted(accounts.items())
1550 account_items = [(self.current_account, accounts.get(self.current_account))]
1553 for k, account in account_items:
1555 if len(accounts) > 1:
1556 name = self.wallet.get_account_name(k)
1557 c,u = self.wallet.get_account_balance(k)
1558 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1559 l.addTopLevelItem(account_item)
1560 account_item.setExpanded(self.accounts_expanded.get(k, True))
1561 account_item.setData(0, 32, k)
1565 sequences = [0,1] if account.has_change() else [0]
1566 for is_change in sequences:
1567 if len(sequences) > 1:
1568 name = _("Receiving") if not is_change else _("Change")
1569 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1570 account_item.addChild(seq_item)
1572 seq_item.setExpanded(True)
1574 seq_item = account_item
1576 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1579 addr_list = account.get_addresses(is_change)
1580 for address in addr_list:
1581 num, is_used = self.wallet.is_used(address)
1582 label = self.wallet.labels.get(address,'')
1583 c, u = self.wallet.get_addr_balance(address)
1584 balance = self.format_amount(c + u)
1585 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1586 item.setFont(0, QFont(MONOSPACE_FONT))
1587 item.setData(0, 32, True) # label can be edited
1588 if address in self.wallet.frozen_addresses:
1589 item.setBackgroundColor(0, QColor('lightblue'))
1590 if self.wallet.is_beyond_limit(address, account, is_change):
1591 item.setBackgroundColor(0, QColor('red'))
1594 seq_item.insertChild(0, used_item)
1596 used_item.addChild(item)
1598 seq_item.addChild(item)
1600 # we use column 1 because column 0 may be hidden
1601 l.setCurrentItem(l.topLevelItem(0),1)
1604 def update_contacts_tab(self):
1605 l = self.contacts_list
1608 for address in self.wallet.addressbook:
1609 label = self.wallet.labels.get(address,'')
1610 n = self.wallet.get_num_tx(address)
1611 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1612 item.setFont(0, QFont(MONOSPACE_FONT))
1613 # 32 = label can be edited (bool)
1614 item.setData(0,32, True)
1616 item.setData(0,33, address)
1617 l.addTopLevelItem(item)
1619 run_hook('update_contacts_tab', l)
1620 l.setCurrentItem(l.topLevelItem(0))
1623 def create_console_tab(self):
1624 from console import Console
1625 self.console = console = Console()
1629 def update_console(self):
1630 console = self.console
1631 console.history = self.config.get("console-history",[])
1632 console.history_index = len(console.history)
1634 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1635 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1637 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1639 def mkfunc(f, method):
1640 return lambda *args: apply( f, (method, args, self.password_dialog ))
1642 if m[0]=='_' or m in ['network','wallet']: continue
1643 methods[m] = mkfunc(c._run, m)
1645 console.updateNamespace(methods)
1648 def change_account(self,s):
1649 if s == _("All accounts"):
1650 self.current_account = None
1652 accounts = self.wallet.get_account_names()
1653 for k, v in accounts.items():
1655 self.current_account = k
1656 self.update_history_tab()
1657 self.update_status()
1658 self.update_address_tab()
1659 self.update_receive_tab()
1661 def create_status_bar(self):
1664 sb.setFixedHeight(35)
1665 qtVersion = qVersion()
1667 self.balance_label = QLabel("")
1668 sb.addWidget(self.balance_label)
1670 from version_getter import UpdateLabel
1671 self.updatelabel = UpdateLabel(self.config, sb)
1673 self.account_selector = QComboBox()
1674 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1675 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1676 sb.addPermanentWidget(self.account_selector)
1678 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1679 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1681 self.lock_icon = QIcon()
1682 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1683 sb.addPermanentWidget( self.password_button )
1685 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1686 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1687 sb.addPermanentWidget( self.seed_button )
1688 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1689 sb.addPermanentWidget( self.status_button )
1691 run_hook('create_status_bar', (sb,))
1693 self.setStatusBar(sb)
1696 def update_lock_icon(self):
1697 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1698 self.password_button.setIcon( icon )
1701 def update_buttons_on_seed(self):
1702 if self.wallet.has_seed():
1703 self.seed_button.show()
1705 self.seed_button.hide()
1707 if not self.wallet.is_watching_only():
1708 self.password_button.show()
1709 self.send_button.setText(_("Send"))
1711 self.password_button.hide()
1712 self.send_button.setText(_("Create unsigned transaction"))
1715 def change_password_dialog(self):
1716 from password_dialog import PasswordDialog
1717 d = PasswordDialog(self.wallet, self)
1719 self.update_lock_icon()
1722 def new_contact_dialog(self):
1725 d.setWindowTitle(_("New Contact"))
1726 vbox = QVBoxLayout(d)
1727 vbox.addWidget(QLabel(_('New Contact')+':'))
1729 grid = QGridLayout()
1732 grid.addWidget(QLabel(_("Address")), 1, 0)
1733 grid.addWidget(line1, 1, 1)
1734 grid.addWidget(QLabel(_("Name")), 2, 0)
1735 grid.addWidget(line2, 2, 1)
1737 vbox.addLayout(grid)
1738 vbox.addLayout(ok_cancel_buttons(d))
1743 address = str(line1.text())
1744 label = unicode(line2.text())
1746 if not is_valid(address):
1747 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1750 self.wallet.add_contact(address)
1752 self.wallet.set_label(address, label)
1754 self.update_contacts_tab()
1755 self.update_history_tab()
1756 self.update_completions()
1757 self.tabs.setCurrentIndex(3)
1761 def new_account_dialog(self, password):
1763 dialog = QDialog(self)
1765 dialog.setWindowTitle(_("New Account"))
1767 vbox = QVBoxLayout()
1768 vbox.addWidget(QLabel(_('Account name')+':'))
1771 msg = _("Note: Newly created accounts are 'pending' until they receive novacoins.") + " " \
1772 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1777 vbox.addLayout(ok_cancel_buttons(dialog))
1778 dialog.setLayout(vbox)
1782 name = str(e.text())
1785 self.wallet.create_pending_account(name, password)
1786 self.update_address_tab()
1787 self.tabs.setCurrentIndex(3)
1792 def show_master_public_keys(self):
1794 dialog = QDialog(self)
1796 dialog.setWindowTitle(_("Master Public Keys"))
1798 main_layout = QGridLayout()
1799 mpk_dict = self.wallet.get_master_public_keys()
1801 for key, value in mpk_dict.items():
1802 main_layout.addWidget(QLabel(key), i, 0)
1803 mpk_text = QTextEdit()
1804 mpk_text.setReadOnly(True)
1805 mpk_text.setMaximumHeight(170)
1806 mpk_text.setText(value)
1807 main_layout.addWidget(mpk_text, i + 1, 0)
1810 vbox = QVBoxLayout()
1811 vbox.addLayout(main_layout)
1812 vbox.addLayout(close_button(dialog))
1814 dialog.setLayout(vbox)
1819 def show_seed_dialog(self, password):
1820 if not self.wallet.has_seed():
1821 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1825 mnemonic = self.wallet.get_mnemonic(password)
1827 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1829 from seed_dialog import SeedDialog
1830 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1835 def show_qrcode(self, data, title = _("QR code")):
1838 d = QRDialog(data, self, title)
1842 def do_protect(self, func, args):
1843 if self.wallet.use_encryption:
1844 password = self.password_dialog()
1850 if args != (False,):
1851 args = (self,) + args + (password,)
1853 args = (self,password)
1857 def show_public_keys(self, address):
1858 if not address: return
1860 pubkey_list = self.wallet.get_public_keys(address)
1861 except Exception as e:
1862 traceback.print_exc(file=sys.stdout)
1863 self.show_message(str(e))
1867 d.setMinimumSize(600, 200)
1869 vbox = QVBoxLayout()
1870 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1871 vbox.addWidget( QLabel(_("Public key") + ':'))
1873 keys.setReadOnly(True)
1874 keys.setText('\n'.join(pubkey_list))
1875 vbox.addWidget(keys)
1876 vbox.addLayout(close_button(d))
1881 def show_private_key(self, address, password):
1882 if not address: return
1884 pk_list = self.wallet.get_private_key(address, password)
1885 except Exception as e:
1886 traceback.print_exc(file=sys.stdout)
1887 self.show_message(str(e))
1891 d.setMinimumSize(600, 200)
1893 vbox = QVBoxLayout()
1894 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1895 vbox.addWidget( QLabel(_("Private key") + ':'))
1897 keys.setReadOnly(True)
1898 keys.setText('\n'.join(pk_list))
1899 vbox.addWidget(keys)
1900 vbox.addLayout(close_button(d))
1906 def do_sign(self, address, message, signature, password):
1907 message = unicode(message.toPlainText())
1908 message = message.encode('utf-8')
1910 sig = self.wallet.sign_message(str(address.text()), message, password)
1911 signature.setText(sig)
1912 except Exception as e:
1913 self.show_message(str(e))
1915 def do_verify(self, address, message, signature):
1916 message = unicode(message.toPlainText())
1917 message = message.encode('utf-8')
1918 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1919 self.show_message(_("Signature verified"))
1921 self.show_message(_("Error: wrong signature"))
1924 def sign_verify_message(self, address=''):
1927 d.setWindowTitle(_('Sign/verify Message'))
1928 d.setMinimumSize(410, 290)
1930 layout = QGridLayout(d)
1932 message_e = QTextEdit()
1933 layout.addWidget(QLabel(_('Message')), 1, 0)
1934 layout.addWidget(message_e, 1, 1)
1935 layout.setRowStretch(2,3)
1937 address_e = QLineEdit()
1938 address_e.setText(address)
1939 layout.addWidget(QLabel(_('Address')), 2, 0)
1940 layout.addWidget(address_e, 2, 1)
1942 signature_e = QTextEdit()
1943 layout.addWidget(QLabel(_('Signature')), 3, 0)
1944 layout.addWidget(signature_e, 3, 1)
1945 layout.setRowStretch(3,1)
1947 hbox = QHBoxLayout()
1949 b = QPushButton(_("Sign"))
1950 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1953 b = QPushButton(_("Verify"))
1954 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1957 b = QPushButton(_("Close"))
1958 b.clicked.connect(d.accept)
1960 layout.addLayout(hbox, 4, 1)
1965 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1967 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1968 message_e.setText(decrypted)
1969 except Exception as e:
1970 self.show_message(str(e))
1973 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1974 message = unicode(message_e.toPlainText())
1975 message = message.encode('utf-8')
1977 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1978 encrypted_e.setText(encrypted)
1979 except Exception as e:
1980 self.show_message(str(e))
1984 def encrypt_message(self, address = ''):
1987 d.setWindowTitle(_('Encrypt/decrypt Message'))
1988 d.setMinimumSize(610, 490)
1990 layout = QGridLayout(d)
1992 message_e = QTextEdit()
1993 layout.addWidget(QLabel(_('Message')), 1, 0)
1994 layout.addWidget(message_e, 1, 1)
1995 layout.setRowStretch(2,3)
1997 pubkey_e = QLineEdit()
1999 pubkey = self.wallet.get_public_keys(address)[0]
2000 pubkey_e.setText(pubkey)
2001 layout.addWidget(QLabel(_('Public key')), 2, 0)
2002 layout.addWidget(pubkey_e, 2, 1)
2004 encrypted_e = QTextEdit()
2005 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
2006 layout.addWidget(encrypted_e, 3, 1)
2007 layout.setRowStretch(3,1)
2009 hbox = QHBoxLayout()
2010 b = QPushButton(_("Encrypt"))
2011 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2014 b = QPushButton(_("Decrypt"))
2015 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2018 b = QPushButton(_("Close"))
2019 b.clicked.connect(d.accept)
2022 layout.addLayout(hbox, 4, 1)
2026 def question(self, msg):
2027 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2029 def show_message(self, msg):
2030 QMessageBox.information(self, _('Message'), msg, _('OK'))
2032 def password_dialog(self, msg=None):
2035 d.setWindowTitle(_("Enter Password"))
2040 vbox = QVBoxLayout()
2042 msg = _('Please enter your password')
2043 vbox.addWidget(QLabel(msg))
2045 grid = QGridLayout()
2047 grid.addWidget(QLabel(_('Password')), 1, 0)
2048 grid.addWidget(pw, 1, 1)
2049 vbox.addLayout(grid)
2051 vbox.addLayout(ok_cancel_buttons(d))
2054 run_hook('password_dialog', pw, grid, 1)
2055 if not d.exec_(): return
2056 return unicode(pw.text())
2065 def tx_from_text(self, txt):
2066 "json or raw hexadecimal"
2075 return Transaction.deserialize(txt)
2077 traceback.print_exc(file=sys.stdout)
2078 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2082 tx_dict = json.loads(str(txt))
2083 assert "hex" in tx_dict.keys()
2084 tx = Transaction.deserialize(tx_dict["hex"])
2085 #if tx_dict.has_key("input_info"):
2086 # input_info = json.loads(tx_dict['input_info'])
2087 # tx.add_input_info(input_info)
2090 traceback.print_exc(file=sys.stdout)
2091 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2094 def read_tx_from_qrcode(self):
2095 data = run_hook('scan_qr_hook')
2098 # transactions are binary, but qrcode seems to return utf8...
2099 z = data.decode('utf8')
2103 data = s.encode('hex')
2104 tx = self.tx_from_text(data)
2107 self.show_transaction(tx)
2110 def read_tx_from_file(self):
2111 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2115 with open(fileName, "r") as f:
2116 file_content = f.read()
2117 except (ValueError, IOError, os.error), reason:
2118 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2120 return self.tx_from_text(file_content)
2124 def sign_raw_transaction(self, tx, password):
2126 self.wallet.signrawtransaction(tx, [], password)
2127 except Exception as e:
2128 traceback.print_exc(file=sys.stdout)
2129 QMessageBox.warning(self, _("Error"), str(e))
2131 def do_process_from_text(self):
2132 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2135 tx = self.tx_from_text(text)
2137 self.show_transaction(tx)
2139 def do_process_from_file(self):
2140 tx = self.read_tx_from_file()
2142 self.show_transaction(tx)
2144 def do_process_from_txid(self):
2145 from electrum_nvc import transaction
2146 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2148 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2150 tx = transaction.Transaction.deserialize(r)
2152 self.show_transaction(tx)
2154 self.show_message("unknown transaction")
2156 def do_process_from_csvReader(self, csvReader):
2161 for position, row in enumerate(csvReader):
2163 if not is_address(address):
2164 errors.append((position, address))
2166 amount = Decimal(row[1])
2167 amount = int(1000000*amount)
2168 outputs.append(('address', address, amount))
2169 except (ValueError, IOError, os.error), reason:
2170 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2174 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2175 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2179 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2180 except Exception as e:
2181 self.show_message(str(e))
2184 self.show_transaction(tx)
2186 def do_process_from_csv_file(self):
2187 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2191 with open(fileName, "r") as f:
2192 csvReader = csv.reader(f)
2193 self.do_process_from_csvReader(csvReader)
2194 except (ValueError, IOError, os.error), reason:
2195 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2198 def do_process_from_csv_text(self):
2199 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2200 + _("Format: address, amount. One output per line"), _("Load CSV"))
2203 f = StringIO.StringIO(text)
2204 csvReader = csv.reader(f)
2205 self.do_process_from_csvReader(csvReader)
2210 def export_privkeys_dialog(self, password):
2211 if self.wallet.is_watching_only():
2212 self.show_message(_("This is a watching-only wallet"))
2216 d.setWindowTitle(_('Private keys'))
2217 d.setMinimumSize(850, 300)
2218 vbox = QVBoxLayout(d)
2220 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2221 _("Exposing a single private key can compromise your entire wallet!"),
2222 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2223 vbox.addWidget(QLabel(msg))
2229 defaultname = 'electrum-private-keys.csv'
2230 select_msg = _('Select file to export your private keys to')
2231 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2232 vbox.addLayout(hbox)
2234 h, b = ok_cancel_buttons2(d, _('Export'))
2239 addresses = self.wallet.addresses(True)
2241 def privkeys_thread():
2242 for addr in addresses:
2246 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2247 d.emit(SIGNAL('computing_privkeys'))
2248 d.emit(SIGNAL('show_privkeys'))
2250 def show_privkeys():
2251 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2255 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2256 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2257 threading.Thread(target=privkeys_thread).start()
2263 filename = filename_e.text()
2268 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2269 except (IOError, os.error), reason:
2270 export_error_label = _("Electrum was unable to produce a private key-export.")
2271 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2273 except Exception as e:
2274 self.show_message(str(e))
2277 self.show_message(_("Private keys exported."))
2280 def do_export_privkeys(self, fileName, pklist, is_csv):
2281 with open(fileName, "w+") as f:
2283 transaction = csv.writer(f)
2284 transaction.writerow(["address", "private_key"])
2285 for addr, pk in pklist.items():
2286 transaction.writerow(["%34s"%addr,pk])
2289 f.write(json.dumps(pklist, indent = 4))
2292 def do_import_labels(self):
2293 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2294 if not labelsFile: return
2296 f = open(labelsFile, 'r')
2299 for key, value in json.loads(data).items():
2300 self.wallet.set_label(key, value)
2301 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2302 except (IOError, os.error), reason:
2303 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2306 def do_export_labels(self):
2307 labels = self.wallet.labels
2309 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2311 with open(fileName, 'w+') as f:
2312 json.dump(labels, f)
2313 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2314 except (IOError, os.error), reason:
2315 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2318 def export_history_dialog(self):
2321 d.setWindowTitle(_('Export History'))
2322 d.setMinimumSize(400, 200)
2323 vbox = QVBoxLayout(d)
2325 defaultname = os.path.expanduser('~/electrum-nvc-history.csv')
2326 select_msg = _('Select file to export your wallet transactions to')
2328 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2329 vbox.addLayout(hbox)
2333 h, b = ok_cancel_buttons2(d, _('Export'))
2338 filename = filename_e.text()
2343 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2344 except (IOError, os.error), reason:
2345 export_error_label = _("Electrum was unable to produce a transaction export.")
2346 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2349 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2352 def do_export_history(self, wallet, fileName, is_csv):
2353 history = wallet.get_tx_history()
2355 for item in history:
2356 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2358 if timestamp is not None:
2360 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2361 except [RuntimeError, TypeError, NameError] as reason:
2362 time_string = "unknown"
2365 time_string = "unknown"
2367 time_string = "pending"
2369 if value is not None:
2370 value_string = format_satoshis(value, True)
2375 fee_string = format_satoshis(fee, True)
2380 label, is_default_label = wallet.get_label(tx_hash)
2381 label = label.encode('utf-8')
2385 balance_string = format_satoshis(balance, False)
2387 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2389 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2391 with open(fileName, "w+") as f:
2393 transaction = csv.writer(f)
2394 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2396 transaction.writerow(line)
2399 f.write(json.dumps(lines, indent = 4))
2402 def sweep_key_dialog(self):
2404 d.setWindowTitle(_('Sweep private keys'))
2405 d.setMinimumSize(600, 300)
2407 vbox = QVBoxLayout(d)
2408 vbox.addWidget(QLabel(_("Enter private keys")))
2410 keys_e = QTextEdit()
2411 keys_e.setTabChangesFocus(True)
2412 vbox.addWidget(keys_e)
2414 h, address_e = address_field(self.wallet.addresses())
2418 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2419 vbox.addLayout(hbox)
2420 button.setEnabled(False)
2423 addr = str(address_e.text())
2424 if bitcoin.is_address(addr):
2428 pk = str(keys_e.toPlainText()).strip()
2429 if Wallet.is_private_key(pk):
2432 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2433 keys_e.textChanged.connect(f)
2434 address_e.textChanged.connect(f)
2438 fee = self.wallet.fee
2439 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2440 self.show_transaction(tx)
2444 def do_import_privkey(self, password):
2445 if not self.wallet.has_imported_keys():
2446 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2447 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2448 + _('Are you sure you understand what you are doing?'), 3, 4)
2451 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2454 text = str(text).split()
2459 addr = self.wallet.import_key(key, password)
2460 except Exception as e:
2466 addrlist.append(addr)
2468 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2470 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2471 self.update_address_tab()
2472 self.update_history_tab()
2475 def settings_dialog(self):
2477 d.setWindowTitle(_('Electrum Settings'))
2479 vbox = QVBoxLayout()
2480 grid = QGridLayout()
2481 grid.setColumnStretch(0,1)
2483 nz_label = QLabel(_('Display zeros') + ':')
2484 grid.addWidget(nz_label, 0, 0)
2485 nz_e = AmountEdit(None,True)
2486 nz_e.setText("%d"% self.num_zeros)
2487 grid.addWidget(nz_e, 0, 1)
2488 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2489 grid.addWidget(HelpButton(msg), 0, 2)
2490 if not self.config.is_modifiable('num_zeros'):
2491 for w in [nz_e, nz_label]: w.setEnabled(False)
2493 lang_label=QLabel(_('Language') + ':')
2494 grid.addWidget(lang_label, 1, 0)
2495 lang_combo = QComboBox()
2496 from electrum_nvc.i18n import languages
2497 lang_combo.addItems(languages.values())
2499 index = languages.keys().index(self.config.get("language",''))
2502 lang_combo.setCurrentIndex(index)
2503 grid.addWidget(lang_combo, 1, 1)
2504 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2505 if not self.config.is_modifiable('language'):
2506 for w in [lang_combo, lang_label]: w.setEnabled(False)
2509 fee_label = QLabel(_('Transaction fee') + ':')
2510 grid.addWidget(fee_label, 2, 0)
2511 fee_e = BTCAmountEdit(self.get_decimal_point)
2512 fee_e.setAmount(self.wallet.fee)
2513 grid.addWidget(fee_e, 2, 1)
2514 msg = _('Fee per kilobyte of transaction.') + '\n' \
2515 + _('Recommended value') + ': ' + self.format_amount(1000) + ' ' + self.base_unit()
2516 grid.addWidget(HelpButton(msg), 2, 2)
2517 if not self.config.is_modifiable('fee_per_kb'):
2518 for w in [fee_e, fee_label]: w.setEnabled(False)
2520 units = ['NVC', 'mNVC', 'bits']
2521 unit_label = QLabel(_('Base unit') + ':')
2522 grid.addWidget(unit_label, 3, 0)
2523 unit_combo = QComboBox()
2524 unit_combo.addItems(units)
2525 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2526 grid.addWidget(unit_combo, 3, 1)
2527 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2528 + '\n1NVC=1000mNVC.\n' \
2529 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2531 usechange_cb = QCheckBox(_('Use change addresses'))
2532 usechange_cb.setChecked(self.wallet.use_change)
2533 grid.addWidget(usechange_cb, 4, 0)
2534 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2535 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2537 block_explorers = ['explorer.novaco.in', 'novacoin.su']
2538 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2539 grid.addWidget(block_ex_label, 5, 0)
2540 block_ex_combo = QComboBox()
2541 block_ex_combo.addItems(block_explorers)
2542 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'explorer.novaco.in')))
2543 grid.addWidget(block_ex_combo, 5, 1)
2544 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2546 show_tx = self.config.get('show_before_broadcast', False)
2547 showtx_cb = QCheckBox(_('Show before broadcast'))
2548 showtx_cb.setChecked(show_tx)
2549 grid.addWidget(showtx_cb, 6, 0)
2550 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2552 vbox.addLayout(grid)
2554 vbox.addLayout(ok_cancel_buttons(d))
2558 if not d.exec_(): return
2560 fee = fee_e.get_amount()
2562 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2565 self.wallet.set_fee(fee)
2567 nz = unicode(nz_e.text())
2572 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2575 if self.num_zeros != nz:
2577 self.config.set_key('num_zeros', nz, True)
2578 self.update_history_tab()
2579 self.update_address_tab()
2581 usechange_result = usechange_cb.isChecked()
2582 if self.wallet.use_change != usechange_result:
2583 self.wallet.use_change = usechange_result
2584 self.wallet.storage.put('use_change', self.wallet.use_change)
2586 if showtx_cb.isChecked() != show_tx:
2587 self.config.set_key('show_before_broadcast', not show_tx)
2589 unit_result = units[unit_combo.currentIndex()]
2590 if self.base_unit() != unit_result:
2591 if unit_result == 'NVC':
2592 self.decimal_point = 6
2593 elif unit_result == 'mNVC':
2594 self.decimal_point = 3
2595 elif unit_result == 'bits':
2596 self.decimal_point = 2
2598 raise Exception('Unknown base unit')
2599 self.config.set_key('decimal_point', self.decimal_point, True)
2600 self.update_history_tab()
2601 self.update_status()
2603 need_restart = False
2605 lang_request = languages.keys()[lang_combo.currentIndex()]
2606 if lang_request != self.config.get('language'):
2607 self.config.set_key("language", lang_request, True)
2610 be_result = block_explorers[block_ex_combo.currentIndex()]
2611 self.config.set_key('block_explorer', be_result, True)
2613 run_hook('close_settings_dialog')
2616 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2619 def run_network_dialog(self):
2620 if not self.network:
2622 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2624 def closeEvent(self, event):
2626 self.config.set_key("is_maximized", self.isMaximized())
2627 if not self.isMaximized():
2629 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2630 self.save_column_widths()
2631 self.config.set_key("console-history", self.console.history[-50:], True)
2632 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2636 def plugins_dialog(self):
2637 from electrum_nvc.plugins import plugins
2640 d.setWindowTitle(_('Electrum Plugins'))
2643 vbox = QVBoxLayout(d)
2646 scroll = QScrollArea()
2647 scroll.setEnabled(True)
2648 scroll.setWidgetResizable(True)
2649 scroll.setMinimumSize(400,250)
2650 vbox.addWidget(scroll)
2654 w.setMinimumHeight(len(plugins)*35)
2656 grid = QGridLayout()
2657 grid.setColumnStretch(0,1)
2660 def do_toggle(cb, p, w):
2663 if w: w.setEnabled(r)
2665 def mk_toggle(cb, p, w):
2666 return lambda: do_toggle(cb,p,w)
2668 for i, p in enumerate(plugins):
2670 cb = QCheckBox(p.fullname())
2671 cb.setDisabled(not p.is_available())
2672 cb.setChecked(p.is_enabled())
2673 grid.addWidget(cb, i, 0)
2674 if p.requires_settings():
2675 w = p.settings_widget(self)
2676 w.setEnabled( p.is_enabled() )
2677 grid.addWidget(w, i, 1)
2680 cb.clicked.connect(mk_toggle(cb,p,w))
2681 grid.addWidget(HelpButton(p.description()), i, 2)
2683 print_msg(_("Error: cannot display plugin"), p)
2684 traceback.print_exc(file=sys.stdout)
2685 grid.setRowStretch(i+1,1)
2687 vbox.addLayout(close_button(d))
2692 def show_account_details(self, k):
2693 account = self.wallet.accounts[k]
2696 d.setWindowTitle(_('Account Details'))
2699 vbox = QVBoxLayout(d)
2700 name = self.wallet.get_account_name(k)
2701 label = QLabel('Name: ' + name)
2702 vbox.addWidget(label)
2704 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2706 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2708 vbox.addWidget(QLabel(_('Master Public Key:')))
2711 text.setReadOnly(True)
2712 text.setMaximumHeight(170)
2713 vbox.addWidget(text)
2715 mpk_text = '\n'.join( account.get_master_pubkeys() )
2716 text.setText(mpk_text)
2718 vbox.addLayout(close_button(d))