1#!/usr/local/bin/python3.8 2# 3# Electrum - lightweight Bitcoin client 4# Copyright (C) 2012 thomasv@gitorious 5# 6# Permission is hereby granted, free of charge, to any person 7# obtaining a copy of this software and associated documentation files 8# (the "Software"), to deal in the Software without restriction, 9# including without limitation the rights to use, copy, modify, merge, 10# publish, distribute, sublicense, and/or sell copies of the Software, 11# and to permit persons to whom the Software is furnished to do so, 12# subject to the following conditions: 13# 14# The above copyright notice and this permission notice shall be 15# included in all copies or substantial portions of the Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24# SOFTWARE. 25import sys 26import time 27import threading 28import os 29import traceback 30import json 31import shutil 32import weakref 33import csv 34from decimal import Decimal 35import base64 36from functools import partial 37import queue 38import asyncio 39from typing import Optional, TYPE_CHECKING, Sequence, List, Union 40 41from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont 42from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal, QPoint 43from PyQt5.QtCore import QTimer 44from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget, 45 QMenuBar, QFileDialog, QCheckBox, QLabel, 46 QVBoxLayout, QGridLayout, QLineEdit, 47 QHBoxLayout, QPushButton, QScrollArea, QTextEdit, 48 QShortcut, QMainWindow, QCompleter, QInputDialog, 49 QWidget, QSizePolicy, QStatusBar, QToolTip, QDialog, 50 QMenu, QAction, QStackedWidget, QToolButton) 51 52import electrum 53from electrum.gui import messages 54from electrum import (keystore, ecc, constants, util, bitcoin, commands, 55 paymentrequest, lnutil) 56from electrum.bitcoin import COIN, is_address 57from electrum.plugin import run_hook, BasePlugin 58from electrum.i18n import _ 59from electrum.util import (format_time, 60 UserCancelled, profiler, 61 bh2u, bfh, InvalidPassword, 62 UserFacingException, 63 get_new_wallet_name, send_exception_to_crash_reporter, 64 InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds, 65 NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs, 66 AddTransactionException, BITCOIN_BIP21_URI_SCHEME, 67 InvoiceError) 68from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice 69from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice 70from electrum.transaction import (Transaction, PartialTxInput, 71 PartialTransaction, PartialTxOutput) 72from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, 73 sweep_preparations, InternalAddressCorruption, 74 CannotDoubleSpendTx, CannotCPFP) 75from electrum.version import ELECTRUM_VERSION 76from electrum.network import (Network, TxBroadcastError, BestEffortRequestFailed, 77 UntrustedServerReturnedError, NetworkException) 78from electrum.exchange_rate import FxThread 79from electrum.simple_config import SimpleConfig 80from electrum.logging import Logger 81from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError 82from electrum.lnaddr import lndecode, LnDecodeException 83 84from .exception_window import Exception_Hook 85from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit, SizedFreezableLineEdit 86from .qrcodewidget import QRCodeWidget, QRDialog 87from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit 88from .transaction_dialog import show_transaction 89from .fee_slider import FeeSlider, FeeComboBox 90from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog, 91 WindowModalDialog, ChoicesLayout, HelpLabel, Buttons, 92 OkButton, InfoButton, WWLabel, TaskThread, CancelButton, 93 CloseButton, HelpButton, MessageBoxMixin, EnterButton, 94 import_meta_gui, export_meta_gui, 95 filename_field, address_field, char_width_in_lineedit, webopen, 96 TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT, 97 getOpenFileName, getSaveFileName, BlockingWaitingDialog) 98from .util import ButtonsTextEdit, ButtonsLineEdit 99from .installwizard import WIF_HELP_TEXT 100from .history_list import HistoryList, HistoryModel 101from .update_checker import UpdateCheck, UpdateCheckThread 102from .channels_list import ChannelsList 103from .confirm_tx_dialog import ConfirmTxDialog 104from .transaction_dialog import PreviewTxDialog 105from .rbf_dialog import BumpFeeDialog, DSCancelDialog 106from .qrreader import scan_qrcode 107 108if TYPE_CHECKING: 109 from . import ElectrumGui 110 111 112LN_NUM_PAYMENT_ATTEMPTS = 10 113 114 115class StatusBarButton(QToolButton): 116 # note: this class has a custom stylesheet applied in stylesheet_patcher.py 117 def __init__(self, icon, tooltip, func): 118 QToolButton.__init__(self) 119 self.setText('') 120 self.setIcon(icon) 121 self.setToolTip(tooltip) 122 self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 123 self.setAutoRaise(True) 124 self.setMaximumWidth(25) 125 self.clicked.connect(self.onPress) 126 self.func = func 127 self.setIconSize(QSize(25,25)) 128 self.setCursor(QCursor(Qt.PointingHandCursor)) 129 130 def onPress(self, checked=False): 131 '''Drops the unwanted PyQt5 "checked" argument''' 132 self.func() 133 134 def keyPressEvent(self, e): 135 if e.key() in [Qt.Key_Return, Qt.Key_Enter]: 136 self.func() 137 138 139def protected(func): 140 '''Password request wrapper. The password is passed to the function 141 as the 'password' named argument. "None" indicates either an 142 unencrypted wallet, or the user cancelled the password request. 143 An empty input is passed as the empty string.''' 144 def request_password(self, *args, **kwargs): 145 parent = self.top_level_window() 146 password = None 147 while self.wallet.has_keystore_encryption(): 148 password = self.password_dialog(parent=parent) 149 if password is None: 150 # User cancelled password input 151 return 152 try: 153 self.wallet.check_password(password) 154 break 155 except Exception as e: 156 self.show_error(str(e), parent=parent) 157 continue 158 159 kwargs['password'] = password 160 return func(self, *args, **kwargs) 161 return request_password 162 163 164class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): 165 166 payment_request_ok_signal = pyqtSignal() 167 payment_request_error_signal = pyqtSignal() 168 network_signal = pyqtSignal(str, object) 169 #ln_payment_attempt_signal = pyqtSignal(str) 170 alias_received_signal = pyqtSignal() 171 computing_privkeys_signal = pyqtSignal() 172 show_privkeys_signal = pyqtSignal() 173 show_error_signal = pyqtSignal(str) 174 175 payment_request: Optional[paymentrequest.PaymentRequest] 176 177 def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): 178 QMainWindow.__init__(self) 179 180 self.gui_object = gui_object 181 self.config = config = gui_object.config # type: SimpleConfig 182 self.gui_thread = gui_object.gui_thread 183 assert wallet, "no wallet" 184 self.wallet = wallet 185 if wallet.has_lightning(): 186 self.wallet.config.set_key('show_channels_tab', True) 187 188 Exception_Hook.maybe_setup(config=self.config, wallet=self.wallet) 189 190 self.network = gui_object.daemon.network # type: Network 191 self.fx = gui_object.daemon.fx # type: FxThread 192 self.contacts = wallet.contacts 193 self.tray = gui_object.tray 194 self.app = gui_object.app 195 self._cleaned_up = False 196 self.payment_request = None # type: Optional[paymentrequest.PaymentRequest] 197 self.payto_URI = None 198 self.checking_accounts = False 199 self.qr_window = None 200 self.pluginsdialog = None 201 self.showing_cert_mismatch_error = False 202 self.tl_windows = [] 203 self.pending_invoice = None 204 Logger.__init__(self) 205 206 self.tx_notification_queue = queue.Queue() 207 self.tx_notification_last_time = 0 208 209 self.create_status_bar() 210 self.need_update = threading.Event() 211 212 self.completions = QStringListModel() 213 214 coincontrol_sb = self.create_coincontrol_statusbar() 215 216 self.tabs = tabs = QTabWidget(self) 217 self.send_tab = self.create_send_tab() 218 self.receive_tab = self.create_receive_tab() 219 self.addresses_tab = self.create_addresses_tab() 220 self.utxo_tab = self.create_utxo_tab() 221 self.console_tab = self.create_console_tab() 222 self.contacts_tab = self.create_contacts_tab() 223 self.channels_tab = self.create_channels_tab() 224 tabs.addTab(self.create_history_tab(), read_QIcon("tab_history.png"), _('History')) 225 tabs.addTab(self.send_tab, read_QIcon("tab_send.png"), _('Send')) 226 tabs.addTab(self.receive_tab, read_QIcon("tab_receive.png"), _('Receive')) 227 228 def add_optional_tab(tabs, tab, icon, description, name): 229 tab.tab_icon = icon 230 tab.tab_description = description 231 tab.tab_pos = len(tabs) 232 tab.tab_name = name 233 if self.config.get('show_{}_tab'.format(name), False): 234 tabs.addTab(tab, icon, description.replace("&", "")) 235 236 add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses"), "addresses") 237 add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels") 238 add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins"), "utxo") 239 add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts"), "contacts") 240 add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole"), "console") 241 242 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 243 244 central_widget = QScrollArea() 245 vbox = QVBoxLayout(central_widget) 246 vbox.setContentsMargins(0, 0, 0, 0) 247 vbox.addWidget(tabs) 248 vbox.addWidget(coincontrol_sb) 249 250 self.setCentralWidget(central_widget) 251 252 self.setMinimumWidth(640) 253 self.setMinimumHeight(400) 254 if self.config.get("is_maximized"): 255 self.showMaximized() 256 257 self.setWindowIcon(read_QIcon("electrum.png")) 258 self.init_menubar() 259 260 wrtabs = weakref.proxy(tabs) 261 QShortcut(QKeySequence("Ctrl+W"), self, self.close) 262 QShortcut(QKeySequence("Ctrl+Q"), self, self.close) 263 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet) 264 QShortcut(QKeySequence("F5"), self, self.update_wallet) 265 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() - 1)%wrtabs.count())) 266 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() + 1)%wrtabs.count())) 267 268 for i in range(wrtabs.count()): 269 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: wrtabs.setCurrentIndex(i)) 270 271 self.payment_request_ok_signal.connect(self.payment_request_ok) 272 self.payment_request_error_signal.connect(self.payment_request_error) 273 self.show_error_signal.connect(self.show_error) 274 self.history_list.setFocus(True) 275 276 # network callbacks 277 if self.network: 278 self.network_signal.connect(self.on_network_qt) 279 interests = ['wallet_updated', 'network_updated', 'blockchain_updated', 280 'new_transaction', 'status', 281 'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes', 282 'on_history', 'channel', 'channels_updated', 283 'payment_failed', 'payment_succeeded', 284 'invoice_status', 'request_status', 'ln_gossip_sync_progress', 285 'cert_mismatch', 'gossip_db_loaded'] 286 # To avoid leaking references to "self" that prevent the 287 # window from being GC-ed when closed, callbacks should be 288 # methods of this class only, and specifically not be 289 # partials, lambdas or methods of subobjects. Hence... 290 util.register_callback(self.on_network, interests) 291 # set initial message 292 self.console.showMessage(self.network.banner) 293 294 # update fee slider in case we missed the callback 295 #self.fee_slider.update() 296 self.load_wallet(wallet) 297 gui_object.timer.timeout.connect(self.timer_actions) 298 self.fetch_alias() 299 300 # If the option hasn't been set yet 301 if config.get('check_updates') is None: 302 choice = self.question(title="Electrum - " + _("Enable update check"), 303 msg=_("For security reasons we advise that you always use the latest version of Electrum.") + " " + 304 _("Would you like to be notified when there is a newer version of Electrum available?")) 305 config.set_key('check_updates', bool(choice), save=True) 306 307 self._update_check_thread = None 308 if config.get('check_updates', False): 309 # The references to both the thread and the window need to be stored somewhere 310 # to prevent GC from getting in our way. 311 def on_version_received(v): 312 if UpdateCheck.is_newer(v): 313 self.update_check_button.setText(_("Update to Electrum {} is available").format(v)) 314 self.update_check_button.clicked.connect(lambda: self.show_update_check(v)) 315 self.update_check_button.show() 316 self._update_check_thread = UpdateCheckThread() 317 self._update_check_thread.checked.connect(on_version_received) 318 self._update_check_thread.start() 319 320 def run_coroutine_from_thread(self, coro, on_result=None): 321 def task(): 322 try: 323 f = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) 324 r = f.result() 325 if on_result: 326 on_result(r) 327 except Exception as e: 328 self.logger.exception("exception in coro scheduled via window.wallet") 329 self.show_error_signal.emit(str(e)) 330 self.wallet.thread.add(task) 331 332 def on_fx_history(self): 333 self.history_model.refresh('fx_history') 334 self.address_list.update() 335 336 def on_fx_quotes(self): 337 self.update_status() 338 # Refresh edits with the new rate 339 edit = self.fiat_send_e if self.fiat_send_e.is_last_edited else self.amount_e 340 edit.textEdited.emit(edit.text()) 341 edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e 342 edit.textEdited.emit(edit.text()) 343 # History tab needs updating if it used spot 344 if self.fx.history_used_spot: 345 self.history_model.refresh('fx_quotes') 346 self.address_list.update() 347 348 def toggle_tab(self, tab): 349 show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) 350 self.config.set_key('show_{}_tab'.format(tab.tab_name), show) 351 item_text = (_("Hide {}") if show else _("Show {}")).format(tab.tab_description) 352 tab.menu_action.setText(item_text) 353 if show: 354 # Find out where to place the tab 355 index = len(self.tabs) 356 for i in range(len(self.tabs)): 357 try: 358 if tab.tab_pos < self.tabs.widget(i).tab_pos: 359 index = i 360 break 361 except AttributeError: 362 pass 363 self.tabs.insertTab(index, tab, tab.tab_icon, tab.tab_description.replace("&", "")) 364 else: 365 i = self.tabs.indexOf(tab) 366 self.tabs.removeTab(i) 367 368 def push_top_level_window(self, window): 369 '''Used for e.g. tx dialog box to ensure new dialogs are appropriately 370 parented. This used to be done by explicitly providing the parent 371 window, but that isn't something hardware wallet prompts know.''' 372 self.tl_windows.append(window) 373 374 def pop_top_level_window(self, window): 375 self.tl_windows.remove(window) 376 377 def top_level_window(self, test_func=None): 378 '''Do the right thing in the presence of tx dialog windows''' 379 override = self.tl_windows[-1] if self.tl_windows else None 380 if override and test_func and not test_func(override): 381 override = None # only override if ok for test_func 382 return self.top_level_window_recurse(override, test_func) 383 384 def diagnostic_name(self): 385 #return '{}:{}'.format(self.__class__.__name__, self.wallet.diagnostic_name()) 386 return self.wallet.diagnostic_name() 387 388 def is_hidden(self): 389 return self.isMinimized() or self.isHidden() 390 391 def show_or_hide(self): 392 if self.is_hidden(): 393 self.bring_to_top() 394 else: 395 self.hide() 396 397 def bring_to_top(self): 398 self.show() 399 self.raise_() 400 401 def on_error(self, exc_info): 402 e = exc_info[1] 403 if isinstance(e, UserCancelled): 404 pass 405 elif isinstance(e, UserFacingException): 406 self.show_error(str(e)) 407 else: 408 # TODO would be nice if we just sent these to the crash reporter... 409 # anything we don't want to send there, we should explicitly catch 410 # send_exception_to_crash_reporter(e) 411 try: 412 self.logger.error("on_error", exc_info=exc_info) 413 except OSError: 414 pass # see #4418 415 self.show_error(repr(e)) 416 417 def on_network(self, event, *args): 418 # Handle in GUI thread 419 self.network_signal.emit(event, args) 420 421 def on_network_qt(self, event, args=None): 422 # Handle a network message in the GUI thread 423 # note: all windows get events from all wallets! 424 if event == 'wallet_updated': 425 wallet = args[0] 426 if wallet == self.wallet: 427 self.need_update.set() 428 elif event == 'network_updated': 429 self.gui_object.network_updated_signal_obj.network_updated_signal \ 430 .emit(event, args) 431 self.network_signal.emit('status', None) 432 elif event == 'blockchain_updated': 433 # to update number of confirmations in history 434 self.need_update.set() 435 elif event == 'new_transaction': 436 wallet, tx = args 437 if wallet == self.wallet: 438 self.tx_notification_queue.put(tx) 439 elif event == 'on_quotes': 440 self.on_fx_quotes() 441 elif event == 'on_history': 442 self.on_fx_history() 443 elif event == 'gossip_db_loaded': 444 self.channels_list.gossip_db_loaded.emit(*args) 445 elif event == 'channels_updated': 446 wallet = args[0] 447 if wallet == self.wallet: 448 self.channels_list.update_rows.emit(*args) 449 elif event == 'channel': 450 wallet = args[0] 451 if wallet == self.wallet: 452 self.channels_list.update_single_row.emit(*args) 453 self.update_status() 454 elif event == 'request_status': 455 self.on_request_status(*args) 456 elif event == 'invoice_status': 457 self.on_invoice_status(*args) 458 elif event == 'payment_succeeded': 459 wallet = args[0] 460 if wallet == self.wallet: 461 self.on_payment_succeeded(*args) 462 elif event == 'payment_failed': 463 wallet = args[0] 464 if wallet == self.wallet: 465 self.on_payment_failed(*args) 466 elif event == 'status': 467 self.update_status() 468 elif event == 'banner': 469 self.console.showMessage(args[0]) 470 elif event == 'verified': 471 wallet, tx_hash, tx_mined_status = args 472 if wallet == self.wallet: 473 self.history_model.update_tx_mined_status(tx_hash, tx_mined_status) 474 elif event == 'fee': 475 pass 476 elif event == 'fee_histogram': 477 self.history_model.on_fee_histogram() 478 elif event == 'ln_gossip_sync_progress': 479 self.update_lightning_icon() 480 elif event == 'cert_mismatch': 481 self.show_cert_mismatch_error() 482 else: 483 self.logger.info(f"unexpected network event: {event} {args}") 484 485 def fetch_alias(self): 486 self.alias_info = None 487 alias = self.config.get('alias') 488 if alias: 489 alias = str(alias) 490 def f(): 491 self.alias_info = self.contacts.resolve_openalias(alias) 492 self.alias_received_signal.emit() 493 t = threading.Thread(target=f) 494 t.setDaemon(True) 495 t.start() 496 497 def close_wallet(self): 498 if self.wallet: 499 self.logger.info(f'close_wallet {self.wallet.storage.path}') 500 run_hook('close_wallet', self.wallet) 501 502 @profiler 503 def load_wallet(self, wallet: Abstract_Wallet): 504 wallet.thread = TaskThread(self, self.on_error) 505 self.update_recently_visited(wallet.storage.path) 506 if wallet.has_lightning(): 507 util.trigger_callback('channels_updated', wallet) 508 self.need_update.set() 509 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized 510 # update menus 511 self.seed_menu.setEnabled(self.wallet.has_seed()) 512 self.update_lock_icon() 513 self.update_buttons_on_seed() 514 self.update_console() 515 self.clear_receive_tab() 516 self.request_list.update() 517 self.channels_list.update() 518 self.tabs.show() 519 self.init_geometry() 520 if self.config.get('hide_gui') and self.gui_object.tray.isVisible(): 521 self.hide() 522 else: 523 self.show() 524 self.watching_only_changed() 525 run_hook('load_wallet', wallet, self) 526 try: 527 wallet.try_detecting_internal_addresses_corruption() 528 except InternalAddressCorruption as e: 529 self.show_error(str(e)) 530 send_exception_to_crash_reporter(e) 531 532 def init_geometry(self): 533 winpos = self.wallet.db.get("winpos-qt") 534 try: 535 screen = self.app.desktop().screenGeometry() 536 assert screen.contains(QRect(*winpos)) 537 self.setGeometry(*winpos) 538 except: 539 self.logger.info("using default geometry") 540 self.setGeometry(100, 100, 840, 400) 541 542 def watching_only_changed(self): 543 name = "Electrum" 544 if constants.net.TESTNET: 545 name += " " + constants.net.NET_NAME.capitalize() 546 title = '%s %s - %s' % (name, ELECTRUM_VERSION, 547 self.wallet.basename()) 548 extra = [self.wallet.db.get('wallet_type', '?')] 549 if self.wallet.is_watching_only(): 550 extra.append(_('watching only')) 551 title += ' [%s]'% ', '.join(extra) 552 self.setWindowTitle(title) 553 self.password_menu.setEnabled(self.wallet.may_have_password()) 554 self.import_privkey_menu.setVisible(self.wallet.can_import_privkey()) 555 self.import_address_menu.setVisible(self.wallet.can_import_address()) 556 self.export_menu.setEnabled(self.wallet.can_export()) 557 558 def warn_if_watching_only(self): 559 if self.wallet.is_watching_only(): 560 msg = ' '.join([ 561 _("This wallet is watching-only."), 562 _("This means you will not be able to spend Bitcoins with it."), 563 _("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.") 564 ]) 565 self.show_warning(msg, title=_('Watch-only wallet')) 566 567 def warn_if_testnet(self): 568 if not constants.net.TESTNET: 569 return 570 # user might have opted out already 571 if self.config.get('dont_show_testnet_warning', False): 572 return 573 # only show once per process lifecycle 574 if getattr(self.gui_object, '_warned_testnet', False): 575 return 576 self.gui_object._warned_testnet = True 577 msg = ''.join([ 578 _("You are in testnet mode."), ' ', 579 _("Testnet coins are worthless."), '\n', 580 _("Testnet is separate from the main Bitcoin network. It is used for testing.") 581 ]) 582 cb = QCheckBox(_("Don't show this again.")) 583 cb_checked = False 584 def on_cb(x): 585 nonlocal cb_checked 586 cb_checked = x == Qt.Checked 587 cb.stateChanged.connect(on_cb) 588 self.show_warning(msg, title=_('Testnet'), checkbox=cb) 589 if cb_checked: 590 self.config.set_key('dont_show_testnet_warning', True) 591 592 def open_wallet(self): 593 try: 594 wallet_folder = self.get_wallet_folder() 595 except FileNotFoundError as e: 596 self.show_error(str(e)) 597 return 598 filename, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) 599 if not filename: 600 return 601 self.gui_object.new_window(filename) 602 603 def select_backup_dir(self, b): 604 name = self.config.get('backup_dir', '') 605 dirname = QFileDialog.getExistingDirectory(self, "Select your wallet backup directory", name) 606 if dirname: 607 self.config.set_key('backup_dir', dirname) 608 self.backup_dir_e.setText(dirname) 609 610 def backup_wallet(self): 611 d = WindowModalDialog(self, _("File Backup")) 612 vbox = QVBoxLayout(d) 613 grid = QGridLayout() 614 backup_help = "" 615 backup_dir = self.config.get('backup_dir') 616 backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help) 617 msg = _('Please select a backup directory') 618 if self.wallet.has_lightning() and self.wallet.lnworker.channels: 619 msg += '\n\n' + ' '.join([ 620 _("Note that lightning channels will be converted to channel backups."), 621 _("You cannot use channel backups to perform lightning payments."), 622 _("Channel backups can only be used to request your channels to be closed.") 623 ]) 624 self.backup_dir_e = QPushButton(backup_dir) 625 self.backup_dir_e.clicked.connect(self.select_backup_dir) 626 grid.addWidget(backup_dir_label, 1, 0) 627 grid.addWidget(self.backup_dir_e, 1, 1) 628 vbox.addLayout(grid) 629 vbox.addWidget(WWLabel(msg)) 630 vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) 631 if not d.exec_(): 632 return False 633 backup_dir = self.config.get_backup_dir() 634 if backup_dir is None: 635 self.show_message(_("You need to configure a backup directory in your preferences"), title=_("Backup not configured")) 636 return 637 try: 638 new_path = self.wallet.save_backup(backup_dir) 639 except BaseException as reason: 640 self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup")) 641 return 642 msg = _("A copy of your wallet file was created in")+" '%s'" % str(new_path) 643 self.show_message(msg, title=_("Wallet backup created")) 644 return True 645 646 def update_recently_visited(self, filename): 647 recent = self.config.get('recently_open', []) 648 try: 649 sorted(recent) 650 except: 651 recent = [] 652 if filename in recent: 653 recent.remove(filename) 654 recent.insert(0, filename) 655 recent = [path for path in recent if os.path.exists(path)] 656 recent = recent[:5] 657 self.config.set_key('recently_open', recent) 658 self.recently_visited_menu.clear() 659 for i, k in enumerate(sorted(recent)): 660 b = os.path.basename(k) 661 def loader(k): 662 return lambda: self.gui_object.new_window(k) 663 self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1))) 664 self.recently_visited_menu.setEnabled(len(recent)) 665 666 def get_wallet_folder(self): 667 return os.path.dirname(os.path.abspath(self.wallet.storage.path)) 668 669 def new_wallet(self): 670 try: 671 wallet_folder = self.get_wallet_folder() 672 except FileNotFoundError as e: 673 self.show_error(str(e)) 674 return 675 filename = get_new_wallet_name(wallet_folder) 676 full_path = os.path.join(wallet_folder, filename) 677 self.gui_object.start_new_window(full_path, None) 678 679 def init_menubar(self): 680 menubar = QMenuBar() 681 682 file_menu = menubar.addMenu(_("&File")) 683 self.recently_visited_menu = file_menu.addMenu(_("&Recently open")) 684 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open) 685 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New) 686 file_menu.addAction(_("&Save backup"), self.backup_wallet).setShortcut(QKeySequence.SaveAs) 687 file_menu.addAction(_("Delete"), self.remove_wallet) 688 file_menu.addSeparator() 689 file_menu.addAction(_("&Quit"), self.close) 690 691 wallet_menu = menubar.addMenu(_("&Wallet")) 692 wallet_menu.addAction(_("&Information"), self.show_wallet_info) 693 wallet_menu.addSeparator() 694 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog) 695 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog) 696 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys")) 697 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog) 698 self.import_privkey_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey) 699 self.export_menu = self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog) 700 self.import_address_menu = wallet_menu.addAction(_("Import addresses"), self.import_addresses) 701 wallet_menu.addSeparator() 702 703 addresses_menu = wallet_menu.addMenu(_("&Addresses")) 704 addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config)) 705 labels_menu = wallet_menu.addMenu(_("&Labels")) 706 labels_menu.addAction(_("&Import"), self.do_import_labels) 707 labels_menu.addAction(_("&Export"), self.do_export_labels) 708 history_menu = wallet_menu.addMenu(_("&History")) 709 history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config)) 710 history_menu.addAction(_("&Summary"), self.history_list.show_summary) 711 history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog) 712 history_menu.addAction(_("&Export"), self.history_list.export_history_dialog) 713 contacts_menu = wallet_menu.addMenu(_("Contacts")) 714 contacts_menu.addAction(_("&New"), self.new_contact_dialog) 715 contacts_menu.addAction(_("Import"), lambda: self.import_contacts()) 716 contacts_menu.addAction(_("Export"), lambda: self.export_contacts()) 717 invoices_menu = wallet_menu.addMenu(_("Invoices")) 718 invoices_menu.addAction(_("Import"), lambda: self.import_invoices()) 719 invoices_menu.addAction(_("Export"), lambda: self.export_invoices()) 720 requests_menu = wallet_menu.addMenu(_("Requests")) 721 requests_menu.addAction(_("Import"), lambda: self.import_requests()) 722 requests_menu.addAction(_("Export"), lambda: self.export_requests()) 723 724 wallet_menu.addSeparator() 725 wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) 726 727 def add_toggle_action(view_menu, tab): 728 is_shown = self.config.get('show_{}_tab'.format(tab.tab_name), False) 729 item_name = (_("Hide") if is_shown else _("Show")) + " " + tab.tab_description 730 tab.menu_action = view_menu.addAction(item_name, lambda: self.toggle_tab(tab)) 731 732 view_menu = menubar.addMenu(_("&View")) 733 add_toggle_action(view_menu, self.addresses_tab) 734 add_toggle_action(view_menu, self.utxo_tab) 735 add_toggle_action(view_menu, self.channels_tab) 736 add_toggle_action(view_menu, self.contacts_tab) 737 add_toggle_action(view_menu, self.console_tab) 738 739 tools_menu = menubar.addMenu(_("&Tools")) # type: QMenu 740 preferences_action = tools_menu.addAction(_("Preferences"), self.settings_dialog) # type: QAction 741 if sys.platform == 'darwin': 742 # "Settings"/"Preferences" are all reserved keywords in macOS. 743 # preferences_action will get picked up based on name (and put into a standardized location, 744 # and given a standard reserved hotkey) 745 # Hence, this menu item will be at a "uniform location re macOS processes" 746 preferences_action.setMenuRole(QAction.PreferencesRole) # make sure OS recognizes it as preferences 747 # Add another preferences item, to also have a "uniform location for Electrum between different OSes" 748 tools_menu.addAction(_("Electrum preferences"), self.settings_dialog) 749 750 tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network)) 751 if self.network and self.network.local_watchtower: 752 tools_menu.addAction(_("Local &Watchtower"), self.gui_object.show_watchtower_dialog) 753 tools_menu.addAction(_("&Plugins"), self.plugins_dialog) 754 tools_menu.addSeparator() 755 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message) 756 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message) 757 tools_menu.addSeparator() 758 759 paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.paytomany) 760 761 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction")) 762 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file) 763 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text) 764 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid) 765 raw_transaction_menu.addAction(_("&From QR code"), self.read_tx_from_qrcode) 766 self.raw_transaction_menu = raw_transaction_menu 767 run_hook('init_menubar_tools', self, tools_menu) 768 769 help_menu = menubar.addMenu(_("&Help")) 770 help_menu.addAction(_("&About"), self.show_about) 771 help_menu.addAction(_("&Check for updates"), self.show_update_check) 772 help_menu.addAction(_("&Official website"), lambda: webopen("https://electrum.org")) 773 help_menu.addSeparator() 774 help_menu.addAction(_("&Documentation"), lambda: webopen("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents) 775 if not constants.net.TESTNET: 776 help_menu.addAction(_("&Bitcoin Paper"), self.show_bitcoin_paper) 777 help_menu.addAction(_("&Report Bug"), self.show_report_bug) 778 help_menu.addSeparator() 779 help_menu.addAction(_("&Donate to server"), self.donate_to_server) 780 781 self.setMenuBar(menubar) 782 783 def donate_to_server(self): 784 d = self.network.get_donation_address() 785 if d: 786 host = self.network.get_parameters().server.host 787 self.pay_to_URI('bitcoin:%s?message=donation for %s'%(d, host)) 788 else: 789 self.show_error(_('No donation address for this server')) 790 791 def show_about(self): 792 QMessageBox.about(self, "Electrum", 793 (_("Version")+" %s" % ELECTRUM_VERSION + "\n\n" + 794 _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin.") + " " + 795 _("You do not need to perform regular backups, because your wallet can be " 796 "recovered from a secret phrase that you can memorize or write on paper.") + " " + 797 _("Startup times are instant because it operates in conjunction with high-performance " 798 "servers that handle the most complicated parts of the Bitcoin system.") + "\n\n" + 799 _("Uses icons from the Icons8 icon pack (icons8.com)."))) 800 801 def show_bitcoin_paper(self): 802 filename = os.path.join(self.config.path, 'bitcoin.pdf') 803 if not os.path.exists(filename): 804 s = self._fetch_tx_from_network("54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713") 805 if not s: 806 return 807 s = s.split("0100000000000000")[1:-1] 808 out = ''.join(x[6:136] + x[138:268] + x[270:400] if len(x) > 136 else x[6:] for x in s)[16:-20] 809 with open(filename, 'wb') as f: 810 f.write(bytes.fromhex(out)) 811 webopen('file:///' + filename) 812 813 def show_update_check(self, version=None): 814 self.gui_object._update_check = UpdateCheck(latest_version=version) 815 816 def show_report_bug(self): 817 msg = ' '.join([ 818 _("Please report any bugs as issues on github:<br/>"), 819 f'''<a href="{constants.GIT_REPO_ISSUES_URL}">{constants.GIT_REPO_ISSUES_URL}</a><br/><br/>''', 820 _("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."), 821 _("Try to explain not only what the bug is, but how it occurs.") 822 ]) 823 self.show_message(msg, title="Electrum - " + _("Reporting Bugs"), rich_text=True) 824 825 def notify_transactions(self): 826 if self.tx_notification_queue.qsize() == 0: 827 return 828 if not self.wallet.up_to_date: 829 return # no notifications while syncing 830 now = time.time() 831 rate_limit = 20 # seconds 832 if self.tx_notification_last_time + rate_limit > now: 833 return 834 self.tx_notification_last_time = now 835 self.logger.info("Notifying GUI about new transactions") 836 txns = [] 837 while True: 838 try: 839 txns.append(self.tx_notification_queue.get_nowait()) 840 except queue.Empty: 841 break 842 # Combine the transactions if there are at least three 843 if len(txns) >= 3: 844 total_amount = 0 845 for tx in txns: 846 tx_wallet_delta = self.wallet.get_wallet_delta(tx) 847 if not tx_wallet_delta.is_relevant: 848 continue 849 total_amount += tx_wallet_delta.delta 850 self.notify(_("{} new transactions: Total amount received in the new transactions {}") 851 .format(len(txns), self.format_amount_and_units(total_amount))) 852 else: 853 for tx in txns: 854 tx_wallet_delta = self.wallet.get_wallet_delta(tx) 855 if not tx_wallet_delta.is_relevant: 856 continue 857 self.notify(_("New transaction: {}").format(self.format_amount_and_units(tx_wallet_delta.delta))) 858 859 def notify(self, message): 860 if self.tray: 861 try: 862 # this requires Qt 5.9 863 self.tray.showMessage("Electrum", message, read_QIcon("electrum_dark_icon"), 20000) 864 except TypeError: 865 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000) 866 867 def timer_actions(self): 868 self.request_list.refresh_status() 869 # Note this runs in the GUI thread 870 if self.need_update.is_set(): 871 self.need_update.clear() 872 self.update_wallet() 873 elif not self.wallet.up_to_date: 874 # this updates "synchronizing" progress 875 self.update_status() 876 # resolve aliases 877 # FIXME this is a blocking network call that has a timeout of 5 sec 878 self.payto_e.resolve() 879 self.notify_transactions() 880 881 def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str: 882 """Formats amount as string, converting to desired unit. 883 E.g. 500_000 -> '0.005' 884 """ 885 return self.config.format_amount(amount_sat, is_diff=is_diff, whitespaces=whitespaces) 886 887 def format_amount_and_units(self, amount_sat, *, timestamp: int = None) -> str: 888 """Returns string with both bitcoin and fiat amounts, in desired units. 889 E.g. 500_000 -> '0.005 BTC (191.42 EUR)' 890 """ 891 text = self.config.format_amount_and_units(amount_sat) 892 fiat = self.fx.format_amount_and_units(amount_sat, timestamp=timestamp) if self.fx else None 893 if text and fiat: 894 text += f' ({fiat})' 895 return text 896 897 def format_fiat_and_units(self, amount_sat) -> str: 898 """Returns string of FX fiat amount, in desired units. 899 E.g. 500_000 -> '191.42 EUR' 900 """ 901 return self.fx.format_amount_and_units(amount_sat) if self.fx else '' 902 903 def format_fee_rate(self, fee_rate): 904 return self.config.format_fee_rate(fee_rate) 905 906 def get_decimal_point(self): 907 return self.config.get_decimal_point() 908 909 def base_unit(self): 910 return self.config.get_base_unit() 911 912 def connect_fields(self, window, btc_e, fiat_e, fee_e): 913 914 def edit_changed(edit): 915 if edit.follows: 916 return 917 edit.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) 918 fiat_e.is_last_edited = (edit == fiat_e) 919 amount = edit.get_amount() 920 rate = self.fx.exchange_rate() if self.fx else Decimal('NaN') 921 if rate.is_nan() or amount is None: 922 if edit is fiat_e: 923 btc_e.setText("") 924 if fee_e: 925 fee_e.setText("") 926 else: 927 fiat_e.setText("") 928 else: 929 if edit is fiat_e: 930 btc_e.follows = True 931 btc_e.setAmount(int(amount / Decimal(rate) * COIN)) 932 btc_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) 933 btc_e.follows = False 934 if fee_e: 935 window.update_fee() 936 else: 937 fiat_e.follows = True 938 fiat_e.setText(self.fx.ccy_amount_str( 939 amount * Decimal(rate) / COIN, False)) 940 fiat_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) 941 fiat_e.follows = False 942 943 btc_e.follows = False 944 fiat_e.follows = False 945 fiat_e.textChanged.connect(partial(edit_changed, fiat_e)) 946 btc_e.textChanged.connect(partial(edit_changed, btc_e)) 947 fiat_e.is_last_edited = False 948 949 def update_status(self): 950 if not self.wallet: 951 return 952 953 if self.network is None: 954 text = _("Offline") 955 icon = read_QIcon("status_disconnected.png") 956 957 elif self.network.is_connected(): 958 server_height = self.network.get_server_height() 959 server_lag = self.network.get_local_height() - server_height 960 fork_str = "_fork" if len(self.network.get_blockchains())>1 else "" 961 # Server height can be 0 after switching to a new server 962 # until we get a headers subscription request response. 963 # Display the synchronizing message in that case. 964 if not self.wallet.up_to_date or server_height == 0: 965 num_sent, num_answered = self.wallet.get_history_sync_state_details() 966 text = ("{} ({}/{})" 967 .format(_("Synchronizing..."), num_answered, num_sent)) 968 icon = read_QIcon("status_waiting.png") 969 elif server_lag > 1: 970 text = _("Server is lagging ({} blocks)").format(server_lag) 971 icon = read_QIcon("status_lagging%s.png"%fork_str) 972 else: 973 c, u, x = self.wallet.get_balance() 974 text = _("Balance") + ": %s "%(self.format_amount_and_units(c)) 975 if u: 976 text += " [%s unconfirmed]"%(self.format_amount(u, is_diff=True).strip()) 977 if x: 978 text += " [%s unmatured]"%(self.format_amount(x, is_diff=True).strip()) 979 if self.wallet.has_lightning(): 980 l = self.wallet.lnworker.get_balance() 981 text += u' \U000026a1 %s'%(self.format_amount_and_units(l).strip()) 982 # append fiat balance and price 983 if self.fx.is_enabled(): 984 text += self.fx.get_fiat_status_text(c + u + x, 985 self.base_unit(), self.get_decimal_point()) or '' 986 if not self.network.proxy: 987 icon = read_QIcon("status_connected%s.png"%fork_str) 988 else: 989 icon = read_QIcon("status_connected_proxy%s.png"%fork_str) 990 else: 991 if self.network.proxy: 992 text = "{} ({})".format(_("Not connected"), _("proxy enabled")) 993 else: 994 text = _("Not connected") 995 icon = read_QIcon("status_disconnected.png") 996 997 if self.tray: 998 self.tray.setToolTip("%s (%s)" % (text, self.wallet.basename())) 999 self.balance_label.setText(text) 1000 if self.status_button: 1001 self.status_button.setIcon(icon) 1002 1003 def update_wallet(self): 1004 self.update_status() 1005 if self.wallet.up_to_date or not self.network or not self.network.is_connected(): 1006 self.update_tabs() 1007 1008 def update_tabs(self, wallet=None): 1009 if wallet is None: 1010 wallet = self.wallet 1011 if wallet != self.wallet: 1012 return 1013 self.history_model.refresh('update_tabs') 1014 self.request_list.update() 1015 self.address_list.update() 1016 self.utxo_list.update() 1017 self.contact_list.update() 1018 self.invoice_list.update() 1019 self.channels_list.update_rows.emit(wallet) 1020 self.update_completions() 1021 1022 def create_channels_tab(self): 1023 self.channels_list = ChannelsList(self) 1024 t = self.channels_list.get_toolbar() 1025 return self.create_list_tab(self.channels_list, t) 1026 1027 def create_history_tab(self): 1028 self.history_model = HistoryModel(self) 1029 self.history_list = l = HistoryList(self, self.history_model) 1030 self.history_model.set_view(self.history_list) 1031 l.searchable_list = l 1032 toolbar = l.create_toolbar(self.config) 1033 tab = self.create_list_tab(l, toolbar) 1034 toolbar_shown = bool(self.config.get('show_toolbar_history', False)) 1035 l.show_toolbar(toolbar_shown) 1036 return tab 1037 1038 def show_address(self, addr): 1039 from . import address_dialog 1040 d = address_dialog.AddressDialog(self, addr) 1041 d.exec_() 1042 1043 def show_channel(self, channel_id): 1044 from . import channel_details 1045 channel_details.ChannelDetailsDialog(self, channel_id).show() 1046 1047 def show_transaction(self, tx, *, tx_desc=None): 1048 '''tx_desc is set only for txs created in the Send tab''' 1049 show_transaction(tx, parent=self, desc=tx_desc) 1050 1051 def show_lightning_transaction(self, tx_item): 1052 from .lightning_tx_dialog import LightningTxDialog 1053 d = LightningTxDialog(self, tx_item) 1054 d.show() 1055 1056 def create_receive_tab(self): 1057 # A 4-column grid layout. All the stretch is in the last column. 1058 # The exchange rate plugin adds a fiat widget in column 2 1059 self.receive_grid = grid = QGridLayout() 1060 grid.setSpacing(8) 1061 grid.setColumnStretch(3, 1) 1062 1063 self.receive_message_e = SizedFreezableLineEdit(width=700) 1064 grid.addWidget(QLabel(_('Description')), 0, 0) 1065 grid.addWidget(self.receive_message_e, 0, 1, 1, 4) 1066 self.receive_message_e.textChanged.connect(self.update_receive_qr) 1067 1068 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point) 1069 grid.addWidget(QLabel(_('Requested amount')), 1, 0) 1070 grid.addWidget(self.receive_amount_e, 1, 1) 1071 self.receive_amount_e.textChanged.connect(self.update_receive_qr) 1072 1073 self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '') 1074 if not self.fx or not self.fx.is_enabled(): 1075 self.fiat_receive_e.setVisible(False) 1076 grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft) 1077 1078 self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None) 1079 self.connect_fields(self, self.amount_e, self.fiat_send_e, None) 1080 1081 self.expires_combo = QComboBox() 1082 evl = sorted(pr_expiration_values.items()) 1083 evl_keys = [i[0] for i in evl] 1084 evl_values = [i[1] for i in evl] 1085 default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) 1086 try: 1087 i = evl_keys.index(default_expiry) 1088 except ValueError: 1089 i = 0 1090 self.expires_combo.addItems(evl_values) 1091 self.expires_combo.setCurrentIndex(i) 1092 def on_expiry(i): 1093 self.config.set_key('request_expiry', evl_keys[i]) 1094 self.expires_combo.currentIndexChanged.connect(on_expiry) 1095 msg = ''.join([ 1096 _('Expiration date of your request.'), ' ', 1097 _('This information is seen by the recipient if you send them a signed payment request.'), 1098 '\n\n', 1099 _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ', 1100 _('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ', 1101 _('You can reuse a bitcoin address any number of times but it is not good for your privacy.'), 1102 '\n\n', 1103 _('For Lightning requests, payments will not be accepted after the expiration.'), 1104 ]) 1105 grid.addWidget(HelpLabel(_('Expires after') + ' (?)', msg), 2, 0) 1106 grid.addWidget(self.expires_combo, 2, 1) 1107 self.expires_label = QLineEdit('') 1108 self.expires_label.setReadOnly(1) 1109 self.expires_label.setFocusPolicy(Qt.NoFocus) 1110 self.expires_label.hide() 1111 grid.addWidget(self.expires_label, 2, 1) 1112 1113 self.clear_invoice_button = QPushButton(_('Clear')) 1114 self.clear_invoice_button.clicked.connect(self.clear_receive_tab) 1115 self.create_invoice_button = QPushButton(_('New Address')) 1116 self.create_invoice_button.setIcon(read_QIcon("bitcoin.png")) 1117 self.create_invoice_button.setToolTip('Create on-chain request') 1118 self.create_invoice_button.clicked.connect(lambda: self.create_invoice(False)) 1119 self.receive_buttons = buttons = QHBoxLayout() 1120 buttons.addStretch(1) 1121 buttons.addWidget(self.clear_invoice_button) 1122 buttons.addWidget(self.create_invoice_button) 1123 if self.wallet.has_lightning(): 1124 self.create_lightning_invoice_button = QPushButton(_('Lightning')) 1125 self.create_lightning_invoice_button.setToolTip('Create lightning request') 1126 self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png")) 1127 self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True)) 1128 buttons.addWidget(self.create_lightning_invoice_button) 1129 grid.addLayout(buttons, 4, 0, 1, -1) 1130 1131 self.receive_payreq_e = ButtonsTextEdit() 1132 self.receive_payreq_e.setFont(QFont(MONOSPACE_FONT)) 1133 self.receive_payreq_e.addCopyButton(self.app) 1134 self.receive_payreq_e.setReadOnly(True) 1135 self.receive_payreq_e.textChanged.connect(self.update_receive_qr) 1136 self.receive_payreq_e.setFocusPolicy(Qt.ClickFocus) 1137 1138 self.receive_qr = QRCodeWidget(fixedSize=220) 1139 self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window() 1140 self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor)) 1141 self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor)) 1142 1143 self.receive_address_e = ButtonsTextEdit() 1144 self.receive_address_e.setFont(QFont(MONOSPACE_FONT)) 1145 self.receive_address_e.addCopyButton(self.app) 1146 self.receive_address_e.setReadOnly(True) 1147 self.receive_address_e.textChanged.connect(self.update_receive_address_styling) 1148 1149 qr_show = lambda: self.show_qrcode(str(self.receive_address_e.text()), _('Receiving address'), parent=self) 1150 qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" 1151 self.receive_address_e.addButton(qr_icon, qr_show, _("Show as QR code")) 1152 1153 self.receive_requests_label = QLabel(_('Receive queue')) 1154 1155 from .request_list import RequestList 1156 self.request_list = RequestList(self) 1157 1158 receive_tabs = QTabWidget() 1159 receive_tabs.addTab(self.receive_address_e, _('Address')) 1160 receive_tabs.addTab(self.receive_payreq_e, _('Request')) 1161 receive_tabs.addTab(self.receive_qr, _('QR Code')) 1162 receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) 1163 receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i)) 1164 receive_tabs_sp = receive_tabs.sizePolicy() 1165 receive_tabs_sp.setRetainSizeWhenHidden(True) 1166 receive_tabs.setSizePolicy(receive_tabs_sp) 1167 1168 def maybe_hide_receive_tabs(): 1169 receive_tabs.setVisible(bool(self.receive_payreq_e.text())) 1170 self.receive_payreq_e.textChanged.connect(maybe_hide_receive_tabs) 1171 maybe_hide_receive_tabs() 1172 1173 # layout 1174 vbox_g = QVBoxLayout() 1175 vbox_g.addLayout(grid) 1176 vbox_g.addStretch() 1177 hbox = QHBoxLayout() 1178 hbox.addLayout(vbox_g) 1179 hbox.addStretch() 1180 hbox.addWidget(receive_tabs) 1181 1182 w = QWidget() 1183 w.searchable_list = self.request_list 1184 vbox = QVBoxLayout(w) 1185 vbox.addLayout(hbox) 1186 1187 vbox.addStretch(1) 1188 vbox.addWidget(self.receive_requests_label) 1189 vbox.addWidget(self.request_list) 1190 vbox.setStretchFactor(self.request_list, 1000) 1191 1192 return w 1193 1194 def delete_requests(self, keys): 1195 for key in keys: 1196 self.wallet.delete_request(key) 1197 self.request_list.update() 1198 self.clear_receive_tab() 1199 1200 def delete_lightning_payreq(self, payreq_key): 1201 self.wallet.lnworker.delete_invoice(payreq_key) 1202 self.request_list.update() 1203 self.invoice_list.update() 1204 self.clear_receive_tab() 1205 1206 def sign_payment_request(self, addr): 1207 alias = self.config.get('alias') 1208 if alias and self.alias_info: 1209 alias_addr, alias_name, validated = self.alias_info 1210 if alias_addr: 1211 if self.wallet.is_mine(alias_addr): 1212 msg = _('This payment request will be signed.') + '\n' + _('Please enter your password') 1213 password = None 1214 if self.wallet.has_keystore_encryption(): 1215 password = self.password_dialog(msg) 1216 if not password: 1217 return 1218 try: 1219 self.wallet.sign_payment_request(addr, alias, alias_addr, password) 1220 except Exception as e: 1221 self.show_error(repr(e)) 1222 return 1223 else: 1224 return 1225 1226 def create_invoice(self, is_lightning: bool): 1227 amount = self.receive_amount_e.get_amount() 1228 message = self.receive_message_e.text() 1229 expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) 1230 try: 1231 if is_lightning: 1232 if not self.wallet.lnworker.channels: 1233 self.show_error(_("You need to open a Lightning channel first.")) 1234 return 1235 # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) 1236 key = self.wallet.lnworker.add_request(amount, message, expiry) 1237 else: 1238 key = self.create_bitcoin_request(amount, message, expiry) 1239 if not key: 1240 return 1241 self.address_list.update() 1242 except InvoiceError as e: 1243 self.show_error(_('Error creating payment request') + ':\n' + str(e)) 1244 return 1245 1246 assert key is not None 1247 self.request_list.update() 1248 self.request_list.select_key(key) 1249 # clear request fields 1250 self.receive_amount_e.setText('') 1251 self.receive_message_e.setText('') 1252 # copy to clipboard 1253 r = self.wallet.get_request(key) 1254 content = r.invoice if r.is_lightning() else r.get_address() 1255 title = _('Invoice') if is_lightning else _('Address') 1256 self.do_copy(content, title=title) 1257 1258 def create_bitcoin_request(self, amount: int, message: str, expiration: int) -> Optional[str]: 1259 addr = self.wallet.get_unused_address() 1260 if addr is None: 1261 if not self.wallet.is_deterministic(): # imported wallet 1262 msg = [ 1263 _('No more addresses in your wallet.'), ' ', 1264 _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ', 1265 _('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n', 1266 _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'), 1267 ] 1268 if not self.question(''.join(msg)): 1269 return 1270 addr = self.wallet.get_receiving_address() 1271 else: # deterministic wallet 1272 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?")): 1273 return 1274 addr = self.wallet.create_new_address(False) 1275 req = self.wallet.make_payment_request(addr, amount, message, expiration) 1276 try: 1277 self.wallet.add_payment_request(req) 1278 except Exception as e: 1279 self.logger.exception('Error adding payment request') 1280 self.show_error(_('Error adding payment request') + ':\n' + repr(e)) 1281 else: 1282 self.sign_payment_request(addr) 1283 return addr 1284 1285 def do_copy(self, content: str, *, title: str = None) -> None: 1286 self.app.clipboard().setText(content) 1287 if title is None: 1288 tooltip_text = _("Text copied to clipboard").format(title) 1289 else: 1290 tooltip_text = _("{} copied to clipboard").format(title) 1291 QToolTip.showText(QCursor.pos(), tooltip_text, self) 1292 1293 def clear_receive_tab(self): 1294 self.receive_payreq_e.setText('') 1295 self.receive_address_e.setText('') 1296 self.receive_message_e.setText('') 1297 self.receive_amount_e.setAmount(None) 1298 self.expires_label.hide() 1299 self.expires_combo.show() 1300 self.request_list.clearSelection() 1301 1302 def toggle_qr_window(self): 1303 from . import qrwindow 1304 if not self.qr_window: 1305 self.qr_window = qrwindow.QR_Window(self) 1306 self.qr_window.setVisible(True) 1307 self.qr_window_geometry = self.qr_window.geometry() 1308 else: 1309 if not self.qr_window.isVisible(): 1310 self.qr_window.setVisible(True) 1311 self.qr_window.setGeometry(self.qr_window_geometry) 1312 else: 1313 self.qr_window_geometry = self.qr_window.geometry() 1314 self.qr_window.setVisible(False) 1315 self.update_receive_qr() 1316 1317 def show_send_tab(self): 1318 self.tabs.setCurrentIndex(self.tabs.indexOf(self.send_tab)) 1319 1320 def show_receive_tab(self): 1321 self.tabs.setCurrentIndex(self.tabs.indexOf(self.receive_tab)) 1322 1323 def update_receive_qr(self): 1324 uri = str(self.receive_payreq_e.text()) 1325 if maybe_extract_bolt11_invoice(uri): 1326 # encode lightning invoices as uppercase so QR encoding can use 1327 # alphanumeric mode; resulting in smaller QR codes 1328 uri = uri.upper() 1329 self.receive_qr.setData(uri) 1330 if self.qr_window and self.qr_window.isVisible(): 1331 self.qr_window.qrw.setData(uri) 1332 1333 def update_receive_address_styling(self): 1334 addr = str(self.receive_address_e.text()) 1335 if is_address(addr) and self.wallet.is_used(addr): 1336 self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) 1337 self.receive_address_e.setToolTip(_("This address has already been used. " 1338 "For better privacy, do not reuse it for new payments.")) 1339 else: 1340 self.receive_address_e.setStyleSheet("") 1341 self.receive_address_e.setToolTip("") 1342 1343 def create_send_tab(self): 1344 # A 4-column grid layout. All the stretch is in the last column. 1345 # The exchange rate plugin adds a fiat widget in column 2 1346 self.send_grid = grid = QGridLayout() 1347 grid.setSpacing(8) 1348 grid.setColumnStretch(3, 1) 1349 1350 from .paytoedit import PayToEdit 1351 self.amount_e = BTCAmountEdit(self.get_decimal_point) 1352 self.payto_e = PayToEdit(self) 1353 self.payto_e.addPasteButton(self.app) 1354 msg = _('Recipient of the funds.') + '\n\n'\ 1355 + _('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)') 1356 payto_label = HelpLabel(_('Pay to'), msg) 1357 grid.addWidget(payto_label, 1, 0) 1358 grid.addWidget(self.payto_e, 1, 1, 1, -1) 1359 1360 completer = QCompleter() 1361 completer.setCaseSensitivity(False) 1362 self.payto_e.set_completer(completer) 1363 completer.setModel(self.completions) 1364 1365 msg = _('Description of the transaction (not mandatory).') + '\n\n'\ 1366 + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.') 1367 description_label = HelpLabel(_('Description'), msg) 1368 grid.addWidget(description_label, 2, 0) 1369 self.message_e = SizedFreezableLineEdit(width=700) 1370 grid.addWidget(self.message_e, 2, 1, 1, -1) 1371 1372 msg = _('Amount to be sent.') + '\n\n' \ 1373 + _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' ' \ 1374 + _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n' \ 1375 + _('Keyboard shortcut: type "!" to send all your coins.') 1376 amount_label = HelpLabel(_('Amount'), msg) 1377 grid.addWidget(amount_label, 3, 0) 1378 grid.addWidget(self.amount_e, 3, 1) 1379 1380 self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '') 1381 if not self.fx or not self.fx.is_enabled(): 1382 self.fiat_send_e.setVisible(False) 1383 grid.addWidget(self.fiat_send_e, 3, 2) 1384 self.amount_e.frozen.connect( 1385 lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly())) 1386 1387 self.max_button = EnterButton(_("Max"), self.spend_max) 1388 self.max_button.setFixedWidth(100) 1389 self.max_button.setCheckable(True) 1390 grid.addWidget(self.max_button, 3, 3) 1391 1392 self.save_button = EnterButton(_("Save"), self.do_save_invoice) 1393 self.send_button = EnterButton(_("Pay") + "...", self.do_pay) 1394 self.clear_button = EnterButton(_("Clear"), self.do_clear) 1395 1396 buttons = QHBoxLayout() 1397 buttons.addStretch(1) 1398 buttons.addWidget(self.clear_button) 1399 buttons.addWidget(self.save_button) 1400 buttons.addWidget(self.send_button) 1401 grid.addLayout(buttons, 6, 1, 1, 4) 1402 1403 self.amount_e.shortcut.connect(self.spend_max) 1404 1405 def reset_max(text): 1406 self.max_button.setChecked(False) 1407 enable = not bool(text) and not self.amount_e.isReadOnly() 1408 #self.max_button.setEnabled(enable) 1409 self.amount_e.textEdited.connect(reset_max) 1410 self.fiat_send_e.textEdited.connect(reset_max) 1411 1412 self.set_onchain(False) 1413 1414 self.invoices_label = QLabel(_('Send queue')) 1415 from .invoice_list import InvoiceList 1416 self.invoice_list = InvoiceList(self) 1417 1418 vbox0 = QVBoxLayout() 1419 vbox0.addLayout(grid) 1420 hbox = QHBoxLayout() 1421 hbox.addLayout(vbox0) 1422 hbox.addStretch(1) 1423 w = QWidget() 1424 vbox = QVBoxLayout(w) 1425 vbox.addLayout(hbox) 1426 vbox.addStretch(1) 1427 vbox.addWidget(self.invoices_label) 1428 vbox.addWidget(self.invoice_list) 1429 vbox.setStretchFactor(self.invoice_list, 1000) 1430 w.searchable_list = self.invoice_list 1431 run_hook('create_send_tab', grid) 1432 return w 1433 1434 def spend_max(self): 1435 if run_hook('abort_send', self): 1436 return 1437 outputs = self.payto_e.get_outputs(True) 1438 if not outputs: 1439 return 1440 make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( 1441 coins=self.get_coins(), 1442 outputs=outputs, 1443 fee=fee_est, 1444 is_sweep=False) 1445 1446 try: 1447 try: 1448 tx = make_tx(None) 1449 except (NotEnoughFunds, NoDynamicFeeEstimates) as e: 1450 # Check if we had enough funds excluding fees, 1451 # if so, still provide opportunity to set lower fees. 1452 tx = make_tx(0) 1453 except MultipleSpendMaxTxOutputs as e: 1454 self.max_button.setChecked(False) 1455 self.show_error(str(e)) 1456 return 1457 except NotEnoughFunds as e: 1458 self.max_button.setChecked(False) 1459 text = self.get_text_not_enough_funds_mentioning_frozen() 1460 self.show_error(text) 1461 return 1462 1463 self.max_button.setChecked(True) 1464 amount = tx.output_value() 1465 __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0) 1466 amount_after_all_fees = amount - x_fee_amount 1467 self.amount_e.setAmount(amount_after_all_fees) 1468 # show tooltip explaining max amount 1469 mining_fee = tx.get_fee() 1470 mining_fee_str = self.format_amount_and_units(mining_fee) 1471 msg = _("Mining fee: {} (can be adjusted on next screen)").format(mining_fee_str) 1472 if x_fee_amount: 1473 twofactor_fee_str = self.format_amount_and_units(x_fee_amount) 1474 msg += "\n" + _("2fa fee: {} (for the next batch of transactions)").format(twofactor_fee_str) 1475 frozen_bal = self.get_frozen_balance_str() 1476 if frozen_bal: 1477 msg += "\n" + _("Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)").format(frozen_bal) 1478 QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg) 1479 1480 def get_contact_payto(self, key): 1481 _type, label = self.contacts.get(key) 1482 return label + ' <' + key + '>' if _type == 'address' else key 1483 1484 def update_completions(self): 1485 l = [self.get_contact_payto(key) for key in self.contacts.keys()] 1486 self.completions.setStringList(l) 1487 1488 @protected 1489 def protect(self, func, args, password): 1490 return func(*args, password) 1491 1492 def read_outputs(self) -> List[PartialTxOutput]: 1493 if self.payment_request: 1494 outputs = self.payment_request.get_outputs() 1495 else: 1496 outputs = self.payto_e.get_outputs(self.max_button.isChecked()) 1497 return outputs 1498 1499 def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool: 1500 """Returns whether there are errors with outputs. 1501 Also shows error dialog to user if so. 1502 """ 1503 if not outputs: 1504 self.show_error(_('No outputs')) 1505 return True 1506 1507 for o in outputs: 1508 if o.scriptpubkey is None: 1509 self.show_error(_('Bitcoin Address is None')) 1510 return True 1511 if o.value is None: 1512 self.show_error(_('Invalid Amount')) 1513 return True 1514 1515 return False # no errors 1516 1517 def check_send_tab_payto_line_and_show_errors(self) -> bool: 1518 """Returns whether there are errors. 1519 Also shows error dialog to user if so. 1520 """ 1521 pr = self.payment_request 1522 if pr: 1523 if pr.has_expired(): 1524 self.show_error(_('Payment request has expired')) 1525 return True 1526 1527 if not pr: 1528 errors = self.payto_e.get_errors() 1529 if errors: 1530 if len(errors) == 1 and not errors[0].is_multiline: 1531 err = errors[0] 1532 self.show_warning(_("Failed to parse 'Pay to' line") + ":\n" + 1533 f"{err.line_content[:40]}...\n\n" 1534 f"{err.exc!r}") 1535 else: 1536 self.show_warning(_("Invalid Lines found:") + "\n\n" + 1537 '\n'.join([_("Line #") + 1538 f"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})" 1539 for err in errors])) 1540 return True 1541 1542 if self.payto_e.is_alias and self.payto_e.validated is False: 1543 alias = self.payto_e.toPlainText() 1544 msg = _('WARNING: the alias "{}" could not be validated via an additional ' 1545 'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n' 1546 msg += _('Do you wish to continue?') 1547 if not self.question(msg): 1548 return True 1549 1550 return False # no errors 1551 1552 def pay_lightning_invoice(self, invoice: str, *, amount_msat: Optional[int]): 1553 if amount_msat is None: 1554 raise Exception("missing amount for LN invoice") 1555 amount_sat = Decimal(amount_msat) / 1000 1556 # FIXME this is currently lying to user as we truncate to satoshis 1557 msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat)) 1558 if not self.question(msg): 1559 return 1560 self.save_pending_invoice() 1561 def task(): 1562 coro = self.wallet.lnworker.pay_invoice(invoice, amount_msat=amount_msat, attempts=LN_NUM_PAYMENT_ATTEMPTS) 1563 fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) 1564 return fut.result() 1565 self.wallet.thread.add(task) 1566 1567 def on_request_status(self, wallet, key, status): 1568 if wallet != self.wallet: 1569 return 1570 req = self.wallet.receive_requests.get(key) 1571 if req is None: 1572 return 1573 if status == PR_PAID: 1574 self.notify(_('Payment received') + '\n' + key) 1575 self.need_update.set() 1576 else: 1577 self.request_list.update_item(key, req) 1578 1579 def on_invoice_status(self, wallet, key): 1580 if wallet != self.wallet: 1581 return 1582 invoice = self.wallet.get_invoice(key) 1583 if invoice is None: 1584 return 1585 status = self.wallet.get_invoice_status(invoice) 1586 if status == PR_PAID: 1587 self.invoice_list.update() 1588 else: 1589 self.invoice_list.update_item(key, invoice) 1590 1591 def on_payment_succeeded(self, wallet, key): 1592 description = self.wallet.get_label(key) 1593 self.notify(_('Payment succeeded') + '\n\n' + description) 1594 self.need_update.set() 1595 1596 def on_payment_failed(self, wallet, key, reason): 1597 self.show_error(_('Payment failed') + '\n\n' + reason) 1598 1599 def read_invoice(self): 1600 if self.check_send_tab_payto_line_and_show_errors(): 1601 return 1602 try: 1603 if not self._is_onchain: 1604 invoice_str = self.payto_e.lightning_invoice 1605 if not invoice_str: 1606 return 1607 if not self.wallet.has_lightning(): 1608 self.show_error(_('Lightning is disabled')) 1609 return 1610 invoice = LNInvoice.from_bech32(invoice_str) 1611 if invoice.get_amount_msat() is None: 1612 amount_sat = self.amount_e.get_amount() 1613 if amount_sat: 1614 invoice.amount_msat = int(amount_sat * 1000) 1615 else: 1616 self.show_error(_('No amount')) 1617 return 1618 return invoice 1619 else: 1620 outputs = self.read_outputs() 1621 if self.check_send_tab_onchain_outputs_and_show_errors(outputs): 1622 return 1623 message = self.message_e.text() 1624 return self.wallet.create_invoice( 1625 outputs=outputs, 1626 message=message, 1627 pr=self.payment_request, 1628 URI=self.payto_URI) 1629 except InvoiceError as e: 1630 self.show_error(_('Error creating payment') + ':\n' + str(e)) 1631 1632 def do_save_invoice(self): 1633 self.pending_invoice = self.read_invoice() 1634 if not self.pending_invoice: 1635 return 1636 self.save_pending_invoice() 1637 1638 def save_pending_invoice(self): 1639 if not self.pending_invoice: 1640 return 1641 self.do_clear() 1642 self.wallet.save_invoice(self.pending_invoice) 1643 self.invoice_list.update() 1644 self.pending_invoice = None 1645 1646 def do_pay(self): 1647 self.pending_invoice = self.read_invoice() 1648 if not self.pending_invoice: 1649 return 1650 self.do_pay_invoice(self.pending_invoice) 1651 1652 def pay_multiple_invoices(self, invoices): 1653 outputs = [] 1654 for invoice in invoices: 1655 outputs += invoice.outputs 1656 self.pay_onchain_dialog(self.get_coins(), outputs) 1657 1658 def do_pay_invoice(self, invoice: 'Invoice'): 1659 if invoice.type == PR_TYPE_LN: 1660 assert isinstance(invoice, LNInvoice) 1661 self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat()) 1662 elif invoice.type == PR_TYPE_ONCHAIN: 1663 assert isinstance(invoice, OnchainInvoice) 1664 self.pay_onchain_dialog(self.get_coins(), invoice.outputs) 1665 else: 1666 raise Exception('unknown invoice type') 1667 1668 def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]: 1669 coins = self.get_manually_selected_coins() 1670 if coins is not None: 1671 return coins 1672 else: 1673 return self.wallet.get_spendable_coins(None, nonlocal_only=nonlocal_only) 1674 1675 def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]: 1676 """Return a list of selected coins or None. 1677 Note: None means selection is not being used, 1678 while an empty sequence means the user specifically selected that. 1679 """ 1680 return self.utxo_list.get_spend_list() 1681 1682 def get_text_not_enough_funds_mentioning_frozen(self) -> str: 1683 text = _("Not enough funds") 1684 frozen_str = self.get_frozen_balance_str() 1685 if frozen_str: 1686 text += " ({} {})".format( 1687 frozen_str, _("are frozen") 1688 ) 1689 return text 1690 1691 def get_frozen_balance_str(self) -> Optional[str]: 1692 frozen_bal = sum(self.wallet.get_frozen_balance()) 1693 if not frozen_bal: 1694 return None 1695 return self.format_amount_and_units(frozen_bal) 1696 1697 def pay_onchain_dialog( 1698 self, inputs: Sequence[PartialTxInput], 1699 outputs: List[PartialTxOutput], *, 1700 external_keypairs=None) -> None: 1701 # trustedcoin requires this 1702 if run_hook('abort_send', self): 1703 return 1704 is_sweep = bool(external_keypairs) 1705 make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( 1706 coins=inputs, 1707 outputs=outputs, 1708 fee=fee_est, 1709 is_sweep=is_sweep) 1710 output_values = [x.value for x in outputs] 1711 if output_values.count('!') > 1: 1712 self.show_error(_("More than one output set to spend max")) 1713 return 1714 1715 output_value = '!' if '!' in output_values else sum(output_values) 1716 conf_dlg = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep) 1717 if conf_dlg.not_enough_funds: 1718 # Check if we had enough funds excluding fees, 1719 # if so, still provide opportunity to set lower fees. 1720 if not conf_dlg.have_enough_funds_assuming_zero_fees(): 1721 text = self.get_text_not_enough_funds_mentioning_frozen() 1722 self.show_message(text) 1723 return 1724 1725 # shortcut to advanced preview (after "enough funds" check!) 1726 if self.config.get('advanced_preview'): 1727 preview_dlg = PreviewTxDialog( 1728 window=self, 1729 make_tx=make_tx, 1730 external_keypairs=external_keypairs, 1731 output_value=output_value) 1732 preview_dlg.show() 1733 return 1734 1735 cancelled, is_send, password, tx = conf_dlg.run() 1736 if cancelled: 1737 return 1738 if is_send: 1739 self.save_pending_invoice() 1740 def sign_done(success): 1741 if success: 1742 self.broadcast_or_show(tx) 1743 self.sign_tx_with_password(tx, callback=sign_done, password=password, 1744 external_keypairs=external_keypairs) 1745 else: 1746 preview_dlg = PreviewTxDialog( 1747 window=self, 1748 make_tx=make_tx, 1749 external_keypairs=external_keypairs, 1750 output_value=output_value) 1751 preview_dlg.show() 1752 1753 def broadcast_or_show(self, tx: Transaction): 1754 if not tx.is_complete(): 1755 self.show_transaction(tx) 1756 return 1757 if not self.network: 1758 self.show_error(_("You can't broadcast a transaction without a live network connection.")) 1759 self.show_transaction(tx) 1760 return 1761 self.broadcast_transaction(tx) 1762 1763 @protected 1764 def sign_tx(self, tx, *, callback, external_keypairs, password): 1765 self.sign_tx_with_password(tx, callback=callback, password=password, external_keypairs=external_keypairs) 1766 1767 def sign_tx_with_password(self, tx: PartialTransaction, *, callback, password, external_keypairs=None): 1768 '''Sign the transaction in a separate thread. When done, calls 1769 the callback with a success code of True or False. 1770 ''' 1771 def on_success(result): 1772 callback(True) 1773 def on_failure(exc_info): 1774 self.on_error(exc_info) 1775 callback(False) 1776 on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success 1777 if external_keypairs: 1778 # can sign directly 1779 task = partial(tx.sign, external_keypairs) 1780 else: 1781 task = partial(self.wallet.sign_transaction, tx, password) 1782 msg = _('Signing transaction...') 1783 WaitingDialog(self, msg, task, on_success, on_failure) 1784 1785 def broadcast_transaction(self, tx: Transaction): 1786 1787 def broadcast_thread(): 1788 # non-GUI thread 1789 pr = self.payment_request 1790 if pr and pr.has_expired(): 1791 self.payment_request = None 1792 return False, _("Invoice has expired") 1793 try: 1794 self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) 1795 except TxBroadcastError as e: 1796 return False, e.get_message_for_gui() 1797 except BestEffortRequestFailed as e: 1798 return False, repr(e) 1799 # success 1800 txid = tx.txid() 1801 if pr: 1802 self.payment_request = None 1803 refund_address = self.wallet.get_receiving_address() 1804 coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address) 1805 fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) 1806 ack_status, ack_msg = fut.result(timeout=20) 1807 self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") 1808 return True, txid 1809 1810 # Capture current TL window; override might be removed on return 1811 parent = self.top_level_window(lambda win: isinstance(win, MessageBoxMixin)) 1812 1813 def broadcast_done(result): 1814 # GUI thread 1815 if result: 1816 success, msg = result 1817 if success: 1818 parent.show_message(_('Payment sent.') + '\n' + msg) 1819 self.invoice_list.update() 1820 else: 1821 msg = msg or '' 1822 parent.show_error(msg) 1823 1824 WaitingDialog(self, _('Broadcasting transaction...'), 1825 broadcast_thread, broadcast_done, self.on_error) 1826 1827 def mktx_for_open_channel(self, *, funding_sat, node_id): 1828 coins = self.get_coins(nonlocal_only=True) 1829 make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel( 1830 coins=coins, 1831 funding_sat=funding_sat, 1832 node_id=node_id, 1833 fee_est=fee_est) 1834 return make_tx 1835 1836 def open_channel(self, connect_str, funding_sat, push_amt): 1837 try: 1838 node_id, rest = extract_nodeid(connect_str) 1839 except ConnStringFormatError as e: 1840 self.show_error(str(e)) 1841 return 1842 if self.wallet.lnworker.has_conflicting_backup_with(node_id): 1843 msg = messages.MGS_CONFLICTING_BACKUP_INSTANCE 1844 if not self.question(msg): 1845 return 1846 # use ConfirmTxDialog 1847 # we need to know the fee before we broadcast, because the txid is required 1848 make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id) 1849 d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, is_sweep=False) 1850 # disable preview button because the user must not broadcast tx before establishment_flow 1851 d.preview_button.setEnabled(False) 1852 cancelled, is_send, password, funding_tx = d.run() 1853 if not is_send: 1854 return 1855 if cancelled: 1856 return 1857 # read funding_sat from tx; converts '!' to int value 1858 funding_sat = funding_tx.output_value_for_address(ln_dummy_address()) 1859 def task(): 1860 return self.wallet.lnworker.open_channel( 1861 connect_str=connect_str, 1862 funding_tx=funding_tx, 1863 funding_sat=funding_sat, 1864 push_amt_sat=push_amt, 1865 password=password) 1866 def on_failure(exc_info): 1867 type_, e, traceback = exc_info 1868 self.show_error(_('Could not open channel: {}').format(repr(e))) 1869 WaitingDialog(self, _('Opening channel...'), task, self.on_open_channel_success, on_failure) 1870 1871 def on_open_channel_success(self, args): 1872 chan, funding_tx = args 1873 lnworker = self.wallet.lnworker 1874 if not chan.has_onchain_backup(): 1875 backup_dir = self.config.get_backup_dir() 1876 if backup_dir is not None: 1877 self.show_message(_(f'Your wallet backup has been updated in {backup_dir}')) 1878 else: 1879 data = lnworker.export_channel_backup(chan.channel_id) 1880 help_text = _(messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL) 1881 self.show_qrcode( 1882 data, _('Save channel backup'), 1883 help_text=help_text, 1884 show_copy_text_btn=True) 1885 n = chan.constraints.funding_txn_minimum_depth 1886 message = '\n'.join([ 1887 _('Channel established.'), 1888 _('Remote peer ID') + ':' + chan.node_id.hex(), 1889 _('This channel will be usable after {} confirmations').format(n) 1890 ]) 1891 if not funding_tx.is_complete(): 1892 message += '\n\n' + _('Please sign and broadcast the funding transaction') 1893 self.show_message(message) 1894 self.show_transaction(funding_tx) 1895 else: 1896 self.show_message(message) 1897 1898 def query_choice(self, msg, choices): 1899 # Needed by QtHandler for hardware wallets 1900 dialog = WindowModalDialog(self.top_level_window()) 1901 clayout = ChoicesLayout(msg, choices) 1902 vbox = QVBoxLayout(dialog) 1903 vbox.addLayout(clayout.layout()) 1904 vbox.addLayout(Buttons(OkButton(dialog))) 1905 if not dialog.exec_(): 1906 return None 1907 return clayout.selected_index() 1908 1909 def lock_amount(self, b: bool) -> None: 1910 self.amount_e.setFrozen(b) 1911 self.max_button.setEnabled(not b) 1912 1913 def prepare_for_payment_request(self): 1914 self.show_send_tab() 1915 self.payto_e.is_pr = True 1916 for e in [self.payto_e, self.message_e]: 1917 e.setFrozen(True) 1918 self.lock_amount(True) 1919 self.payto_e.setText(_("please wait...")) 1920 return True 1921 1922 def delete_invoices(self, keys): 1923 for key in keys: 1924 self.wallet.delete_invoice(key) 1925 self.invoice_list.update() 1926 1927 def payment_request_ok(self): 1928 pr = self.payment_request 1929 if not pr: 1930 return 1931 key = pr.get_id() 1932 invoice = self.wallet.get_invoice(key) 1933 if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID: 1934 self.show_message("invoice already paid") 1935 self.do_clear() 1936 self.payment_request = None 1937 return 1938 self.payto_e.is_pr = True 1939 if not pr.has_expired(): 1940 self.payto_e.setGreen() 1941 else: 1942 self.payto_e.setExpired() 1943 self.payto_e.setText(pr.get_requestor()) 1944 self.amount_e.setAmount(pr.get_amount()) 1945 self.message_e.setText(pr.get_memo()) 1946 # signal to set fee 1947 self.amount_e.textEdited.emit("") 1948 1949 def payment_request_error(self): 1950 pr = self.payment_request 1951 if not pr: 1952 return 1953 self.show_message(pr.error) 1954 self.payment_request = None 1955 self.do_clear() 1956 1957 def on_pr(self, request: 'paymentrequest.PaymentRequest'): 1958 self.set_onchain(True) 1959 self.payment_request = request 1960 if self.payment_request.verify(self.contacts): 1961 self.payment_request_ok_signal.emit() 1962 else: 1963 self.payment_request_error_signal.emit() 1964 1965 def parse_lightning_invoice(self, invoice): 1966 """Parse ln invoice, and prepare the send tab for it.""" 1967 try: 1968 lnaddr = lndecode(invoice) 1969 except Exception as e: 1970 raise LnDecodeException(e) from e 1971 pubkey = bh2u(lnaddr.pubkey.serialize()) 1972 for k,v in lnaddr.tags: 1973 if k == 'd': 1974 description = v 1975 break 1976 else: 1977 description = '' 1978 self.payto_e.setFrozen(True) 1979 self.payto_e.setText(pubkey) 1980 self.message_e.setText(description) 1981 if lnaddr.get_amount_sat() is not None: 1982 self.amount_e.setAmount(lnaddr.get_amount_sat()) 1983 #self.amount_e.textEdited.emit("") 1984 self.set_onchain(False) 1985 1986 def set_onchain(self, b): 1987 self._is_onchain = b 1988 self.max_button.setEnabled(b) 1989 1990 def pay_to_URI(self, URI): 1991 if not URI: 1992 return 1993 try: 1994 out = util.parse_URI(URI, self.on_pr) 1995 except InvalidBitcoinURI as e: 1996 self.show_error(_("Error parsing URI") + f":\n{e}") 1997 return 1998 self.show_send_tab() 1999 self.payto_URI = out 2000 r = out.get('r') 2001 sig = out.get('sig') 2002 name = out.get('name') 2003 if r or (name and sig): 2004 self.prepare_for_payment_request() 2005 return 2006 address = out.get('address') 2007 amount = out.get('amount') 2008 label = out.get('label') 2009 message = out.get('message') 2010 # use label as description (not BIP21 compliant) 2011 if label and not message: 2012 message = label 2013 if address: 2014 self.payto_e.setText(address) 2015 if message: 2016 self.message_e.setText(message) 2017 if amount: 2018 self.amount_e.setAmount(amount) 2019 self.amount_e.textEdited.emit("") 2020 2021 2022 def do_clear(self): 2023 self.max_button.setChecked(False) 2024 self.payment_request = None 2025 self.payto_URI = None 2026 self.payto_e.is_pr = False 2027 self.set_onchain(False) 2028 for e in [self.payto_e, self.message_e, self.amount_e]: 2029 e.setText('') 2030 e.setFrozen(False) 2031 self.update_status() 2032 run_hook('do_clear', self) 2033 2034 def set_frozen_state_of_addresses(self, addrs, freeze: bool): 2035 self.wallet.set_frozen_state_of_addresses(addrs, freeze) 2036 self.address_list.update() 2037 self.utxo_list.update() 2038 2039 def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool): 2040 utxos_str = {utxo.prevout.to_str() for utxo in utxos} 2041 self.wallet.set_frozen_state_of_coins(utxos_str, freeze) 2042 self.utxo_list.update() 2043 2044 def create_list_tab(self, l, toolbar=None): 2045 w = QWidget() 2046 w.searchable_list = l 2047 vbox = QVBoxLayout() 2048 w.setLayout(vbox) 2049 #vbox.setContentsMargins(0, 0, 0, 0) 2050 #vbox.setSpacing(0) 2051 if toolbar: 2052 vbox.addLayout(toolbar) 2053 vbox.addWidget(l) 2054 return w 2055 2056 def create_addresses_tab(self): 2057 from .address_list import AddressList 2058 self.address_list = l = AddressList(self) 2059 toolbar = l.create_toolbar(self.config) 2060 tab = self.create_list_tab(l, toolbar) 2061 toolbar_shown = bool(self.config.get('show_toolbar_addresses', False)) 2062 l.show_toolbar(toolbar_shown) 2063 return tab 2064 2065 def create_utxo_tab(self): 2066 from .utxo_list import UTXOList 2067 self.utxo_list = UTXOList(self) 2068 return self.create_list_tab(self.utxo_list) 2069 2070 def create_contacts_tab(self): 2071 from .contact_list import ContactList 2072 self.contact_list = l = ContactList(self) 2073 return self.create_list_tab(l) 2074 2075 def remove_address(self, addr): 2076 if not self.question(_("Do you want to remove {} from your wallet?").format(addr)): 2077 return 2078 try: 2079 self.wallet.delete_address(addr) 2080 except UserFacingException as e: 2081 self.show_error(str(e)) 2082 else: 2083 self.need_update.set() # history, addresses, coins 2084 self.clear_receive_tab() 2085 2086 def paytomany(self): 2087 self.show_send_tab() 2088 self.payto_e.paytomany() 2089 msg = '\n'.join([ 2090 _('Enter a list of outputs in the \'Pay to\' field.'), 2091 _('One output per line.'), 2092 _('Format: address, amount'), 2093 _('You may load a CSV file using the file icon.') 2094 ]) 2095 self.show_message(msg, title=_('Pay to many')) 2096 2097 def payto_contacts(self, labels): 2098 paytos = [self.get_contact_payto(label) for label in labels] 2099 self.show_send_tab() 2100 if len(paytos) == 1: 2101 self.payto_e.setText(paytos[0]) 2102 self.amount_e.setFocus() 2103 else: 2104 text = "\n".join([payto + ", 0" for payto in paytos]) 2105 self.payto_e.setText(text) 2106 self.payto_e.setFocus() 2107 2108 def set_contact(self, label, address): 2109 if not is_address(address): 2110 self.show_error(_('Invalid Address')) 2111 self.contact_list.update() # Displays original unchanged value 2112 return False 2113 self.contacts[address] = ('address', label) 2114 self.contact_list.update() 2115 self.history_list.update() 2116 self.update_completions() 2117 return True 2118 2119 def delete_contacts(self, labels): 2120 if not self.question(_("Remove {} from your list of contacts?") 2121 .format(" + ".join(labels))): 2122 return 2123 for label in labels: 2124 self.contacts.pop(label) 2125 self.history_list.update() 2126 self.contact_list.update() 2127 self.update_completions() 2128 2129 def show_onchain_invoice(self, invoice: OnchainInvoice): 2130 amount_str = self.format_amount(invoice.amount_sat) + ' ' + self.base_unit() 2131 d = WindowModalDialog(self, _("Onchain Invoice")) 2132 vbox = QVBoxLayout(d) 2133 grid = QGridLayout() 2134 grid.addWidget(QLabel(_("Amount") + ':'), 1, 0) 2135 grid.addWidget(QLabel(amount_str), 1, 1) 2136 if len(invoice.outputs) == 1: 2137 grid.addWidget(QLabel(_("Address") + ':'), 2, 0) 2138 grid.addWidget(QLabel(invoice.get_address()), 2, 1) 2139 else: 2140 outputs_str = '\n'.join(map(lambda x: x.address + ' : ' + self.format_amount(x.value)+ self.base_unit(), invoice.outputs)) 2141 grid.addWidget(QLabel(_("Outputs") + ':'), 2, 0) 2142 grid.addWidget(QLabel(outputs_str), 2, 1) 2143 grid.addWidget(QLabel(_("Description") + ':'), 3, 0) 2144 grid.addWidget(QLabel(invoice.message), 3, 1) 2145 if invoice.exp: 2146 grid.addWidget(QLabel(_("Expires") + ':'), 4, 0) 2147 grid.addWidget(QLabel(format_time(invoice.exp + invoice.time)), 4, 1) 2148 if invoice.bip70: 2149 pr = paymentrequest.PaymentRequest(bytes.fromhex(invoice.bip70)) 2150 pr.verify(self.contacts) 2151 grid.addWidget(QLabel(_("Requestor") + ':'), 5, 0) 2152 grid.addWidget(QLabel(pr.get_requestor()), 5, 1) 2153 grid.addWidget(QLabel(_("Signature") + ':'), 6, 0) 2154 grid.addWidget(QLabel(pr.get_verify_status()), 6, 1) 2155 def do_export(): 2156 key = pr.get_id() 2157 name = str(key) + '.bip70' 2158 fn = getSaveFileName( 2159 parent=self, 2160 title=_("Save invoice to file"), 2161 filename=name, 2162 filter="*.bip70", 2163 config=self.config, 2164 ) 2165 if not fn: 2166 return 2167 with open(fn, 'wb') as f: 2168 data = f.write(pr.raw) 2169 self.show_message(_('BIP70 invoice saved as {}').format(fn)) 2170 exportButton = EnterButton(_('Export'), do_export) 2171 buttons = Buttons(exportButton, CloseButton(d)) 2172 else: 2173 buttons = Buttons(CloseButton(d)) 2174 vbox.addLayout(grid) 2175 vbox.addLayout(buttons) 2176 d.exec_() 2177 2178 def show_lightning_invoice(self, invoice: LNInvoice): 2179 lnaddr = lndecode(invoice.invoice) 2180 d = WindowModalDialog(self, _("Lightning Invoice")) 2181 vbox = QVBoxLayout(d) 2182 grid = QGridLayout() 2183 grid.addWidget(QLabel(_("Node ID") + ':'), 0, 0) 2184 grid.addWidget(QLabel(lnaddr.pubkey.serialize().hex()), 0, 1) 2185 grid.addWidget(QLabel(_("Amount") + ':'), 1, 0) 2186 amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit() 2187 grid.addWidget(QLabel(amount_str), 1, 1) 2188 grid.addWidget(QLabel(_("Description") + ':'), 2, 0) 2189 grid.addWidget(QLabel(invoice.message), 2, 1) 2190 grid.addWidget(QLabel(_("Hash") + ':'), 3, 0) 2191 payhash_e = ButtonsLineEdit(lnaddr.paymenthash.hex()) 2192 payhash_e.addCopyButton(self.app) 2193 payhash_e.setReadOnly(True) 2194 vbox.addWidget(payhash_e) 2195 grid.addWidget(payhash_e, 3, 1) 2196 if invoice.exp: 2197 grid.addWidget(QLabel(_("Expires") + ':'), 4, 0) 2198 grid.addWidget(QLabel(format_time(invoice.time + invoice.exp)), 4, 1) 2199 vbox.addLayout(grid) 2200 invoice_e = ShowQRTextEdit(config=self.config) 2201 invoice_e.addCopyButton(self.app) 2202 invoice_e.setText(invoice.invoice) 2203 vbox.addWidget(invoice_e) 2204 vbox.addLayout(Buttons(CloseButton(d),)) 2205 d.exec_() 2206 2207 def create_console_tab(self): 2208 from .console import Console 2209 self.console = console = Console() 2210 return console 2211 2212 def update_console(self): 2213 console = self.console 2214 console.history = self.wallet.db.get("qt-console-history", []) 2215 console.history_index = len(console.history) 2216 2217 console.updateNamespace({ 2218 'wallet': self.wallet, 2219 'network': self.network, 2220 'plugins': self.gui_object.plugins, 2221 'window': self, 2222 'config': self.config, 2223 'electrum': electrum, 2224 'daemon': self.gui_object.daemon, 2225 'util': util, 2226 'bitcoin': bitcoin, 2227 'lnutil': lnutil, 2228 }) 2229 2230 c = commands.Commands( 2231 config=self.config, 2232 daemon=self.gui_object.daemon, 2233 network=self.network, 2234 callback=lambda: self.console.set_json(True)) 2235 methods = {} 2236 def mkfunc(f, method): 2237 return lambda *args, **kwargs: f(method, 2238 args, 2239 self.password_dialog, 2240 **{**kwargs, 'wallet': self.wallet}) 2241 for m in dir(c): 2242 if m[0]=='_' or m in ['network','wallet','config','daemon']: continue 2243 methods[m] = mkfunc(c._run, m) 2244 2245 console.updateNamespace(methods) 2246 2247 def create_status_bar(self): 2248 2249 sb = QStatusBar() 2250 sb.setFixedHeight(35) 2251 2252 self.balance_label = QLabel("Loading wallet...") 2253 self.balance_label.setTextInteractionFlags(Qt.TextSelectableByMouse) 2254 self.balance_label.setStyleSheet("""QLabel { padding: 0 }""") 2255 sb.addWidget(self.balance_label) 2256 2257 self.search_box = QLineEdit() 2258 self.search_box.textChanged.connect(self.do_search) 2259 self.search_box.hide() 2260 sb.addPermanentWidget(self.search_box) 2261 2262 self.update_check_button = QPushButton("") 2263 self.update_check_button.setFlat(True) 2264 self.update_check_button.setCursor(QCursor(Qt.PointingHandCursor)) 2265 self.update_check_button.setIcon(read_QIcon("update.png")) 2266 self.update_check_button.hide() 2267 sb.addPermanentWidget(self.update_check_button) 2268 2269 self.password_button = StatusBarButton(QIcon(), _("Password"), self.change_password_dialog) 2270 sb.addPermanentWidget(self.password_button) 2271 2272 sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog)) 2273 self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog) 2274 sb.addPermanentWidget(self.seed_button) 2275 self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog) 2276 sb.addPermanentWidget(self.lightning_button) 2277 self.update_lightning_icon() 2278 self.status_button = None 2279 if self.network: 2280 self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog) 2281 sb.addPermanentWidget(self.status_button) 2282 run_hook('create_status_bar', sb) 2283 self.setStatusBar(sb) 2284 2285 def create_coincontrol_statusbar(self): 2286 self.coincontrol_sb = sb = QStatusBar() 2287 sb.setSizeGripEnabled(False) 2288 #sb.setFixedHeight(3 * char_width_in_lineedit()) 2289 sb.setStyleSheet('QStatusBar::item {border: None;} ' 2290 + ColorScheme.GREEN.as_stylesheet(True)) 2291 2292 self.coincontrol_label = QLabel() 2293 self.coincontrol_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 2294 self.coincontrol_label.setTextInteractionFlags(Qt.TextSelectableByMouse) 2295 sb.addWidget(self.coincontrol_label) 2296 2297 clear_cc_button = EnterButton(_('Reset'), lambda: self.utxo_list.set_spend_list(None)) 2298 clear_cc_button.setStyleSheet("margin-right: 5px;") 2299 sb.addPermanentWidget(clear_cc_button) 2300 2301 sb.setVisible(False) 2302 return sb 2303 2304 def set_coincontrol_msg(self, msg: Optional[str]) -> None: 2305 if not msg: 2306 self.coincontrol_label.setText("") 2307 self.coincontrol_sb.setVisible(False) 2308 return 2309 self.coincontrol_label.setText(msg) 2310 self.coincontrol_sb.setVisible(True) 2311 2312 def update_lightning_icon(self): 2313 if not self.wallet.has_lightning(): 2314 self.lightning_button.setVisible(False) 2315 return 2316 if self.network is None or self.network.channel_db is None: 2317 self.lightning_button.setVisible(False) 2318 return 2319 self.lightning_button.setVisible(True) 2320 2321 cur, total, progress_percent = self.network.lngossip.get_sync_progress_estimate() 2322 # self.logger.debug(f"updating lngossip sync progress estimate: cur={cur}, total={total}") 2323 progress_str = "??%" 2324 if progress_percent is not None: 2325 progress_str = f"{progress_percent}%" 2326 if progress_percent and progress_percent >= 100: 2327 self.lightning_button.setMaximumWidth(25) 2328 self.lightning_button.setText('') 2329 self.lightning_button.setToolTip(_("The Lightning Network graph is fully synced.")) 2330 else: 2331 self.lightning_button.setMaximumWidth(25 + 5 * char_width_in_lineedit()) 2332 self.lightning_button.setText(progress_str) 2333 self.lightning_button.setToolTip(_("The Lightning Network graph is syncing...\n" 2334 "Payments are more likely to succeed with a more complete graph.")) 2335 2336 def update_lock_icon(self): 2337 icon = read_QIcon("lock.png") if self.wallet.has_password() else read_QIcon("unlock.png") 2338 self.password_button.setIcon(icon) 2339 2340 def update_buttons_on_seed(self): 2341 self.seed_button.setVisible(self.wallet.has_seed()) 2342 self.password_button.setVisible(self.wallet.may_have_password()) 2343 2344 def change_password_dialog(self): 2345 from electrum.storage import StorageEncryptionVersion 2346 if self.wallet.get_available_storage_encryption_version() == StorageEncryptionVersion.XPUB_PASSWORD: 2347 from .password_dialog import ChangePasswordDialogForHW 2348 d = ChangePasswordDialogForHW(self, self.wallet) 2349 ok, encrypt_file = d.run() 2350 if not ok: 2351 return 2352 2353 try: 2354 hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption() 2355 except UserCancelled: 2356 return 2357 except BaseException as e: 2358 self.logger.exception('') 2359 self.show_error(repr(e)) 2360 return 2361 old_password = hw_dev_pw if self.wallet.has_password() else None 2362 new_password = hw_dev_pw if encrypt_file else None 2363 else: 2364 from .password_dialog import ChangePasswordDialogForSW 2365 d = ChangePasswordDialogForSW(self, self.wallet) 2366 ok, old_password, new_password, encrypt_file = d.run() 2367 2368 if not ok: 2369 return 2370 try: 2371 self.wallet.update_password(old_password, new_password, encrypt_storage=encrypt_file) 2372 except InvalidPassword as e: 2373 self.show_error(str(e)) 2374 return 2375 except BaseException: 2376 self.logger.exception('Failed to update password') 2377 self.show_error(_('Failed to update password')) 2378 return 2379 msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected') 2380 self.show_message(msg, title=_("Success")) 2381 self.update_lock_icon() 2382 2383 def toggle_search(self): 2384 self.search_box.setHidden(not self.search_box.isHidden()) 2385 if not self.search_box.isHidden(): 2386 self.search_box.setFocus(1) 2387 else: 2388 self.do_search('') 2389 2390 def do_search(self, t): 2391 tab = self.tabs.currentWidget() 2392 if hasattr(tab, 'searchable_list'): 2393 tab.searchable_list.filter(t) 2394 2395 def new_contact_dialog(self): 2396 d = WindowModalDialog(self, _("New Contact")) 2397 vbox = QVBoxLayout(d) 2398 vbox.addWidget(QLabel(_('New Contact') + ':')) 2399 grid = QGridLayout() 2400 line1 = QLineEdit() 2401 line1.setFixedWidth(32 * char_width_in_lineedit()) 2402 line2 = QLineEdit() 2403 line2.setFixedWidth(32 * char_width_in_lineedit()) 2404 grid.addWidget(QLabel(_("Address")), 1, 0) 2405 grid.addWidget(line1, 1, 1) 2406 grid.addWidget(QLabel(_("Name")), 2, 0) 2407 grid.addWidget(line2, 2, 1) 2408 vbox.addLayout(grid) 2409 vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) 2410 if d.exec_(): 2411 self.set_contact(line2.text(), line1.text()) 2412 2413 def init_lightning_dialog(self, dialog): 2414 assert not self.wallet.has_lightning() 2415 if self.wallet.can_have_deterministic_lightning(): 2416 msg = _( 2417 "Lightning is not enabled because this wallet was created with an old version of Electrum. " 2418 "Create lightning keys?") 2419 else: 2420 msg = _( 2421 "Warning: this wallet type does not support channel recovery from seed. " 2422 "You will need to backup your wallet everytime you create a new wallet. " 2423 "Create lightning keys?") 2424 if self.question(msg): 2425 self._init_lightning_dialog(dialog=dialog) 2426 2427 @protected 2428 def _init_lightning_dialog(self, *, dialog, password): 2429 dialog.close() 2430 self.wallet.init_lightning(password=password) 2431 self.update_lightning_icon() 2432 self.show_message(_('Lightning keys have been initialized.')) 2433 2434 def show_wallet_info(self): 2435 dialog = WindowModalDialog(self, _("Wallet Information")) 2436 dialog.setMinimumSize(800, 100) 2437 vbox = QVBoxLayout() 2438 wallet_type = self.wallet.db.get('wallet_type', '') 2439 if self.wallet.is_watching_only(): 2440 wallet_type += ' [{}]'.format(_('watching-only')) 2441 seed_available = _('False') 2442 if self.wallet.has_seed(): 2443 seed_available = _('True') 2444 ks = self.wallet.keystore 2445 assert isinstance(ks, keystore.Deterministic_KeyStore) 2446 seed_available += f" ({ks.get_seed_type()})" 2447 keystore_types = [k.get_type_text() for k in self.wallet.get_keystores()] 2448 grid = QGridLayout() 2449 basename = os.path.basename(self.wallet.storage.path) 2450 grid.addWidget(QLabel(_("Wallet name")+ ':'), 0, 0) 2451 grid.addWidget(QLabel(basename), 0, 1) 2452 grid.addWidget(QLabel(_("Wallet type")+ ':'), 1, 0) 2453 grid.addWidget(QLabel(wallet_type), 1, 1) 2454 grid.addWidget(QLabel(_("Script type")+ ':'), 2, 0) 2455 grid.addWidget(QLabel(self.wallet.txin_type), 2, 1) 2456 grid.addWidget(QLabel(_("Seed available") + ':'), 3, 0) 2457 grid.addWidget(QLabel(str(seed_available)), 3, 1) 2458 if len(keystore_types) <= 1: 2459 grid.addWidget(QLabel(_("Keystore type") + ':'), 4, 0) 2460 ks_type = str(keystore_types[0]) if keystore_types else _('No keystore') 2461 grid.addWidget(QLabel(ks_type), 4, 1) 2462 # lightning 2463 grid.addWidget(QLabel(_('Lightning') + ':'), 5, 0) 2464 from .util import IconLabel 2465 if self.wallet.has_lightning(): 2466 if self.wallet.lnworker.has_deterministic_node_id(): 2467 grid.addWidget(QLabel(_('Enabled')), 5, 1) 2468 else: 2469 label = IconLabel(text='Enabled, non-recoverable channels') 2470 label.setIcon(read_QIcon('nocloud')) 2471 grid.addWidget(label, 5, 1) 2472 if self.wallet.db.get('seed_type') == 'segwit': 2473 msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. " 2474 "This means that you must save a backup of your wallet everytime you create a new channel.\n\n" 2475 "If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed") 2476 else: 2477 msg = _("Your channels cannot be recovered from seed. " 2478 "This means that you must save a backup of your wallet everytime you create a new channel.\n\n" 2479 "If you want to have recoverable channels, you must create a new wallet with an Electrum seed") 2480 grid.addWidget(HelpButton(msg), 5, 3) 2481 grid.addWidget(QLabel(_('Lightning Node ID:')), 7, 0) 2482 # TODO: ButtonsLineEdit should have a addQrButton method 2483 nodeid_text = self.wallet.lnworker.node_keypair.pubkey.hex() 2484 nodeid_e = ButtonsLineEdit(nodeid_text) 2485 qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" 2486 nodeid_e.addButton(qr_icon, lambda: self.show_qrcode(nodeid_text, _("Node ID")), _("Show QR Code")) 2487 nodeid_e.addCopyButton(self.app) 2488 nodeid_e.setReadOnly(True) 2489 nodeid_e.setFont(QFont(MONOSPACE_FONT)) 2490 grid.addWidget(nodeid_e, 8, 0, 1, 4) 2491 else: 2492 if self.wallet.can_have_lightning(): 2493 grid.addWidget(QLabel('Not enabled'), 5, 1) 2494 button = QPushButton(_("Enable")) 2495 button.pressed.connect(lambda: self.init_lightning_dialog(dialog)) 2496 grid.addWidget(button, 5, 3) 2497 else: 2498 grid.addWidget(QLabel(_("Not available for this wallet.")), 5, 1) 2499 grid.addWidget(HelpButton(_("Lightning is currently restricted to HD wallets with p2wpkh addresses.")), 5, 2) 2500 vbox.addLayout(grid) 2501 2502 labels_clayout = None 2503 2504 if self.wallet.is_deterministic(): 2505 keystores = self.wallet.get_keystores() 2506 2507 ks_stack = QStackedWidget() 2508 2509 def select_ks(index): 2510 ks_stack.setCurrentIndex(index) 2511 2512 # only show the combobox in case multiple accounts are available 2513 if len(keystores) > 1: 2514 def label(idx, ks): 2515 if isinstance(self.wallet, Multisig_Wallet) and hasattr(ks, 'label'): 2516 return _("cosigner") + f' {idx+1}: {ks.get_type_text()} {ks.label}' 2517 else: 2518 return _("keystore") + f' {idx+1}' 2519 2520 labels = [label(idx, ks) for idx, ks in enumerate(self.wallet.get_keystores())] 2521 2522 on_click = lambda clayout: select_ks(clayout.selected_index()) 2523 labels_clayout = ChoicesLayout(_("Select keystore"), labels, on_click) 2524 vbox.addLayout(labels_clayout.layout()) 2525 2526 for ks in keystores: 2527 ks_w = QWidget() 2528 ks_vbox = QVBoxLayout() 2529 ks_vbox.setContentsMargins(0, 0, 0, 0) 2530 ks_w.setLayout(ks_vbox) 2531 2532 mpk_text = ShowQRTextEdit(ks.get_master_public_key(), config=self.config) 2533 mpk_text.setMaximumHeight(150) 2534 mpk_text.addCopyButton(self.app) 2535 run_hook('show_xpub_button', mpk_text, ks) 2536 2537 der_path_hbox = QHBoxLayout() 2538 der_path_hbox.setContentsMargins(0, 0, 0, 0) 2539 2540 der_path_hbox.addWidget(QLabel(_("Derivation path") + ':')) 2541 der_path_text = QLabel(ks.get_derivation_prefix() or _("unknown")) 2542 der_path_text.setTextInteractionFlags(Qt.TextSelectableByMouse) 2543 der_path_hbox.addWidget(der_path_text) 2544 der_path_hbox.addStretch() 2545 2546 ks_vbox.addWidget(QLabel(_("Master Public Key"))) 2547 ks_vbox.addWidget(mpk_text) 2548 ks_vbox.addLayout(der_path_hbox) 2549 2550 ks_stack.addWidget(ks_w) 2551 2552 select_ks(0) 2553 vbox.addWidget(ks_stack) 2554 2555 vbox.addStretch(1) 2556 btn_export_info = run_hook('wallet_info_buttons', self, dialog) 2557 btn_close = CloseButton(dialog) 2558 btns = Buttons(btn_export_info, btn_close) 2559 vbox.addLayout(btns) 2560 dialog.setLayout(vbox) 2561 dialog.exec_() 2562 2563 def remove_wallet(self): 2564 if self.question('\n'.join([ 2565 _('Delete wallet file?'), 2566 "%s"%self.wallet.storage.path, 2567 _('If your wallet contains funds, make sure you have saved its seed.')])): 2568 self._delete_wallet() 2569 2570 @protected 2571 def _delete_wallet(self, password): 2572 wallet_path = self.wallet.storage.path 2573 basename = os.path.basename(wallet_path) 2574 r = self.gui_object.daemon.delete_wallet(wallet_path) 2575 self.close() 2576 if r: 2577 self.show_error(_("Wallet removed: {}").format(basename)) 2578 else: 2579 self.show_error(_("Wallet file not found: {}").format(basename)) 2580 2581 @protected 2582 def show_seed_dialog(self, password): 2583 if not self.wallet.has_seed(): 2584 self.show_message(_('This wallet has no seed')) 2585 return 2586 keystore = self.wallet.get_keystore() 2587 try: 2588 seed = keystore.get_seed(password) 2589 passphrase = keystore.get_passphrase(password) 2590 except BaseException as e: 2591 self.show_error(repr(e)) 2592 return 2593 from .seed_dialog import SeedDialog 2594 d = SeedDialog(self, seed, passphrase, config=self.config) 2595 d.exec_() 2596 2597 def show_qrcode(self, data, title = _("QR code"), parent=None, *, 2598 help_text=None, show_copy_text_btn=False): 2599 if not data: 2600 return 2601 d = QRDialog( 2602 data=data, 2603 parent=parent or self, 2604 title=title, 2605 help_text=help_text, 2606 show_copy_text_btn=show_copy_text_btn, 2607 config=self.config, 2608 ) 2609 d.exec_() 2610 2611 @protected 2612 def show_private_key(self, address, password): 2613 if not address: 2614 return 2615 try: 2616 pk = self.wallet.export_private_key(address, password) 2617 except Exception as e: 2618 self.logger.exception('') 2619 self.show_message(repr(e)) 2620 return 2621 xtype = bitcoin.deserialize_privkey(pk)[0] 2622 d = WindowModalDialog(self, _("Private key")) 2623 d.setMinimumSize(600, 150) 2624 vbox = QVBoxLayout() 2625 vbox.addWidget(QLabel(_("Address") + ': ' + address)) 2626 vbox.addWidget(QLabel(_("Script type") + ': ' + xtype)) 2627 vbox.addWidget(QLabel(_("Private key") + ':')) 2628 keys_e = ShowQRTextEdit(text=pk, config=self.config) 2629 keys_e.addCopyButton(self.app) 2630 vbox.addWidget(keys_e) 2631 vbox.addLayout(Buttons(CloseButton(d))) 2632 d.setLayout(vbox) 2633 d.exec_() 2634 2635 msg_sign = _("Signing with an address actually means signing with the corresponding " 2636 "private key, and verifying with the corresponding public key. The " 2637 "address you have entered does not have a unique public key, so these " 2638 "operations cannot be performed.") + '\n\n' + \ 2639 _('The operation is undefined. Not just in Electrum, but in general.') 2640 2641 @protected 2642 def do_sign(self, address, message, signature, password): 2643 address = address.text().strip() 2644 message = message.toPlainText().strip() 2645 if not bitcoin.is_address(address): 2646 self.show_message(_('Invalid Bitcoin address.')) 2647 return 2648 if self.wallet.is_watching_only(): 2649 self.show_message(_('This is a watching-only wallet.')) 2650 return 2651 if not self.wallet.is_mine(address): 2652 self.show_message(_('Address not in wallet.')) 2653 return 2654 txin_type = self.wallet.get_txin_type(address) 2655 if txin_type not in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: 2656 self.show_message(_('Cannot sign messages with this type of address:') + \ 2657 ' ' + txin_type + '\n\n' + self.msg_sign) 2658 return 2659 task = partial(self.wallet.sign_message, address, message, password) 2660 2661 def show_signed_message(sig): 2662 try: 2663 signature.setText(base64.b64encode(sig).decode('ascii')) 2664 except RuntimeError: 2665 # (signature) wrapped C/C++ object has been deleted 2666 pass 2667 2668 self.wallet.thread.add(task, on_success=show_signed_message) 2669 2670 def do_verify(self, address, message, signature): 2671 address = address.text().strip() 2672 message = message.toPlainText().strip().encode('utf-8') 2673 if not bitcoin.is_address(address): 2674 self.show_message(_('Invalid Bitcoin address.')) 2675 return 2676 try: 2677 # This can throw on invalid base64 2678 sig = base64.b64decode(str(signature.toPlainText())) 2679 verified = ecc.verify_message_with_address(address, sig, message) 2680 except Exception as e: 2681 verified = False 2682 if verified: 2683 self.show_message(_("Signature verified")) 2684 else: 2685 self.show_error(_("Wrong signature")) 2686 2687 def sign_verify_message(self, address=''): 2688 d = WindowModalDialog(self, _('Sign/verify Message')) 2689 d.setMinimumSize(610, 290) 2690 2691 layout = QGridLayout(d) 2692 2693 message_e = QTextEdit() 2694 message_e.setAcceptRichText(False) 2695 layout.addWidget(QLabel(_('Message')), 1, 0) 2696 layout.addWidget(message_e, 1, 1) 2697 layout.setRowStretch(2,3) 2698 2699 address_e = QLineEdit() 2700 address_e.setText(address) 2701 layout.addWidget(QLabel(_('Address')), 2, 0) 2702 layout.addWidget(address_e, 2, 1) 2703 2704 signature_e = QTextEdit() 2705 signature_e.setAcceptRichText(False) 2706 layout.addWidget(QLabel(_('Signature')), 3, 0) 2707 layout.addWidget(signature_e, 3, 1) 2708 layout.setRowStretch(3,1) 2709 2710 hbox = QHBoxLayout() 2711 2712 b = QPushButton(_("Sign")) 2713 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e)) 2714 hbox.addWidget(b) 2715 2716 b = QPushButton(_("Verify")) 2717 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e)) 2718 hbox.addWidget(b) 2719 2720 b = QPushButton(_("Close")) 2721 b.clicked.connect(d.accept) 2722 hbox.addWidget(b) 2723 layout.addLayout(hbox, 4, 1) 2724 d.exec_() 2725 2726 @protected 2727 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password): 2728 if self.wallet.is_watching_only(): 2729 self.show_message(_('This is a watching-only wallet.')) 2730 return 2731 cyphertext = encrypted_e.toPlainText() 2732 task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password) 2733 2734 def setText(text): 2735 try: 2736 message_e.setText(text.decode('utf-8')) 2737 except RuntimeError: 2738 # (message_e) wrapped C/C++ object has been deleted 2739 pass 2740 2741 self.wallet.thread.add(task, on_success=setText) 2742 2743 def do_encrypt(self, message_e, pubkey_e, encrypted_e): 2744 message = message_e.toPlainText() 2745 message = message.encode('utf-8') 2746 try: 2747 public_key = ecc.ECPubkey(bfh(pubkey_e.text())) 2748 except BaseException as e: 2749 self.logger.exception('Invalid Public key') 2750 self.show_warning(_('Invalid Public key')) 2751 return 2752 encrypted = public_key.encrypt_message(message) 2753 encrypted_e.setText(encrypted.decode('ascii')) 2754 2755 def encrypt_message(self, address=''): 2756 d = WindowModalDialog(self, _('Encrypt/decrypt Message')) 2757 d.setMinimumSize(610, 490) 2758 2759 layout = QGridLayout(d) 2760 2761 message_e = QTextEdit() 2762 message_e.setAcceptRichText(False) 2763 layout.addWidget(QLabel(_('Message')), 1, 0) 2764 layout.addWidget(message_e, 1, 1) 2765 layout.setRowStretch(2,3) 2766 2767 pubkey_e = QLineEdit() 2768 if address: 2769 pubkey = self.wallet.get_public_key(address) 2770 pubkey_e.setText(pubkey) 2771 layout.addWidget(QLabel(_('Public key')), 2, 0) 2772 layout.addWidget(pubkey_e, 2, 1) 2773 2774 encrypted_e = QTextEdit() 2775 encrypted_e.setAcceptRichText(False) 2776 layout.addWidget(QLabel(_('Encrypted')), 3, 0) 2777 layout.addWidget(encrypted_e, 3, 1) 2778 layout.setRowStretch(3,1) 2779 2780 hbox = QHBoxLayout() 2781 b = QPushButton(_("Encrypt")) 2782 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e)) 2783 hbox.addWidget(b) 2784 2785 b = QPushButton(_("Decrypt")) 2786 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e)) 2787 hbox.addWidget(b) 2788 2789 b = QPushButton(_("Close")) 2790 b.clicked.connect(d.accept) 2791 hbox.addWidget(b) 2792 2793 layout.addLayout(hbox, 4, 1) 2794 d.exec_() 2795 2796 def password_dialog(self, msg=None, parent=None): 2797 from .password_dialog import PasswordDialog 2798 parent = parent or self 2799 d = PasswordDialog(parent, msg) 2800 return d.run() 2801 2802 def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']: 2803 from electrum.transaction import tx_from_any 2804 try: 2805 return tx_from_any(data) 2806 except BaseException as e: 2807 self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e)) 2808 return 2809 2810 def import_channel_backup(self, encrypted: str): 2811 if not self.question('Import channel backup?'): 2812 return 2813 try: 2814 self.wallet.lnworker.import_channel_backup(encrypted) 2815 except Exception as e: 2816 self.show_error("failed to import backup" + '\n' + str(e)) 2817 return 2818 2819 def read_tx_from_qrcode(self): 2820 def cb(success: bool, error: str, data): 2821 if not success: 2822 if error: 2823 self.show_error(error) 2824 return 2825 if not data: 2826 return 2827 # if the user scanned a bitcoin URI 2828 if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): 2829 self.pay_to_URI(data) 2830 return 2831 if data.lower().startswith('channel_backup:'): 2832 self.import_channel_backup(data) 2833 return 2834 # else if the user scanned an offline signed tx 2835 tx = self.tx_from_text(data) 2836 if not tx: 2837 return 2838 self.show_transaction(tx) 2839 2840 scan_qrcode(parent=self.top_level_window(), config=self.config, callback=cb) 2841 2842 def read_tx_from_file(self) -> Optional[Transaction]: 2843 fileName = getOpenFileName( 2844 parent=self, 2845 title=_("Select your transaction file"), 2846 filter=TRANSACTION_FILE_EXTENSION_FILTER_ANY, 2847 config=self.config, 2848 ) 2849 if not fileName: 2850 return 2851 try: 2852 with open(fileName, "rb") as f: 2853 file_content = f.read() # type: Union[str, bytes] 2854 except (ValueError, IOError, os.error) as reason: 2855 self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), 2856 title=_("Unable to read file or no transaction found")) 2857 return 2858 return self.tx_from_text(file_content) 2859 2860 def do_process_from_text(self): 2861 text = text_dialog( 2862 parent=self, 2863 title=_('Input raw transaction'), 2864 header_layout=_("Transaction:"), 2865 ok_label=_("Load transaction"), 2866 config=self.config, 2867 ) 2868 if not text: 2869 return 2870 tx = self.tx_from_text(text) 2871 if tx: 2872 self.show_transaction(tx) 2873 2874 def do_process_from_text_channel_backup(self): 2875 text = text_dialog( 2876 parent=self, 2877 title=_('Input channel backup'), 2878 header_layout=_("Channel Backup:"), 2879 ok_label=_("Load backup"), 2880 config=self.config, 2881 ) 2882 if not text: 2883 return 2884 if text.startswith('channel_backup:'): 2885 self.import_channel_backup(text) 2886 2887 def do_process_from_file(self): 2888 tx = self.read_tx_from_file() 2889 if tx: 2890 self.show_transaction(tx) 2891 2892 def do_process_from_txid(self): 2893 from electrum import transaction 2894 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':') 2895 if ok and txid: 2896 txid = str(txid).strip() 2897 raw_tx = self._fetch_tx_from_network(txid) 2898 if not raw_tx: 2899 return 2900 tx = transaction.Transaction(raw_tx) 2901 self.show_transaction(tx) 2902 2903 def _fetch_tx_from_network(self, txid: str) -> Optional[str]: 2904 if not self.network: 2905 self.show_message(_("You are offline.")) 2906 return 2907 try: 2908 raw_tx = self.network.run_from_another_thread( 2909 self.network.get_transaction(txid, timeout=10)) 2910 except UntrustedServerReturnedError as e: 2911 self.logger.info(f"Error getting transaction from network: {repr(e)}") 2912 self.show_message(_("Error getting transaction from network") + ":\n" + e.get_message_for_gui()) 2913 return 2914 except Exception as e: 2915 self.show_message(_("Error getting transaction from network") + ":\n" + repr(e)) 2916 return 2917 return raw_tx 2918 2919 @protected 2920 def export_privkeys_dialog(self, password): 2921 if self.wallet.is_watching_only(): 2922 self.show_message(_("This is a watching-only wallet")) 2923 return 2924 2925 if isinstance(self.wallet, Multisig_Wallet): 2926 self.show_message(_('WARNING: This is a multi-signature wallet.') + '\n' + 2927 _('It cannot be "backed up" by simply exporting these private keys.')) 2928 2929 d = WindowModalDialog(self, _('Private keys')) 2930 d.setMinimumSize(980, 300) 2931 vbox = QVBoxLayout(d) 2932 2933 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 2934 _("Exposing a single private key can compromise your entire wallet!"), 2935 _("In particular, DO NOT use 'redeem private key' services proposed by third parties.")) 2936 vbox.addWidget(QLabel(msg)) 2937 2938 e = QTextEdit() 2939 e.setReadOnly(True) 2940 vbox.addWidget(e) 2941 2942 defaultname = 'electrum-private-keys.csv' 2943 select_msg = _('Select file to export your private keys to') 2944 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg) 2945 vbox.addLayout(hbox) 2946 2947 b = OkButton(d, _('Export')) 2948 b.setEnabled(False) 2949 vbox.addLayout(Buttons(CancelButton(d), b)) 2950 2951 private_keys = {} 2952 addresses = self.wallet.get_addresses() 2953 done = False 2954 cancelled = False 2955 def privkeys_thread(): 2956 for addr in addresses: 2957 time.sleep(0.1) 2958 if done or cancelled: 2959 break 2960 privkey = self.wallet.export_private_key(addr, password) 2961 private_keys[addr] = privkey 2962 self.computing_privkeys_signal.emit() 2963 if not cancelled: 2964 self.computing_privkeys_signal.disconnect() 2965 self.show_privkeys_signal.emit() 2966 2967 def show_privkeys(): 2968 s = "\n".join(map(lambda x: x[0] + "\t"+ x[1], private_keys.items())) 2969 e.setText(s) 2970 b.setEnabled(True) 2971 self.show_privkeys_signal.disconnect() 2972 nonlocal done 2973 done = True 2974 2975 def on_dialog_closed(*args): 2976 nonlocal done 2977 nonlocal cancelled 2978 if not done: 2979 cancelled = True 2980 self.computing_privkeys_signal.disconnect() 2981 self.show_privkeys_signal.disconnect() 2982 2983 self.computing_privkeys_signal.connect(lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses)))) 2984 self.show_privkeys_signal.connect(show_privkeys) 2985 d.finished.connect(on_dialog_closed) 2986 threading.Thread(target=privkeys_thread).start() 2987 2988 if not d.exec_(): 2989 done = True 2990 return 2991 2992 filename = filename_e.text() 2993 if not filename: 2994 return 2995 2996 try: 2997 self.do_export_privkeys(filename, private_keys, csv_button.isChecked()) 2998 except (IOError, os.error) as reason: 2999 txt = "\n".join([ 3000 _("Electrum was unable to produce a private key-export."), 3001 str(reason) 3002 ]) 3003 self.show_critical(txt, title=_("Unable to create csv")) 3004 3005 except Exception as e: 3006 self.show_message(repr(e)) 3007 return 3008 3009 self.show_message(_("Private keys exported.")) 3010 3011 def do_export_privkeys(self, fileName, pklist, is_csv): 3012 with open(fileName, "w+") as f: 3013 os.chmod(fileName, 0o600) 3014 if is_csv: 3015 transaction = csv.writer(f) 3016 transaction.writerow(["address", "private_key"]) 3017 for addr, pk in pklist.items(): 3018 transaction.writerow(["%34s"%addr,pk]) 3019 else: 3020 f.write(json.dumps(pklist, indent = 4)) 3021 3022 def do_import_labels(self): 3023 def on_import(): 3024 self.need_update.set() 3025 import_meta_gui(self, _('labels'), self.wallet.import_labels, on_import) 3026 3027 def do_export_labels(self): 3028 export_meta_gui(self, _('labels'), self.wallet.export_labels) 3029 3030 def import_invoices(self): 3031 import_meta_gui(self, _('invoices'), self.wallet.import_invoices, self.invoice_list.update) 3032 3033 def export_invoices(self): 3034 export_meta_gui(self, _('invoices'), self.wallet.export_invoices) 3035 3036 def import_requests(self): 3037 import_meta_gui(self, _('requests'), self.wallet.import_requests, self.request_list.update) 3038 3039 def export_requests(self): 3040 export_meta_gui(self, _('requests'), self.wallet.export_requests) 3041 3042 def import_contacts(self): 3043 import_meta_gui(self, _('contacts'), self.contacts.import_file, self.contact_list.update) 3044 3045 def export_contacts(self): 3046 export_meta_gui(self, _('contacts'), self.contacts.export_file) 3047 3048 3049 def sweep_key_dialog(self): 3050 d = WindowModalDialog(self, title=_('Sweep private keys')) 3051 d.setMinimumSize(600, 300) 3052 vbox = QVBoxLayout(d) 3053 hbox_top = QHBoxLayout() 3054 hbox_top.addWidget(QLabel(_("Enter private keys:"))) 3055 hbox_top.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) 3056 vbox.addLayout(hbox_top) 3057 keys_e = ScanQRTextEdit(allow_multi=True, config=self.config) 3058 keys_e.setTabChangesFocus(True) 3059 vbox.addWidget(keys_e) 3060 3061 addresses = self.wallet.get_unused_addresses() 3062 if not addresses: 3063 try: 3064 addresses = self.wallet.get_receiving_addresses() 3065 except AttributeError: 3066 addresses = self.wallet.get_addresses() 3067 h, address_e = address_field(addresses) 3068 vbox.addLayout(h) 3069 3070 vbox.addStretch(1) 3071 button = OkButton(d, _('Sweep')) 3072 vbox.addLayout(Buttons(CancelButton(d), button)) 3073 button.setEnabled(False) 3074 3075 def get_address(): 3076 addr = str(address_e.text()).strip() 3077 if bitcoin.is_address(addr): 3078 return addr 3079 3080 def get_pk(*, raise_on_error=False): 3081 text = str(keys_e.toPlainText()) 3082 return keystore.get_private_keys(text, raise_on_error=raise_on_error) 3083 3084 def on_edit(): 3085 valid_privkeys = False 3086 try: 3087 valid_privkeys = get_pk(raise_on_error=True) is not None 3088 except Exception as e: 3089 button.setToolTip(f'{_("Error")}: {repr(e)}') 3090 else: 3091 button.setToolTip('') 3092 button.setEnabled(get_address() is not None and valid_privkeys) 3093 on_address = lambda text: address_e.setStyleSheet((ColorScheme.DEFAULT if get_address() else ColorScheme.RED).as_stylesheet()) 3094 keys_e.textChanged.connect(on_edit) 3095 address_e.textChanged.connect(on_edit) 3096 address_e.textChanged.connect(on_address) 3097 on_address(str(address_e.text())) 3098 if not d.exec_(): 3099 return 3100 # user pressed "sweep" 3101 addr = get_address() 3102 try: 3103 self.wallet.check_address_for_corruption(addr) 3104 except InternalAddressCorruption as e: 3105 self.show_error(str(e)) 3106 raise 3107 privkeys = get_pk() 3108 3109 def on_success(result): 3110 coins, keypairs = result 3111 outputs = [PartialTxOutput.from_address_and_value(addr, value='!')] 3112 self.warn_if_watching_only() 3113 self.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs) 3114 def on_failure(exc_info): 3115 self.on_error(exc_info) 3116 msg = _('Preparing sweep transaction...') 3117 task = lambda: self.network.run_from_another_thread( 3118 sweep_preparations(privkeys, self.network)) 3119 WaitingDialog(self, msg, task, on_success, on_failure) 3120 3121 def _do_import(self, title, header_layout, func): 3122 text = text_dialog( 3123 parent=self, 3124 title=title, 3125 header_layout=header_layout, 3126 ok_label=_('Import'), 3127 allow_multi=True, 3128 config=self.config, 3129 ) 3130 if not text: 3131 return 3132 keys = str(text).split() 3133 good_inputs, bad_inputs = func(keys) 3134 if good_inputs: 3135 msg = '\n'.join(good_inputs[:10]) 3136 if len(good_inputs) > 10: msg += '\n...' 3137 self.show_message(_("The following addresses were added") 3138 + f' ({len(good_inputs)}):\n' + msg) 3139 if bad_inputs: 3140 msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10]) 3141 if len(bad_inputs) > 10: msg += '\n...' 3142 self.show_error(_("The following inputs could not be imported") 3143 + f' ({len(bad_inputs)}):\n' + msg) 3144 self.address_list.update() 3145 self.history_list.update() 3146 3147 def import_addresses(self): 3148 if not self.wallet.can_import_address(): 3149 return 3150 title, msg = _('Import addresses'), _("Enter addresses")+':' 3151 self._do_import(title, msg, self.wallet.import_addresses) 3152 3153 @protected 3154 def do_import_privkey(self, password): 3155 if not self.wallet.can_import_privkey(): 3156 return 3157 title = _('Import private keys') 3158 header_layout = QHBoxLayout() 3159 header_layout.addWidget(QLabel(_("Enter private keys")+':')) 3160 header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) 3161 self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password)) 3162 3163 def update_fiat(self): 3164 b = self.fx and self.fx.is_enabled() 3165 self.fiat_send_e.setVisible(b) 3166 self.fiat_receive_e.setVisible(b) 3167 self.history_list.update() 3168 self.address_list.refresh_headers() 3169 self.address_list.update() 3170 self.update_status() 3171 3172 def settings_dialog(self): 3173 from .settings_dialog import SettingsDialog 3174 d = SettingsDialog(self, self.config) 3175 self.alias_received_signal.connect(d.set_alias_color) 3176 d.exec_() 3177 self.alias_received_signal.disconnect(d.set_alias_color) 3178 if self.fx: 3179 self.fx.trigger_update() 3180 run_hook('close_settings_dialog') 3181 if d.need_restart: 3182 self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success')) 3183 3184 def closeEvent(self, event): 3185 # note that closeEvent is NOT called if the user quits with Ctrl-C 3186 self.clean_up() 3187 event.accept() 3188 3189 def clean_up(self): 3190 if self._cleaned_up: 3191 return 3192 self._cleaned_up = True 3193 if self.wallet.thread: 3194 self.wallet.thread.stop() 3195 self.wallet.thread = None 3196 util.unregister_callback(self.on_network) 3197 self.config.set_key("is_maximized", self.isMaximized()) 3198 if not self.isMaximized(): 3199 g = self.geometry() 3200 self.wallet.db.put("winpos-qt", [g.left(),g.top(), 3201 g.width(),g.height()]) 3202 self.wallet.db.put("qt-console-history", self.console.history[-50:]) 3203 if self.qr_window: 3204 self.qr_window.close() 3205 self.close_wallet() 3206 3207 if self._update_check_thread: 3208 self._update_check_thread.exit() 3209 self._update_check_thread.wait() 3210 if self.tray: 3211 self.tray = None 3212 self.gui_object.timer.timeout.disconnect(self.timer_actions) 3213 self.gui_object.close_window(self) 3214 3215 def plugins_dialog(self): 3216 self.pluginsdialog = d = WindowModalDialog(self, _('Electrum Plugins')) 3217 3218 plugins = self.gui_object.plugins 3219 3220 vbox = QVBoxLayout(d) 3221 3222 # plugins 3223 scroll = QScrollArea() 3224 scroll.setEnabled(True) 3225 scroll.setWidgetResizable(True) 3226 scroll.setMinimumSize(400,250) 3227 vbox.addWidget(scroll) 3228 3229 w = QWidget() 3230 scroll.setWidget(w) 3231 w.setMinimumHeight(plugins.count() * 35) 3232 3233 grid = QGridLayout() 3234 grid.setColumnStretch(0,1) 3235 w.setLayout(grid) 3236 3237 settings_widgets = {} 3238 3239 def enable_settings_widget(p: Optional['BasePlugin'], name: str, i: int): 3240 widget = settings_widgets.get(name) # type: Optional[QWidget] 3241 if widget and not p: 3242 # plugin got disabled, rm widget 3243 grid.removeWidget(widget) 3244 widget.setParent(None) 3245 settings_widgets.pop(name) 3246 elif widget is None and p and p.requires_settings() and p.is_enabled(): 3247 # plugin got enabled, add widget 3248 widget = settings_widgets[name] = p.settings_widget(d) 3249 grid.addWidget(widget, i, 1) 3250 3251 def do_toggle(cb, name, i): 3252 p = plugins.toggle(name) 3253 cb.setChecked(bool(p)) 3254 enable_settings_widget(p, name, i) 3255 # note: all enabled plugins will receive this hook: 3256 run_hook('init_qt', self.gui_object) 3257 3258 for i, descr in enumerate(plugins.descriptions.values()): 3259 full_name = descr['__name__'] 3260 prefix, _separator, name = full_name.rpartition('.') 3261 p = plugins.get(name) 3262 if descr.get('registers_keystore'): 3263 continue 3264 try: 3265 cb = QCheckBox(descr['fullname']) 3266 plugin_is_loaded = p is not None 3267 cb_enabled = (not plugin_is_loaded and plugins.is_available(name, self.wallet) 3268 or plugin_is_loaded and p.can_user_disable()) 3269 cb.setEnabled(cb_enabled) 3270 cb.setChecked(plugin_is_loaded and p.is_enabled()) 3271 grid.addWidget(cb, i, 0) 3272 enable_settings_widget(p, name, i) 3273 cb.clicked.connect(partial(do_toggle, cb, name, i)) 3274 msg = descr['description'] 3275 if descr.get('requires'): 3276 msg += '\n\n' + _('Requires') + ':\n' + '\n'.join(map(lambda x: x[1], descr.get('requires'))) 3277 grid.addWidget(HelpButton(msg), i, 2) 3278 except Exception: 3279 self.logger.exception(f"cannot display plugin {name}") 3280 grid.setRowStretch(len(plugins.descriptions.values()), 1) 3281 vbox.addLayout(Buttons(CloseButton(d))) 3282 d.exec_() 3283 3284 def cpfp_dialog(self, parent_tx: Transaction) -> None: 3285 new_tx = self.wallet.cpfp(parent_tx, 0) 3286 total_size = parent_tx.estimated_size() + new_tx.estimated_size() 3287 parent_txid = parent_tx.txid() 3288 assert parent_txid 3289 parent_fee = self.wallet.get_tx_fee(parent_txid) 3290 if parent_fee is None: 3291 self.show_error(_("Can't CPFP: unknown fee for parent transaction.")) 3292 return 3293 d = WindowModalDialog(self, _('Child Pays for Parent')) 3294 vbox = QVBoxLayout(d) 3295 msg = ( 3296 "A CPFP is a transaction that sends an unconfirmed output back to " 3297 "yourself, with a high fee. The goal is to have miners confirm " 3298 "the parent transaction in order to get the fee attached to the " 3299 "child transaction.") 3300 vbox.addWidget(WWLabel(_(msg))) 3301 msg2 = ("The proposed fee is computed using your " 3302 "fee/kB settings, applied to the total size of both child and " 3303 "parent transactions. After you broadcast a CPFP transaction, " 3304 "it is normal to see a new unconfirmed transaction in your history.") 3305 vbox.addWidget(WWLabel(_(msg2))) 3306 grid = QGridLayout() 3307 grid.addWidget(QLabel(_('Total size') + ':'), 0, 0) 3308 grid.addWidget(QLabel('%d bytes'% total_size), 0, 1) 3309 max_fee = new_tx.output_value() 3310 grid.addWidget(QLabel(_('Input amount') + ':'), 1, 0) 3311 grid.addWidget(QLabel(self.format_amount(max_fee) + ' ' + self.base_unit()), 1, 1) 3312 output_amount = QLabel('') 3313 grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0) 3314 grid.addWidget(output_amount, 2, 1) 3315 fee_e = BTCAmountEdit(self.get_decimal_point) 3316 combined_fee = QLabel('') 3317 combined_feerate = QLabel('') 3318 def on_fee_edit(x): 3319 fee_for_child = fee_e.get_amount() 3320 if fee_for_child is None: 3321 return 3322 out_amt = max_fee - fee_for_child 3323 out_amt_str = (self.format_amount(out_amt) + ' ' + self.base_unit()) if out_amt else '' 3324 output_amount.setText(out_amt_str) 3325 comb_fee = parent_fee + fee_for_child 3326 comb_fee_str = (self.format_amount(comb_fee) + ' ' + self.base_unit()) if comb_fee else '' 3327 combined_fee.setText(comb_fee_str) 3328 comb_feerate = comb_fee / total_size * 1000 3329 comb_feerate_str = self.format_fee_rate(comb_feerate) if comb_feerate else '' 3330 combined_feerate.setText(comb_feerate_str) 3331 fee_e.textChanged.connect(on_fee_edit) 3332 def get_child_fee_from_total_feerate(fee_per_kb: Optional[int]) -> Optional[int]: 3333 if fee_per_kb is None: 3334 return None 3335 fee = fee_per_kb * total_size / 1000 - parent_fee 3336 fee = round(fee) 3337 fee = min(max_fee, fee) 3338 fee = max(total_size, fee) # pay at least 1 sat/byte for combined size 3339 return fee 3340 suggested_feerate = self.config.fee_per_kb() 3341 fee = get_child_fee_from_total_feerate(suggested_feerate) 3342 fee_e.setAmount(fee) 3343 grid.addWidget(QLabel(_('Fee for child') + ':'), 3, 0) 3344 grid.addWidget(fee_e, 3, 1) 3345 def on_rate(dyn, pos, fee_rate): 3346 fee = get_child_fee_from_total_feerate(fee_rate) 3347 fee_e.setAmount(fee) 3348 fee_slider = FeeSlider(self, self.config, on_rate) 3349 fee_combo = FeeComboBox(fee_slider) 3350 fee_slider.update() 3351 grid.addWidget(fee_slider, 4, 1) 3352 grid.addWidget(fee_combo, 4, 2) 3353 grid.addWidget(QLabel(_('Total fee') + ':'), 5, 0) 3354 grid.addWidget(combined_fee, 5, 1) 3355 grid.addWidget(QLabel(_('Total feerate') + ':'), 6, 0) 3356 grid.addWidget(combined_feerate, 6, 1) 3357 vbox.addLayout(grid) 3358 vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) 3359 if not d.exec_(): 3360 return 3361 fee = fee_e.get_amount() 3362 if fee is None: 3363 return # fee left empty, treat is as "cancel" 3364 if fee > max_fee: 3365 self.show_error(_('Max fee exceeded')) 3366 return 3367 try: 3368 new_tx = self.wallet.cpfp(parent_tx, fee) 3369 except CannotCPFP as e: 3370 self.show_error(str(e)) 3371 return 3372 self.show_transaction(new_tx) 3373 3374 def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool: 3375 """Returns whether successful.""" 3376 # note side-effect: tx is being mutated 3377 assert isinstance(tx, PartialTransaction) 3378 try: 3379 # note: this might download input utxos over network 3380 BlockingWaitingDialog( 3381 self, 3382 _("Adding info to tx, from wallet and network..."), 3383 lambda: tx.add_info_from_wallet(self.wallet, ignore_network_issues=False), 3384 ) 3385 except NetworkException as e: 3386 self.show_error(repr(e)) 3387 return False 3388 return True 3389 3390 def bump_fee_dialog(self, tx: Transaction): 3391 txid = tx.txid() 3392 if not isinstance(tx, PartialTransaction): 3393 tx = PartialTransaction.from_tx(tx) 3394 if not self._add_info_to_tx_from_wallet_and_network(tx): 3395 return 3396 d = BumpFeeDialog(main_window=self, tx=tx, txid=txid) 3397 d.run() 3398 3399 def dscancel_dialog(self, tx: Transaction): 3400 txid = tx.txid() 3401 if not isinstance(tx, PartialTransaction): 3402 tx = PartialTransaction.from_tx(tx) 3403 if not self._add_info_to_tx_from_wallet_and_network(tx): 3404 return 3405 d = DSCancelDialog(main_window=self, tx=tx, txid=txid) 3406 d.run() 3407 3408 def save_transaction_into_wallet(self, tx: Transaction): 3409 win = self.top_level_window() 3410 try: 3411 if not self.wallet.add_transaction(tx): 3412 win.show_error(_("Transaction could not be saved.") + "\n" + 3413 _("It conflicts with current history.")) 3414 return False 3415 except AddTransactionException as e: 3416 win.show_error(e) 3417 return False 3418 else: 3419 self.wallet.save_db() 3420 # need to update at least: history_list, utxo_list, address_list 3421 self.need_update.set() 3422 msg = (_("Transaction added to wallet history.") + '\n\n' + 3423 _("Note: this is an offline transaction, if you want the network " 3424 "to see it, you need to broadcast it.")) 3425 win.msg_box(QPixmap(icon_path("offline_tx.png")), None, _('Success'), msg) 3426 return True 3427 3428 def show_cert_mismatch_error(self): 3429 if self.showing_cert_mismatch_error: 3430 return 3431 self.showing_cert_mismatch_error = True 3432 self.show_critical(title=_("Certificate mismatch"), 3433 msg=_("The SSL certificate provided by the main server did not match the fingerprint passed in with the --serverfingerprint option.") + "\n\n" + 3434 _("Electrum will now exit.")) 3435 self.showing_cert_mismatch_error = False 3436 self.close() 3437