Skip to content

Commit 7d0ac64

Browse files
committed
Merge remote-tracking branch 'spesmilo/pr/9993': lnurl-withdraw
ref #9993
2 parents 2b0cab6 + bcb7406 commit 7d0ac64

File tree

13 files changed

+942
-122
lines changed

13 files changed

+942
-122
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import QtQuick
2+
import QtQuick.Layouts
3+
import QtQuick.Controls
4+
import QtQuick.Controls.Material
5+
6+
import org.electrum 1.0
7+
8+
import "controls"
9+
10+
ElDialog {
11+
id: dialog
12+
13+
title: qsTr('LNURL Withdraw request')
14+
iconSource: '../../../icons/link.png'
15+
16+
property Wallet wallet: Daemon.currentWallet
17+
property RequestDetails requestDetails
18+
19+
padding: 0
20+
21+
property int walletCanReceive: 0
22+
property int providerMinWithdrawable: parseInt(requestDetails.lnurlData['min_withdrawable_sat'])
23+
property int providerMaxWithdrawable: parseInt(requestDetails.lnurlData['max_withdrawable_sat'])
24+
property int effectiveMinWithdrawable: Math.max(providerMinWithdrawable, 1)
25+
property int effectiveMaxWithdrawable: Math.min(providerMaxWithdrawable, walletCanReceive)
26+
property bool insufficientLiquidity: effectiveMinWithdrawable > walletCanReceive
27+
property bool liquidityWarning: providerMaxWithdrawable > walletCanReceive
28+
29+
property bool amountValid: !dialog.insufficientLiquidity &&
30+
amountBtc.textAsSats.satsInt >= dialog.effectiveMinWithdrawable &&
31+
amountBtc.textAsSats.satsInt <= dialog.effectiveMaxWithdrawable
32+
property bool valid: amountValid
33+
34+
Component.onCompleted: {
35+
dialog.walletCanReceive = wallet.lightningCanReceive.satsInt
36+
}
37+
38+
Connections {
39+
// assign walletCanReceive directly to prevent a binding loop
40+
target: wallet
41+
function onLightningCanReceiveChanged() {
42+
if (!requestDetails.busy) {
43+
// don't assign while busy to prevent the view from changing while receiving
44+
// the incoming payment
45+
dialog.walletCanReceive = wallet.lightningCanReceive.satsInt
46+
}
47+
}
48+
}
49+
50+
ColumnLayout {
51+
width: parent.width
52+
53+
GridLayout {
54+
id: rootLayout
55+
columns: 2
56+
57+
Layout.fillWidth: true
58+
Layout.leftMargin: constants.paddingLarge
59+
Layout.rightMargin: constants.paddingLarge
60+
Layout.bottomMargin: constants.paddingLarge
61+
62+
InfoTextArea {
63+
Layout.columnSpan: 2
64+
Layout.fillWidth: true
65+
compact: true
66+
visible: dialog.insufficientLiquidity
67+
text: qsTr('Too little incoming liquidity to satisfy this withdrawal request.')
68+
+ '\n\n'
69+
+ qsTr('Can receive: %1')
70+
.arg(Config.formatSats(dialog.walletCanReceive) + ' ' + Config.baseUnit)
71+
+ '\n'
72+
+ qsTr('Minimum withdrawal amount: %1')
73+
.arg(Config.formatSats(dialog.providerMinWithdrawable) + ' ' + Config.baseUnit)
74+
+ '\n\n'
75+
+ qsTr('Do a submarine swap in the \'Channels\' tab to get more incoming liquidity.')
76+
iconStyle: InfoTextArea.IconStyle.Error
77+
}
78+
79+
InfoTextArea {
80+
Layout.columnSpan: 2
81+
Layout.fillWidth: true
82+
compact: true
83+
visible: !dialog.insufficientLiquidity && dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable
84+
text: qsTr('Amount must be between %1 and %2 %3')
85+
.arg(Config.formatSats(dialog.effectiveMinWithdrawable))
86+
.arg(Config.formatSats(dialog.effectiveMaxWithdrawable))
87+
.arg(Config.baseUnit)
88+
}
89+
90+
InfoTextArea {
91+
Layout.columnSpan: 2
92+
Layout.fillWidth: true
93+
compact: true
94+
visible: dialog.liquidityWarning && !dialog.insufficientLiquidity
95+
text: qsTr('The maximum withdrawable amount (%1) is larger than what your channels can receive (%2).')
96+
.arg(Config.formatSats(dialog.providerMaxWithdrawable) + ' ' + Config.baseUnit)
97+
.arg(Config.formatSats(dialog.walletCanReceive) + ' ' + Config.baseUnit)
98+
+ ' '
99+
+ qsTr('You may need to do a submarine swap to increase your incoming liquidity.')
100+
iconStyle: InfoTextArea.IconStyle.Warn
101+
}
102+
103+
Label {
104+
text: qsTr('Provider')
105+
color: Material.accentColor
106+
}
107+
Label {
108+
Layout.fillWidth: true
109+
text: requestDetails.lnurlData['domain']
110+
}
111+
Label {
112+
text: qsTr('Description')
113+
color: Material.accentColor
114+
visible: requestDetails.lnurlData['default_description']
115+
}
116+
Label {
117+
Layout.fillWidth: true
118+
text: requestDetails.lnurlData['default_description']
119+
visible: requestDetails.lnurlData['default_description']
120+
wrapMode: Text.Wrap
121+
}
122+
123+
Label {
124+
text: qsTr('Amount')
125+
color: Material.accentColor
126+
}
127+
128+
RowLayout {
129+
Layout.fillWidth: true
130+
BtcField {
131+
id: amountBtc
132+
Layout.preferredWidth: rootLayout.width / 3
133+
text: Config.formatSatsForEditing(dialog.effectiveMaxWithdrawable)
134+
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
135+
color: Material.foreground // override gray-out on disabled
136+
fiatfield: amountFiat
137+
}
138+
Label {
139+
text: Config.baseUnit
140+
color: Material.accentColor
141+
}
142+
}
143+
144+
Item { visible: Daemon.fx.enabled; Layout.preferredWidth: 1; Layout.preferredHeight: 1 }
145+
146+
RowLayout {
147+
visible: Daemon.fx.enabled
148+
FiatField {
149+
id: amountFiat
150+
Layout.preferredWidth: rootLayout.width / 3
151+
btcfield: amountBtc
152+
enabled: !dialog.insufficientLiquidity && (dialog.providerMinWithdrawable != dialog.providerMaxWithdrawable)
153+
color: Material.foreground
154+
}
155+
Label {
156+
text: Daemon.fx.fiatCurrency
157+
color: Material.accentColor
158+
}
159+
}
160+
}
161+
162+
FlatButton {
163+
Layout.topMargin: constants.paddingLarge
164+
Layout.fillWidth: true
165+
text: qsTr('Withdraw...')
166+
icon.source: '../../icons/confirmed.png'
167+
enabled: valid && !requestDetails.busy
168+
onClicked: {
169+
var satsAmount = amountBtc.textAsSats.satsInt;
170+
requestDetails.lnurlRequestWithdrawal(satsAmount);
171+
dialog.close();
172+
}
173+
}
174+
}
175+
}

