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