lnurl: implement LNURL-withdraw
adds handling of lnurl-withdraw payment identifiers which allow users to withdraw bitcoin from a service by scanning a qr code or pasting the lnurl-w code as "sending" address.
This commit is contained in:
161
electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml
Normal file
161
electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml
Normal file
@@ -0,0 +1,161 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
|
||||
import org.electrum 1.0
|
||||
|
||||
import "controls"
|
||||
|
||||
ElDialog {
|
||||
id: dialog
|
||||
|
||||
title: qsTr('LNURL Withdraw request')
|
||||
iconSource: '../../../icons/link.png'
|
||||
|
||||
property InvoiceParser invoiceParser
|
||||
|
||||
padding: 0
|
||||
|
||||
property int walletCanReceive: invoiceParser.wallet.lightningCanReceive.satsInt
|
||||
property int providerMinWithdrawable: parseInt(invoiceParser.lnurlData['min_withdrawable_sat'])
|
||||
property int providerMaxWithdrawable: parseInt(invoiceParser.lnurlData['max_withdrawable_sat'])
|
||||
property int effectiveMinWithdrawable: Math.max(providerMinWithdrawable, 1)
|
||||
property int effectiveMaxWithdrawable: Math.min(providerMaxWithdrawable, walletCanReceive)
|
||||
property bool insufficientLiquidity: effectiveMinWithdrawable > walletCanReceive
|
||||
property bool liquidityWarning: providerMaxWithdrawable > walletCanReceive
|
||||
|
||||
property bool amountValid: !dialog.insufficientLiquidity &&
|
||||
amountBtc.textAsSats.satsInt >= dialog.effectiveMinWithdrawable &&
|
||||
amountBtc.textAsSats.satsInt <= dialog.effectiveMaxWithdrawable
|
||||
property bool valid: amountValid
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
||||
GridLayout {
|
||||
id: rootLayout
|
||||
columns: 2
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: constants.paddingLarge
|
||||
Layout.rightMargin: constants.paddingLarge
|
||||
Layout.bottomMargin: constants.paddingLarge
|
||||
|
||||
InfoTextArea {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
compact: true
|
||||
visible: dialog.insufficientLiquidity
|
||||
text: qsTr('Too little incoming liquidity to satisfy this withdrawal request.')
|
||||
+ '\n\n'
|
||||
+ qsTr('Can receive: %1')
|
||||
.arg(Config.formatSats(dialog.walletCanReceive) + ' ' + Config.baseUnit)
|
||||
+ '\n'
|
||||
+ qsTr('Minimum withdrawal amount: %1')
|
||||
.arg(Config.formatSats(dialog.providerMinWithdrawable) + ' ' + Config.baseUnit)
|
||||
+ '\n\n'
|
||||
+ qsTr('Do a submarine swap in the \'Channels\' tab to get more incoming liquidity.')
|
||||
iconStyle: InfoTextArea.IconStyle.Error
|
||||
}
|
||||
|
||||
InfoTextArea {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
compact: true
|
||||
visible: !dialog.insufficientLiquidity && dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable
|
||||
text: qsTr('Amount must be between %1 and %2 %3')
|
||||
.arg(Config.formatSats(dialog.effectiveMinWithdrawable))
|
||||
.arg(Config.formatSats(dialog.effectiveMaxWithdrawable))
|
||||
.arg(Config.baseUnit)
|
||||
}
|
||||
|
||||
InfoTextArea {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
compact: true
|
||||
visible: dialog.liquidityWarning && !dialog.insufficientLiquidity
|
||||
text: qsTr('The maximum withdrawable amount (%1) is larger than what your channels can receive (%2).')
|
||||
.arg(Config.formatSats(dialog.providerMaxWithdrawable) + ' ' + Config.baseUnit)
|
||||
.arg(Config.formatSats(dialog.walletCanReceive) + ' ' + Config.baseUnit)
|
||||
+ ' '
|
||||
+ qsTr('You may need to do a submarine swap to increase your incoming liquidity.')
|
||||
iconStyle: InfoTextArea.IconStyle.Warn
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Provider')
|
||||
color: Material.accentColor
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: invoiceParser.lnurlData['domain']
|
||||
}
|
||||
Label {
|
||||
text: qsTr('Description')
|
||||
color: Material.accentColor
|
||||
visible: invoiceParser.lnurlData['default_description']
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: invoiceParser.lnurlData['default_description']
|
||||
visible: invoiceParser.lnurlData['default_description']
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Amount')
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
BtcField {
|
||||
id: amountBtc
|
||||
Layout.preferredWidth: rootLayout.width / 3
|
||||
text: Config.formatSatsForEditing(dialog.effectiveMaxWithdrawable)
|
||||
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
|
||||
color: Material.foreground // override gray-out on disabled
|
||||
fiatfield: amountFiat
|
||||
onTextAsSatsChanged: {
|
||||
invoiceParser.amountOverride = textAsSats
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: Config.baseUnit
|
||||
color: Material.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
Item { visible: Daemon.fx.enabled; Layout.preferredWidth: 1; Layout.preferredHeight: 1 }
|
||||
|
||||
RowLayout {
|
||||
visible: Daemon.fx.enabled
|
||||
FiatField {
|
||||
id: amountFiat
|
||||
Layout.preferredWidth: rootLayout.width / 3
|
||||
btcfield: amountBtc
|
||||
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
|
||||
color: Material.foreground
|
||||
}
|
||||
Label {
|
||||
text: Daemon.fx.fiatCurrency
|
||||
color: Material.accentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlatButton {
|
||||
Layout.topMargin: constants.paddingLarge
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('Withdraw...')
|
||||
icon.source: '../../icons/confirmed.png'
|
||||
enabled: valid
|
||||
onClicked: {
|
||||
invoiceParser.lnurlRequestWithdrawal()
|
||||
dialog.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,8 +57,8 @@ ElDialog {
|
||||
Layout.fillHeight: true
|
||||
|
||||
hint: Daemon.currentWallet.isLightning
|
||||
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
|
||||
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
|
||||
? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')
|
||||
: qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')
|
||||
|
||||
onFoundText: (data) => {
|
||||
dialog.dispatch(data)
|
||||
|
||||
@@ -44,8 +44,8 @@ Item {
|
||||
// Android based send dialog if on android
|
||||
var scanner = app.scanDialog.createObject(mainView, {
|
||||
hint: Daemon.currentWallet.isLightning
|
||||
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
|
||||
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
|
||||
? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')
|
||||
: qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')
|
||||
})
|
||||
scanner.onFoundText.connect(function(data) {
|
||||
data = data.trim()
|
||||
@@ -423,9 +423,18 @@ Item {
|
||||
|
||||
onLnurlRetrieved: {
|
||||
closeSendDialog()
|
||||
var dialog = lnurlPayDialog.createObject(app, {
|
||||
invoiceParser: invoiceParser
|
||||
})
|
||||
if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {
|
||||
var dialog = lnurlPayDialog.createObject(app, {
|
||||
invoiceParser: invoiceParser
|
||||
})
|
||||
} else if (invoiceParser.invoiceType === Invoice.Type.LNURLWithdrawRequest) {
|
||||
var dialog = lnurlWithdrawDialog.createObject(app, {
|
||||
invoiceParser: invoiceParser
|
||||
})
|
||||
} else {
|
||||
console.log("Unsupported LNURL type:", invoiceParser.invoiceType)
|
||||
return
|
||||
}
|
||||
dialog.open()
|
||||
}
|
||||
onLnurlError: (code, message) => {
|
||||
@@ -739,6 +748,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: lnurlWithdrawDialog
|
||||
LnurlWithdrawRequestDialog {
|
||||
width: parent.width * 0.9
|
||||
anchors.centerIn: parent
|
||||
|
||||
onClosed: destroy()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: otpDialog
|
||||
OtpDialog {
|
||||
|
||||
@@ -14,6 +14,7 @@ from electrum.invoices import (
|
||||
)
|
||||
from electrum.transaction import PartialTxOutput, TxOutput
|
||||
from electrum.lnutil import format_short_channel_id
|
||||
from electrum.lnurl import LNURLData, LNURL3Data, LNURL6Data, request_lnurl_withdraw_callback, LNURLError
|
||||
from electrum.bitcoin import COIN, address_to_script
|
||||
from electrum.paymentrequest import PaymentRequest
|
||||
from electrum.payment_identifier import PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType
|
||||
@@ -33,6 +34,7 @@ class QEInvoice(QObject, QtEventListener):
|
||||
OnchainInvoice = 0
|
||||
LightningInvoice = 1
|
||||
LNURLPayRequest = 2
|
||||
LNURLWithdrawRequest = 3
|
||||
|
||||
@pyqtEnum
|
||||
class Status(IntEnum):
|
||||
@@ -478,7 +480,7 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
@pyqtProperty(bool, notify=lnurlRetrieved)
|
||||
def isLnurlPay(self):
|
||||
return self._lnurlData is not None
|
||||
return self._lnurlData is not None and self.invoiceType == QEInvoice.Type.LNURLPayRequest
|
||||
|
||||
@pyqtProperty(bool, notify=busyChanged)
|
||||
def busy(self):
|
||||
@@ -513,6 +515,12 @@ class QEInvoiceParser(QEInvoice):
|
||||
self._effectiveInvoice = None
|
||||
self.invoiceChanged.emit()
|
||||
|
||||
def setValidLNURLWithdrawRequest(self):
|
||||
self._logger.debug('setValidLNURLWithdrawRequest')
|
||||
self.setInvoiceType(QEInvoice.Type.LNURLWithdrawRequest)
|
||||
self._effectiveInvoice = None
|
||||
self.invoiceChanged.emit()
|
||||
|
||||
def create_onchain_invoice(self, outputs, message, payment_request, uri):
|
||||
return self._wallet.wallet.create_invoice(
|
||||
outputs=outputs,
|
||||
@@ -543,7 +551,7 @@ class QEInvoiceParser(QEInvoice):
|
||||
self._pi = PaymentIdentifier(self._wallet.wallet, recipient)
|
||||
if not self._pi.is_valid() or self._pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,
|
||||
PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11,
|
||||
PaymentIdentifierType.LNURLP,
|
||||
PaymentIdentifierType.LNURL,
|
||||
PaymentIdentifierType.EMAILLIKE,
|
||||
PaymentIdentifierType.DOMAINLIKE]:
|
||||
self.validationError.emit('unknown', _('Unknown invoice'))
|
||||
@@ -562,7 +570,11 @@ class QEInvoiceParser(QEInvoice):
|
||||
self.resolve_pi()
|
||||
return
|
||||
|
||||
if self._pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]:
|
||||
if self._pi.type in [
|
||||
PaymentIdentifierType.LNURLP,
|
||||
PaymentIdentifierType.LNURLW,
|
||||
PaymentIdentifierType.LNADDR,
|
||||
]:
|
||||
self.on_lnurl(self._pi.lnurl_data)
|
||||
return
|
||||
|
||||
@@ -622,7 +634,7 @@ class QEInvoiceParser(QEInvoice):
|
||||
if pi.is_error():
|
||||
if pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE]:
|
||||
msg = _('Could not resolve address')
|
||||
elif pi.type == PaymentIdentifierType.LNURLP:
|
||||
elif pi.type == PaymentIdentifierType.LNURL:
|
||||
msg = _('Could not resolve LNURL') + "\n\n" + pi.get_error()
|
||||
elif pi.type == PaymentIdentifierType.BIP70:
|
||||
msg = _('Could not resolve BIP70 payment request: {}').format(pi.error)
|
||||
@@ -637,19 +649,32 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
self._pi.resolve(on_finished=on_finished)
|
||||
|
||||
def on_lnurl(self, lnurldata):
|
||||
def on_lnurl(self, lnurldata: LNURLData):
|
||||
self._logger.debug('on_lnurl')
|
||||
self._logger.debug(f'{repr(lnurldata)}')
|
||||
|
||||
self._lnurlData = {
|
||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||
'callback_url': lnurldata.callback_url,
|
||||
'min_sendable_sat': lnurldata.min_sendable_sat,
|
||||
'max_sendable_sat': lnurldata.max_sendable_sat,
|
||||
'metadata_plaintext': lnurldata.metadata_plaintext,
|
||||
'comment_allowed': lnurldata.comment_allowed
|
||||
}
|
||||
self.setValidLNURLPayRequest()
|
||||
if isinstance(lnurldata, LNURL6Data):
|
||||
self._lnurlData = {
|
||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||
'callback_url': lnurldata.callback_url,
|
||||
'min_sendable_sat': lnurldata.min_sendable_sat,
|
||||
'max_sendable_sat': lnurldata.max_sendable_sat,
|
||||
'metadata_plaintext': lnurldata.metadata_plaintext,
|
||||
'comment_allowed': lnurldata.comment_allowed,
|
||||
}
|
||||
self.setValidLNURLPayRequest()
|
||||
elif isinstance(lnurldata, LNURL3Data):
|
||||
self._lnurlData = {
|
||||
'domain': urlparse(lnurldata.callback_url).netloc,
|
||||
'callback_url': lnurldata.callback_url,
|
||||
'min_withdrawable_sat': lnurldata.min_withdrawable_sat,
|
||||
'max_withdrawable_sat': lnurldata.max_withdrawable_sat,
|
||||
'default_description': lnurldata.default_description,
|
||||
'k1': lnurldata.k1,
|
||||
}
|
||||
self.setValidLNURLWithdrawRequest()
|
||||
else:
|
||||
raise NotImplementedError(f"Invalid lnurl type in on_lnurl {lnurldata=}")
|
||||
self.lnurlRetrieved.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -657,6 +682,7 @@ class QEInvoiceParser(QEInvoice):
|
||||
def lnurlGetInvoice(self, comment=None):
|
||||
assert self._lnurlData
|
||||
assert self._pi.need_finalize()
|
||||
assert self.invoiceType == QEInvoice.Type.LNURLPayRequest
|
||||
self._logger.debug(f'{repr(self._lnurlData)}')
|
||||
|
||||
amount = self.amountOverride.satsInt
|
||||
@@ -681,6 +707,54 @@ class QEInvoiceParser(QEInvoice):
|
||||
|
||||
self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished)
|
||||
|
||||
@pyqtSlot()
|
||||
def lnurlRequestWithdrawal(self):
|
||||
assert self._lnurlData
|
||||
assert self.invoiceType == QEInvoice.Type.LNURLWithdrawRequest
|
||||
self._logger.debug(f'{repr(self._lnurlData)}')
|
||||
|
||||
amount_sat = self.amountOverride.satsInt
|
||||
|
||||
try:
|
||||
key = self.wallet.wallet.create_request(
|
||||
amount_sat=amount_sat,
|
||||
message=self._lnurlData.get('default_description', ''),
|
||||
exp_delay=120,
|
||||
address=None,
|
||||
)
|
||||
req = self.wallet.wallet.get_request(key)
|
||||
_lnaddr, b11_invoice = self.wallet.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=req.payment_hash,
|
||||
amount_msat=req.get_amount_msat(),
|
||||
message=req.get_message(),
|
||||
expiry=req.exp,
|
||||
fallback_address=None
|
||||
)
|
||||
except Exception as e:
|
||||
self._logger.exception('')
|
||||
self.lnurlError.emit(
|
||||
'lnurl',
|
||||
_("Failed to create payment request for withdrawal: {}").format(str(e))
|
||||
)
|
||||
return
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
|
||||
coro = request_lnurl_withdraw_callback(
|
||||
callback_url=self._lnurlData['callback_url'],
|
||||
k1=self._lnurlData['k1'],
|
||||
bolt_11=b11_invoice,
|
||||
)
|
||||
try:
|
||||
Network.run_from_another_thread(coro)
|
||||
except LNURLError as e:
|
||||
self.lnurlError.emit('lnurl', str(e))
|
||||
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
|
||||
def on_lnurl_invoice(self, orig_amount, invoice):
|
||||
self._logger.debug('on_lnurl_invoice')
|
||||
self._logger.debug(f'{repr(invoice)}')
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Union, Mapping
|
||||
import urllib.parse
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal, QPoint, Qt
|
||||
from PyQt6.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout,
|
||||
@@ -20,15 +21,18 @@ from electrum.util import (
|
||||
from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST
|
||||
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
|
||||
from electrum.network import TxBroadcastError, BestEffortRequestFailed
|
||||
from electrum.payment_identifier import (PaymentIdentifierType, PaymentIdentifier, invoice_from_payment_identifier,
|
||||
payment_identifier_from_invoice)
|
||||
from electrum.payment_identifier import (PaymentIdentifierType, PaymentIdentifier,
|
||||
invoice_from_payment_identifier,
|
||||
payment_identifier_from_invoice, PaymentIdentifierState)
|
||||
from electrum.submarine_swaps import SwapServerError
|
||||
from electrum.fee_policy import FeePolicy, FixedFeePolicy
|
||||
from electrum.lnurl import LNURL3Data, request_lnurl_withdraw_callback, LNURLError
|
||||
|
||||
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
|
||||
from .paytoedit import InvalidPaymentIdentifier
|
||||
from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit,
|
||||
get_icon_camera, read_QIcon, ColorScheme, IconLabel, Spinner, add_input_actions_to_context_menu)
|
||||
get_icon_camera, read_QIcon, ColorScheme, IconLabel, Spinner, Buttons, WWLabel,
|
||||
add_input_actions_to_context_menu, WindowModalDialog, OkButton, CancelButton)
|
||||
from .invoice_list import InvoiceList
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -435,7 +439,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
self.send_button.setEnabled(False)
|
||||
return
|
||||
|
||||
lock_recipient = pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR,
|
||||
lock_recipient = pi.type in [PaymentIdentifierType.LNURL, PaymentIdentifierType.LNURLW,
|
||||
PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR,
|
||||
PaymentIdentifierType.OPENALIAS, PaymentIdentifierType.BIP70,
|
||||
PaymentIdentifierType.BIP21, PaymentIdentifierType.BOLT11] and not pi.need_resolve()
|
||||
lock_amount = pi.is_amount_locked()
|
||||
@@ -504,6 +509,13 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
self.show_error(pi.error)
|
||||
self.do_clear()
|
||||
return
|
||||
if pi.type == PaymentIdentifierType.LNURLW:
|
||||
assert pi.state == PaymentIdentifierState.LNURLW_FINALIZE, \
|
||||
f"Detected LNURLW but not ready to finalize? {pi=}"
|
||||
self.do_clear()
|
||||
self.request_lnurl_withdraw_dialog(pi.lnurl_data)
|
||||
return
|
||||
|
||||
# if openalias add openalias to contacts
|
||||
if pi.type == PaymentIdentifierType.OPENALIAS:
|
||||
key = pi.emaillike if pi.emaillike else pi.domainlike
|
||||
@@ -830,3 +842,145 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
else:
|
||||
total += output.value
|
||||
self.amount_e.setAmount(total if outputs else None)
|
||||
|
||||
def request_lnurl_withdraw_dialog(self, lnurl_data: LNURL3Data):
|
||||
if not self.wallet.has_lightning():
|
||||
self.show_error(
|
||||
_("Cannot request lightning withdrawal, wallet has no lightning channels.")
|
||||
)
|
||||
return
|
||||
|
||||
dialog = WindowModalDialog(self, _("Lightning Withdrawal"))
|
||||
dialog.setMinimumWidth(400)
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
dialog.setLayout(vbox)
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(8)
|
||||
grid.setColumnStretch(3, 1) # Make the last column stretch
|
||||
|
||||
row = 0
|
||||
|
||||
# provider url
|
||||
domain_label = QLabel(_("Provider") + ":")
|
||||
domain_text = WWLabel(urllib.parse.urlparse(lnurl_data.callback_url).netloc)
|
||||
grid.addWidget(domain_label, row, 0)
|
||||
grid.addWidget(domain_text, row, 1, 1, 3)
|
||||
row += 1
|
||||
|
||||
if lnurl_data.default_description:
|
||||
desc_label = QLabel(_("Description") + ":")
|
||||
desc_text = WWLabel(lnurl_data.default_description)
|
||||
grid.addWidget(desc_label, row, 0)
|
||||
grid.addWidget(desc_text, row, 1, 1, 3)
|
||||
row += 1
|
||||
|
||||
min_amount = max(lnurl_data.min_withdrawable_sat, 1)
|
||||
max_amount = min(
|
||||
lnurl_data.max_withdrawable_sat,
|
||||
int(self.wallet.lnworker.num_sats_can_receive())
|
||||
)
|
||||
min_text = self.format_amount_and_units(lnurl_data.min_withdrawable_sat)
|
||||
if min_amount > int(self.wallet.lnworker.num_sats_can_receive()):
|
||||
self.show_error("".join([
|
||||
_("Too little incoming liquidity to satisfy this withdrawal request."), "\n\n",
|
||||
_("Can receive: {}").format(
|
||||
self.format_amount_and_units(self.wallet.lnworker.num_sats_can_receive()),
|
||||
), "\n",
|
||||
_("Minimum withdrawal amount: {}").format(min_text), "\n\n",
|
||||
_("Do a submarine swap in the 'Channels' tab to get more incoming liquidity.")
|
||||
]))
|
||||
return
|
||||
|
||||
is_fixed_amount = lnurl_data.min_withdrawable_sat == lnurl_data.max_withdrawable_sat
|
||||
|
||||
# Range information (only for non-fixed amounts)
|
||||
if not is_fixed_amount:
|
||||
range_label_text = QLabel(_("Range") + ":")
|
||||
range_value = QLabel("{} - {}".format(
|
||||
min_text,
|
||||
self.format_amount_and_units(lnurl_data.max_withdrawable_sat)
|
||||
))
|
||||
grid.addWidget(range_label_text, row, 0)
|
||||
grid.addWidget(range_value, row, 1, 1, 2)
|
||||
row += 1
|
||||
|
||||
# Amount section
|
||||
amount_label = QLabel(_("Amount") + ":")
|
||||
amount_edit = BTCAmountEdit(self.window.get_decimal_point, max_amount=max_amount)
|
||||
amount_edit.setAmount(max_amount)
|
||||
grid.addWidget(amount_label, row, 0)
|
||||
grid.addWidget(amount_edit, row, 1)
|
||||
|
||||
if is_fixed_amount:
|
||||
# Fixed amount, just show the amount
|
||||
amount_edit.setDisabled(True)
|
||||
else:
|
||||
# Range, show max button
|
||||
max_button = EnterButton(_("Max"), lambda: amount_edit.setAmount(max_amount))
|
||||
btn_width = 10 * char_width_in_lineedit()
|
||||
max_button.setFixedWidth(btn_width)
|
||||
grid.addWidget(max_button, row, 2)
|
||||
|
||||
row += 1
|
||||
|
||||
# Warning for insufficient liquidity
|
||||
if lnurl_data.max_withdrawable_sat > int(self.wallet.lnworker.num_sats_can_receive()):
|
||||
warning_text = WWLabel(
|
||||
_("The maximum withdrawable amount is larger than what your channels can receive. "
|
||||
"You may need to do a submarine swap to increase your incoming liquidity.")
|
||||
)
|
||||
warning_text.setStyleSheet("color: orange;")
|
||||
grid.addWidget(warning_text, row, 0, 1, 4)
|
||||
row += 1
|
||||
|
||||
vbox.addLayout(grid)
|
||||
|
||||
# Buttons
|
||||
request_button = OkButton(dialog, _("Request Withdrawal"))
|
||||
cancel_button = CancelButton(dialog)
|
||||
vbox.addLayout(Buttons(cancel_button, request_button))
|
||||
|
||||
# Show dialog and handle result
|
||||
if dialog.exec():
|
||||
if is_fixed_amount:
|
||||
amount_sat = lnurl_data.max_withdrawable_sat
|
||||
else:
|
||||
amount_sat = amount_edit.get_amount()
|
||||
if not amount_sat or not (min_amount <= int(amount_sat) <= max_amount):
|
||||
self.show_error(_("Enter a valid amount. You entered: {}").format(amount_sat))
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
key = self.wallet.create_request(
|
||||
amount_sat=amount_sat,
|
||||
message=lnurl_data.default_description,
|
||||
exp_delay=120,
|
||||
address=None,
|
||||
)
|
||||
req = self.wallet.get_request(key)
|
||||
_lnaddr, b11_invoice = self.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=req.payment_hash,
|
||||
amount_msat=req.get_amount_msat(),
|
||||
message=req.get_message(),
|
||||
expiry=req.exp,
|
||||
fallback_address=None
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception('')
|
||||
self.show_error(
|
||||
f"{_('Failed to create payment request for withdrawal')}: {str(e)}"
|
||||
)
|
||||
return
|
||||
|
||||
coro = request_lnurl_withdraw_callback(
|
||||
callback_url=lnurl_data.callback_url,
|
||||
k1=lnurl_data.k1,
|
||||
bolt_11=b11_invoice
|
||||
)
|
||||
try:
|
||||
self.window.run_coroutine_dialog(coro, _("Requesting lightning withdrawal..."))
|
||||
except LNURLError as e:
|
||||
self.show_error(f"{_('Failed to request withdrawal')}:\n{str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user