electrum/gui/qml/components/SendDialog.qml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ElDialog {
1212
id: dialog
1313

1414
property InvoiceParser invoiceParser
15+
property PIResolver piResolver
1516

1617
signal txFound(data: string)
1718
signal channelBackupFound(data: string)
@@ -36,7 +37,7 @@ ElDialog {
3637
} else if (Daemon.currentWallet.isValidChannelBackup(data)) {
3738
channelBackupFound(data)
3839
} else {
39-
invoiceParser.recipient = data
40+
piResolver.recipient = data
4041
}
4142
}
4243

@@ -57,8 +58,8 @@ ElDialog {
5758
Layout.fillHeight: true
5859

5960
hint: Daemon.currentWallet.isLightning
60-
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
61-
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
61+
? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')
62+
: qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')
6263

6364
onFoundText: (data) => {
6465
dialog.dispatch(data)
@@ -71,7 +72,7 @@ ElDialog {
7172
FlatButton {
7273
Layout.fillWidth: true
7374
Layout.preferredWidth: 1
74-
enabled: !invoiceParser.busy
75+
enabled: !invoiceParser.busy && !piResolver.busy
7576
icon.source: '../../icons/copy_bw.png'
7677
text: qsTr('Paste')
7778
onClicked: {

electrum/gui/qml/components/WalletMainView.qml

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ Item {
3636
function openSendDialog() {
3737
// Qt based send dialog if not on android
3838
if (!AppController.isAndroid()) {
39-
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser})
39+
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser, piResolver: piResolver})
4040
_sendDialog.open()
4141
return
4242
}
4343

4444
// Android based send dialog if on android
4545
var scanner = app.scanDialog.createObject(mainView, {
4646
hint: Daemon.currentWallet.isLightning
47-
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
48-
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
47+
? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')
48+
: qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')
4949
})
5050
scanner.onFoundText.connect(function(data) {
5151
data = data.trim()
@@ -61,7 +61,7 @@ Item {
6161
})
6262
dialog.open()
6363
} else {
64-
invoiceParser.recipient = data
64+
piResolver.recipient = data
6565
}
6666
//scanner.destroy() // TODO
6767
})
@@ -362,7 +362,7 @@ Item {
362362
Layout.preferredWidth: 1
363363
icon.source: '../../icons/tab_send.png'
364364
text: qsTr('Send')
365-
enabled: !invoiceParser.busy
365+
enabled: !invoiceParser.busy && !piResolver.busy && !requestDetails.busy
366366
onClicked: openSendDialog()
367367
onPressAndHold: {
368368
Config.userKnowsPressAndHold = true
@@ -373,6 +373,48 @@ Item {
373373
}
374374
}
375375

376+
PIResolver {
377+
id: piResolver
378+
wallet: Daemon.currentWallet
379+
380+
onResolveError: (code, message) => {
381+
var dialog = app.messageDialog.createObject(app, {
382+
title: qsTr('Error'),
383+
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
384+
text: message
385+
})
386+
dialog.open()
387+
}
388+
389+
onInvoiceResolved: (pi) => {
390+
invoiceParser.fromResolvedPaymentIdentifier(pi)
391+
}
392+
393+
onRequestResolved: (pi) => {
394+
requestDetails.fromResolvedPaymentIdentifier(pi)
395+
}
396+
}
397+
398+
RequestDetails {
399+
id: requestDetails
400+
wallet: Daemon.currentWallet
401+
onNeedsLNURLUserInput: {
402+
closeSendDialog()
403+
var dialog = lnurlWithdrawDialog.createObject(app, {
404+
requestDetails: requestDetails
405+
})
406+
dialog.open()
407+
}
408+
onLnurlError: (code, message) => {
409+
var dialog = app.messageDialog.createObject(app, {
410+
title: qsTr('Error'),
411+
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
412+
text: message
413+
})
414+
dialog.open()
415+
}
416+
}
417+
376418
Invoice {
377419
id: invoice
378420
wallet: Daemon.currentWallet
@@ -420,12 +462,16 @@ Item {
420462
})
421463
dialog.open()
422464
}
423-
424465
onLnurlRetrieved: {
425466
closeSendDialog()
426-
var dialog = lnurlPayDialog.createObject(app, {
427-
invoiceParser: invoiceParser
428-
})
467+
if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {
468+
var dialog = lnurlPayDialog.createObject(app, {
469+
invoiceParser: invoiceParser
470+
})
471+
} else {
472+
console.log("Unsupported LNURL type:", invoiceParser.invoiceType)
473+
return
474+
}
429475
dialog.open()
430476
}
431477
onLnurlError: (code, message) => {
@@ -451,7 +497,7 @@ Item {
451497
_intentUri = uri
452498
return
453499
}
454-
invoiceParser.recipient = uri
500+
piResolver.recipient = uri
455501
}
456502
}
457503

