From af8d1fb401d2f91a779c24afa35b6360208eda4a Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 4 Mar 2025 17:02:22 +0100 Subject: [PATCH] handle jit invoices in qml make min funding amount symbol, change Signal name, change Exception type change minChannelFunding to QEAmount and make message text variable qml: improve translatibility of strings init minchannelfunding value in init method rebase on master --- .../qml/components/ReceiveDetailsDialog.qml | 36 +++++++++++++++++-- electrum/gui/qml/qerequestdetails.py | 11 ++++-- electrum/gui/qml/qewallet.py | 12 +++++++ electrum/lnworker.py | 20 ++++++++--- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 23c840e9e..cc0ba0d47 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -106,10 +106,42 @@ ElDialog { } FlatButton { Layout.fillWidth: true - enabled: Daemon.currentWallet.isLightning && Daemon.currentWallet.lightningCanReceive.satsInt > amountBtc.textAsSats.satsInt + enabled: Daemon.currentWallet.isLightning && (Daemon.currentWallet.lightningCanReceive.satsInt + > amountBtc.textAsSats.satsInt || Daemon.currentWallet.canGetZeroconfChannel) text: qsTr('Lightning') icon.source: '../../icons/lightning.png' - onClicked: { dialog.isLightning = true; doAccept() } + onClicked: { + if (Daemon.currentWallet.lightningCanReceive.satsInt > amountBtc.textAsSats.satsInt) { + // can receive on existing channel + dialog.isLightning = true + doAccept() + } else if (Daemon.currentWallet.canGetZeroconfChannel && amountBtc.textAsSats.satsInt + >= Daemon.currentWallet.minChannelFunding.satsInt) { + // ask for confirmation of zeroconf channel to prevent fee surprise + var confirmdialog = app.messageDialog.createObject(dialog, { + title: qsTr('Confirm just-in-time channel'), + text: [qsTr('Receiving this payment will purchase a Lightning channel from your service provider.'), + qsTr('Fees will be deducted from the payment.'), + qsTr('Do you want to continue?')].join(' '), + yesno: true + }) + confirmdialog.accepted.connect(function () { + dialog.isLightning = true + doAccept() + }) + confirmdialog.open() + } else { + // show error that amnt > 200k is neccessary to get zeroconf channel + var confirmdialog = app.messageDialog.createObject(dialog, { + title: qsTr("Amount too low"), + text: [qsTr("You don't have channels with enough inbound liquidity to receive this payment."), + qsTr("Request at least %1 to open a channel just-in-time.").arg( + Config.formatSats(Daemon.currentWallet.minChannelFunding.satsInt, true))].join(' ') + }) + confirmdialog.open() + } + // can't get zeroconf channel and doesn't have enough inbound liquidity + } } } } diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index ef259ec2a..82dc5fa33 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -6,6 +6,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, py from electrum.logging import get_logger from electrum.invoices import (PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, LN_EXPIRY_NEVER) +from electrum.lnutil import MIN_FUNDING_SAT from .qewallet import QEWallet from .qetypes import QEAmount @@ -118,9 +119,13 @@ class QERequestDetails(QObject, QtEventListener): @pyqtProperty(str, notify=detailsChanged) def bolt11(self): - can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 - if self._req and can_receive > 0 and (self._req.get_amount_sat() or 0) <= can_receive: - bolt11 = self._wallet.wallet.get_bolt11_invoice(self._req) + wallet = self._wallet.wallet + amount_sat = self._req.get_amount_sat() or 0 if self._req else 0 + can_receive = wallet.lnworker.num_sats_can_receive() if wallet.lnworker else 0 + will_req_zeroconf = wallet.lnworker.receive_requires_jit_channel(amount_msat=amount_sat*1000) + if self._req and ((can_receive > 0 and amount_sat <= can_receive) + or (will_req_zeroconf and amount_sat >= MIN_FUNDING_SAT)): + bolt11 = wallet.get_bolt11_invoice(self._req) else: return '' # encode lightning invoices as uppercase so QR encoding can use diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 9953c5455..8fd77e124 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -15,6 +15,7 @@ from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.transaction import PartialTransaction, Transaction from electrum.util import InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop, NotEnoughFunds, \ NoDynamicFeeEstimates +from electrum.lnutil import MIN_FUNDING_SAT from electrum.plugin import run_hook from electrum.wallet import Multisig_Wallet from electrum.crypto import pw_decode_with_version_and_mac @@ -26,6 +27,7 @@ from .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel from .qetransactionlistmodel import QETransactionListModel from .qetypes import QEAmount from .util import QtEventListener, qt_event_listener +from ...lntransport import extract_nodeid from ...fee_policy import FeePolicy if TYPE_CHECKING: @@ -101,6 +103,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener): self._frozenbalance = QEAmount() self._totalbalance = QEAmount() self._lightningcanreceive = QEAmount() + self._minchannelfunding = QEAmount(amount_sat=int(MIN_FUNDING_SAT)) self._lightningcansend = QEAmount() self._lightningbalancefrozen = QEAmount() @@ -459,6 +462,11 @@ class QEWallet(AuthMixin, QObject, QtEventListener): def canSignMessage(self): return not isinstance(self.wallet, Multisig_Wallet) and not self.wallet.is_watching_only() + canGetZeroconfChannelChanged = pyqtSignal() + @pyqtProperty(bool, notify=canGetZeroconfChannelChanged) + def canGetZeroconfChannel(self) -> bool: + return self.wallet.lnworker and self.wallet.lnworker.can_get_zeroconf_channel() + @pyqtProperty(QEAmount, notify=balanceChanged) def frozenBalance(self): c, u, x = self.wallet.get_frozen_balance() @@ -506,6 +514,10 @@ class QEWallet(AuthMixin, QObject, QtEventListener): self._lightningcanreceive.satsInt = int(self.wallet.lnworker.num_sats_can_receive()) return self._lightningcanreceive + @pyqtProperty(QEAmount, notify=dataChanged) + def minChannelFunding(self): + return self._minchannelfunding + @pyqtProperty(int, notify=peersUpdated) def lightningNumPeers(self): if self.isLightning: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 329d031c8..525d531fa 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2193,7 +2193,7 @@ class LNWallet(LNWorker): timestamp = int(time.time()) needs_jit: bool = self.receive_requires_jit_channel(amount_msat) routing_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels, needs_jit=needs_jit) - self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}, jit: {needs_jit}") + self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}, jit: {needs_jit}, sat: {amount_msat or 0 // 1000}") invoice_features = self.features.for_invoice() if not self.uses_trampoline(): invoice_features &= ~ LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM @@ -2756,10 +2756,8 @@ class LNWallet(LNWorker): """Returns true if we cannot receive the amount and have set up a trusted LSP node. Cannot work reliably with 0 amount invoices as we don't know if we are able to receive it. """ - # a trusted zeroconf node is configured - if (self.config.ZEROCONF_TRUSTED_NODE - # the zeroconf node is a peer, it doesn't make sense to request a channel from an offline LSP - and extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0] in self.peers + # zeroconf provider is configured and connected + if (self.can_get_zeroconf_channel() # we cannot receive the amount specified and ((amount_msat and self.num_sats_can_receive() < (amount_msat // 1000)) # or we cannot receive anything, and it's a 0 amount invoice @@ -2767,6 +2765,18 @@ class LNWallet(LNWorker): return True return False + def can_get_zeroconf_channel(self) -> bool: + if not self.config.ACCEPT_ZEROCONF_CHANNELS and self.config.ZEROCONF_TRUSTED_NODE: + # check if zeroconf is accepted and client has trusted zeroconf node configured + return False + try: + node_id = extract_nodeid(self.wallet.config.ZEROCONF_TRUSTED_NODE)[0] + except ConnStringFormatError: + # invalid connection string + return False + # only return True if we are connected to the zeroconf provider + return node_id in self.peers + def _suggest_channels_for_rebalance(self, direction, amount_sat) -> Sequence[Tuple[Channel, int]]: """ Suggest a channel and amount to send/receive with that channel, so that we will be able to receive/send amount_sat