@@ -460,7 +506,7 @@ Item {
460506
function onWalletLoaded() {
461507
infobanner.hide() // start hidden when switching wallets
462508
if (_intentUri) {
463-
invoiceParser.recipient = _intentUri
509+
piResolver.recipient = _intentUri
464510
_intentUri = ''
465511
}
466512
}
@@ -739,6 +785,16 @@ Item {
739785
}
740786
}
741787

788+
Component {
789+
id: lnurlWithdrawDialog
790+
LnurlWithdrawRequestDialog {
791+
width: parent.width * 0.9
792+
anchors.centerIn: parent
793+
794+
onClosed: destroy()
795+
}
796+
}
797+
742798
Component {
743799
id: otpDialog
744800
OtpDialog {

electrum/gui/qml/qeapp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .qefx import QEFX
3535
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
3636
from .qeinvoice import QEInvoice, QEInvoiceParser
37+
from .qepiresolver import QEPIResolver
3738
from .qerequestdetails import QERequestDetails
3839
from .qetypes import QEAmount, QEBytes
3940
from .qeaddressdetails import QEAddressDetails
@@ -489,6 +490,7 @@ def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: '
489490
qmlRegisterType(QEQRScanner, 'org.electrum', 1, 0, 'QRScanner')
490491
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX')
491492
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
493+
qmlRegisterType(QEPIResolver, 'org.electrum', 1, 0, 'PIResolver')
492494
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
493495
qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser')
494496
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')

0 commit comments

Comments
 (0)