From fa8713de17096743e51631476f5769aca23d64cf Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Sun, 20 Sep 2020 23:50:02 -0300 Subject: [PATCH 01/10] move scan to outside of receive. --- lnbits/core/static/js/wallet.js | 36 +++++++++++++++++++++----- lnbits/core/templates/core/wallet.html | 35 +++++++++++++++++++------ 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 8818196..f3dd8b6 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -136,7 +136,7 @@ new Vue({ bolt11: '' } }, - sendCamera: { + theCamera: { show: false, camera: 'auto' }, @@ -206,11 +206,17 @@ new Vue({ } }, methods: { + // closeCamera: function () { + // this.sendCamera.show = false + // }, + // showCamera: function () { + // this.sendCamera.show = true + // }, closeCamera: function () { - this.sendCamera.show = false + this.theCamera.show = false }, showCamera: function () { - this.sendCamera.show = true + this.theCamera.show = true }, showChart: function () { this.paymentsChart.show = true @@ -247,7 +253,7 @@ new Vue({ }, 10000) }, closeSendDialog: function () { - this.sendCamera.show = false + // this.sendCamera.show = false var checker = this.send.paymentChecker setTimeout(function () { clearInterval(checker) @@ -284,10 +290,26 @@ new Vue({ }) }, decodeQR: function (res) { - this.send.data.bolt11 = res - this.decodeInvoice() - this.sendCamera.show = false + if (res.substring(0, 4) == 'lnurl') { + console.log(res) + var self = this + + LNbits.api + .request('GET', '/lnurlscan/' + res, this.g.user.wallets[0].adminkey) + .then(function (response) { + console.log(response.data) + }) + .catch(function (error) { + clearInterval(self.checker) + LNbits.utils.notifyApiError(error) + }) + } else { + this.send.data.bolt11 = res + this.decodeInvoice() + this.theCamera.show = false + } }, + decodeInvoice: function () { if (this.send.data.bolt11.startsWith('lightning:')) { this.send.data.bolt11 = this.send.data.bolt11.slice(10) diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 5d62046..5bf94da 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -14,7 +14,7 @@
- Memo: {{ send.invoice.description }}
+ Description: {{ send.invoice.description }}
Expire date: {{ send.invoice.expireDate }}
Hash: {{ send.invoice.hash }}
- This whole wallet will be deleted, the funds will be - UNRECOVERABLE. -
-+ This whole wallet will be deleted, the funds will be + UNRECOVERABLE. +
+
+ Description: {{ send.invoice.description }}
+ Payment hash: {{ send.invoice.hash }}
+ Expire date: {{ send.invoice.expireDate }}
+
", methods=["GET"])
+@api_check_wallet_key("invoice")
+async def api_lnurlscan(code: str):
+ try:
+ url = lnurl.Lnurl(code)
+ except ValueError:
+ return jsonify({"error": "invalid lnurl"}), HTTPStatus.BAD_REQUEST
+
+ domain = urlparse(url.url).netloc
+ if url.is_login:
+ return jsonify({"domain": domain, "kind": "auth", "error": "unsupported"})
+
+ data: lnurl.LnurlResponseModel = lnurl.get(url.url)
+ if not data.ok:
+ return jsonify({"domain": domain, "error": "failed to get parameters"})
+
+ if type(data) is lnurl.LnurlChannelResponse:
+ return jsonify({"domain": domain, "kind": "channel", "error": "unsupported"})
+
+ params = data.dict()
+ if type(data) is lnurl.LnurlWithdrawResponse:
+ params.update(kind="withdraw", fixed=data.min_withdrawable == data.max_withdrawable)
+
+ if type(data) is lnurl.LnurlPayResponse:
+ params.update(kind="pay", fixed=data.min_sendable == data.max_sendable)
+
+ params.update(domain=domain)
+ return jsonify(params)
+>>>>>>> da8fd9a... send/create buttons wip.
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/display.html b/lnbits/extensions/lnurlp/templates/lnurlp/display.html
index fd9b3de..a2e0389 100644
--- a/lnbits/extensions/lnurlp/templates/lnurlp/display.html
+++ b/lnbits/extensions/lnurlp/templates/lnurlp/display.html
@@ -26,9 +26,7 @@
LNbits LNURL-pay link
-
- Use an LNURL compatible bitcoin wallet to pay.
-
+ Use an LNURL compatible bitcoin wallet to pay.
From 3cd15c40fc7d4755ff12df5cfdb2ace7c1061602 Mon Sep 17 00:00:00 2001
From: fiatjaf
Date: Sun, 11 Oct 2020 22:19:27 -0300
Subject: [PATCH 03/10] lnurl-pay and lnurl-withdraw UI.
---
lnbits/core/static/js/wallet.js | 258 +++++++++++++++----------
lnbits/core/templates/core/wallet.html | 145 ++++++++------
lnbits/core/views/api.py | 29 +--
3 files changed, 259 insertions(+), 173 deletions(-)
diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js
index 3a5eec4..b39a25d 100644
--- a/lnbits/core/static/js/wallet.js
+++ b/lnbits/core/static/js/wallet.js
@@ -14,12 +14,8 @@ function generateChart(canvas, payments) {
}
_.each(
- payments
- .filter(p => !p.pending)
- .sort(function (a, b) {
- return a.time - b.time
- }),
- function (tx) {
+ payments.filter(p => !p.pending).sort((a, b) => a.time - b.time),
+ tx => {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat
@@ -27,19 +23,15 @@ function generateChart(canvas, payments) {
}
)
- _.each(_.groupBy(txs, 'hour'), function (value, day) {
+ _.each(_.groupBy(txs, 'hour'), (value, day) => {
var income = _.reduce(
value,
- function (memo, tx) {
- return tx.sat >= 0 ? memo + tx.sat : memo
- },
+ (memo, tx) => (tx.sat >= 0 ? memo + tx.sat : memo),
0
)
var outcome = _.reduce(
value,
- function (memo, tx) {
- return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
- },
+ (memo, tx) => (tx.sat < 0 ? memo + Math.abs(tx.sat) : memo),
0
)
n = n + income - outcome
@@ -124,23 +116,27 @@ new Vue({
show: false,
status: 'pending',
paymentReq: null,
+ minMax: [0, 2100000000000000],
+ lnurl: null,
data: {
amount: null,
memo: ''
}
},
- send: {
+ parse: {
show: false,
invoice: null,
- lnurl: {},
+ lnurlpay: null,
data: {
- request: ''
+ request: '',
+ amount: 0
+ },
+ paymentChecker: null,
+ camera: {
+ show: false,
+ camera: 'auto'
}
},
- theCamera: {
- show: false,
- camera: 'auto'
- },
payments: [],
paymentsTable: {
columns: [
@@ -197,8 +193,8 @@ new Vue({
return LNbits.utils.search(this.payments, q)
},
canPay: function () {
- if (!this.send.invoice) return false
- return this.send.invoice.sat <= this.balance
+ if (!this.parse.invoice) return false
+ return this.parse.invoice.sat <= this.balance
},
pendingPaymentsExist: function () {
return this.payments
@@ -206,56 +202,55 @@ new Vue({
: false
}
},
+ filters: {
+ msatoshiFormat: function (value) {
+ return LNbits.utils.formatSat(value / 1000)
+ }
+ },
methods: {
closeCamera: function () {
- this.theCamera.show = false
+ this.parse.camera.show = false
},
showCamera: function () {
- this.theCamera.show = true
+ this.parse.camera.show = true
},
showChart: function () {
this.paymentsChart.show = true
- this.$nextTick(function () {
+ this.$nextTick(() => {
generateChart(this.$refs.canvas, this.payments)
})
},
showReceiveDialog: function () {
- this.receive = {
- show: true,
- status: 'pending',
- paymentReq: null,
- data: {
- amount: null,
- memo: ''
- },
- paymentChecker: null
- }
+ this.receive.show = true
+ this.receive.status = 'pending'
+ this.receive.paymentReq = null
+ this.receive.data.amount = null
+ this.receive.data.memo = null
+ this.receive.paymentChecker = null
+ this.receive.minMax = [0, 2100000000000000]
+ this.receive.lnurl = null
},
- showSendDialog: function () {
- this.send = {
- show: true,
- invoice: null,
- lnurl: {},
- data: {
- request: ''
- },
- paymentChecker: null
- }
+ showParseDialog: function () {
+ this.parse.show = true
+ this.parse.invoice = null
+ this.parse.lnurlpay = null
+ this.parse.data.request = ''
+ this.parse.data.paymentChecker = null
+ this.parse.camera.show = false
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
- setTimeout(function () {
+ setTimeout(() => {
clearInterval(checker)
}, 10000)
},
- closeSendDialog: function () {
- var checker = this.send.paymentChecker
- setTimeout(function () {
+ closeParseDialog: function () {
+ var checker = this.parse.paymentChecker
+ setTimeout(() => {
clearInterval(checker)
}, 1000)
},
createInvoice: function () {
- var self = this
this.receive.status = 'loading'
LNbits.api
.createInvoice(
@@ -263,59 +258,96 @@ new Vue({
this.receive.data.amount,
this.receive.data.memo
)
- .then(function (response) {
- self.receive.status = 'success'
- self.receive.paymentReq = response.data.payment_request
+ .then(response => {
+ this.receive.status = 'success'
+ this.receive.paymentReq = response.data.payment_request
+
+ if (this.receive.lnurl) {
+ // send invoice to lnurl callback
+ console.log('sending', this.receive.lnurl)
+ LNbits.api.sendInvoiceToLnurlWithdraw(this.receive.paymentReq)
+ }
- self.receive.paymentChecker = setInterval(function () {
+ this.receive.paymentChecker = setInterval(() => {
LNbits.api
- .getPayment(self.g.wallet, response.data.payment_hash)
- .then(function (response) {
+ .getPayment(this.g.wallet, response.data.payment_hash)
+ .then(response => {
if (response.data.paid) {
- self.fetchPayments()
- self.receive.show = false
- clearInterval(self.receive.paymentChecker)
+ this.fetchPayments()
+ this.receive.show = false
+ clearInterval(this.receive.paymentChecker)
}
})
}, 2000)
})
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- self.receive.status = 'pending'
+ .catch(err => {
+ LNbits.utils.notifyApiError(err)
+ this.receive.status = 'pending'
})
},
decodeQR: function (res) {
- this.send.data.request = res
+ this.parse.data.request = res
this.decodeRequest()
- this.sendCamera.show = false
+ this.parse.camera.show = false
},
decodeRequest: function () {
- if (this.send.data.request.startsWith('lightning:')) {
- this.send.data.request = this.send.data.request.slice(10)
+ this.parse.show = true
+
+ if (this.parse.data.request.startsWith('lightning:')) {
+ this.parse.data.request = this.parse.data.request.slice(10)
}
- if (this.send.data.request.startsWith('lnurl:')) {
- this.send.data.request = this.send.data.request.slice(6)
+ if (this.parse.data.request.startsWith('lnurl:')) {
+ this.parse.data.request = this.parse.data.request.slice(6)
}
- if (this.send.data.request.toLowerCase().startsWith('lnurl1')) {
+ if (this.parse.data.request.toLowerCase().startsWith('lnurl1')) {
LNbits.api
.request(
'GET',
- '/api/v1/lnurlscan/' + this.send.data.request,
+ '/api/v1/lnurlscan/' + this.parse.data.request,
this.g.user.wallets[0].adminkey
)
- .then(function (response) {
- this.send.lnurl[response.kind] = Object.freeze(response)
+ .catch(err => {
+ LNbits.utils.notifyApiError(err)
})
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
+ .then(response => {
+ let data = response.data
+
+ if (data.status === 'ERROR') {
+ Quasar.plugins.Notify.create({
+ timeout: 5000,
+ type: 'warning',
+ message: data.reason,
+ caption: `${data.domain} returned an error to the lnurl call.`,
+ icon: null
+ })
+ return
+ }
+
+ if (data.kind === 'pay') {
+ this.parse.lnurlpay = Object.freeze(data)
+ this.parse.data.amount = data.minSendable / 1000
+ } else if (data.kind === 'withdraw') {
+ this.parse.show = false
+ this.receive.show = true
+ this.receive.status = 'pending'
+ this.receive.data.amount = data.maxWithdrawable
+ this.receive.data.memo = data.defaultDescription
+ this.receive.minMax = [data.minWithdrawable, data.maxWithdrawable]
+ this.receive.lnurl = {
+ domain: data.domain,
+ callback: data.callback,
+ k1: data.k1,
+ fixed: data.fixed
+ }
+ }
})
return
}
let invoice
try {
- invoice = decode(this.send.data.bolt11)
+ invoice = decode(this.parse.data.bolt11)
} catch (error) {
this.$q.notify({
timeout: 3000,
@@ -324,6 +356,7 @@ new Vue({
caption: '400 BAD REQUEST',
icon: null
})
+ this.parse.show = false
return
}
@@ -333,7 +366,7 @@ new Vue({
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
}
- _.each(invoice.data.tags, function (tag) {
+ _.each(invoice.data.tags, tag => {
if (_.isObject(tag) && _.has(tag, 'description')) {
if (tag.description === 'payment_hash') {
cleanInvoice.hash = tag.value
@@ -352,11 +385,37 @@ new Vue({
}
})
- this.send.invoice = Object.freeze(cleanInvoice)
+ this.parse.invoice = Object.freeze(cleanInvoice)
},
payInvoice: function () {
- var self = this
+ let dismissPaymentMsg = this.$q.notify({
+ timeout: 0,
+ message: 'Processing payment...',
+ icon: null
+ })
+ LNbits.api
+ .payInvoice(this.g.wallet, this.parse.data.bolt11)
+ .then(response => {
+ this.parse.paymentChecker = setInterval(() => {
+ LNbits.api
+ .getPayment(this.g.wallet, response.data.payment_hash)
+ .then(res => {
+ if (res.data.paid) {
+ this.parse.show = false
+ clearInterval(this.parse.paymentChecker)
+ dismissPaymentMsg()
+ this.fetchPayments()
+ }
+ })
+ }, 2000)
+ })
+ .catch(err => {
+ dismissPaymentMsg()
+ LNbits.utils.notifyApiError(err)
+ })
+ },
+ payLnurl: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...',
@@ -364,55 +423,52 @@ new Vue({
})
LNbits.api
- .payInvoice(this.g.wallet, this.send.data.bolt11)
- .then(function (response) {
- self.send.paymentChecker = setInterval(function () {
+ .payInvoice(this.g.wallet, this.parse.data.bolt11)
+ .then(response => {
+ this.parse.paymentChecker = setInterval(() => {
LNbits.api
- .getPayment(self.g.wallet, response.data.payment_hash)
- .then(function (res) {
+ .getPayment(this.g.wallet, response.data.payment_hash)
+ .then(res => {
if (res.data.paid) {
- self.send.show = false
- clearInterval(self.send.paymentChecker)
+ this.parse.show = false
+ clearInterval(this.parse.paymentChecker)
dismissPaymentMsg()
- self.fetchPayments()
+ this.fetchPayments()
}
})
}, 2000)
})
- .catch(function (error) {
+ .catch(err => {
dismissPaymentMsg()
- LNbits.utils.notifyApiError(error)
+ LNbits.utils.notifyApiError(err)
})
},
deleteWallet: function (walletId, user) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
- .onOk(function () {
+ .onOk(() => {
LNbits.href.deleteWallet(walletId, user)
})
},
fetchPayments: function (checkPending) {
- var self = this
-
return LNbits.api
.getPayments(this.g.wallet, checkPending)
- .then(function (response) {
- self.payments = response.data
- .map(function (obj) {
+ .then(response => {
+ this.payments = response.data
+ .map(obj => {
return LNbits.map.payment(obj)
})
- .sort(function (a, b) {
+ .sort((a, b) => {
return b.time - a.time
})
})
},
fetchBalance: function () {
- var self = this
- LNbits.api.getWallet(self.g.wallet).then(function (response) {
- self.balance = Math.round(response.data.balance / 1000)
+ LNbits.api.getWallet(this.g.wallet).then(response => {
+ this.balance = Math.round(response.data.balance / 1000)
EventHub.$emit('update-wallet-balance', [
- self.g.wallet.id,
- self.balance
+ this.g.wallet.id,
+ this.balance
])
})
},
@@ -423,7 +479,7 @@ new Vue({
icon: null
})
- this.fetchPayments(true).then(function () {
+ this.fetchPayments(true).then(() => {
dismissMsg()
})
},
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index bf94e96..405f970 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -16,7 +16,7 @@
unelevated
color="deep-purple"
class="full-width"
- @click="showSendDialog"
+ @click="showParseDialog"
>Paste Request
+ {{receive.lnurl.domain}} is requesting an invoice: +
+
+ Description: {{ parse.invoice.description }}
+ Expire date: {{ parse.invoice.expireDate }}
+ Hash: {{ parse.invoice.hash }}
+
+ {{ parse.lnurlpay.maxSendable | msatoshiFormat }} +
+
+ {{ parse.lnurlpay.domain }} is requesting
+ between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and
+ {{ parse.lnurlpay.maxSendable | msatoshiFormat }} sat
+
{{ parse.lnurlpay.description }}
+
+
- Description: {{ send.invoice.description }}
- Payment hash: {{ send.invoice.hash }}
- Expire date: {{ send.invoice.expireDate }}
-
- Description: {{ send.invoice.description }}
- Expire date: {{ send.invoice.expireDate }}
- Hash: {{ send.invoice.hash }}
-
", methods=["GET"])
@@ -206,20 +201,30 @@ async def api_lnurlscan(code: str):
if url.is_login:
return jsonify({"domain": domain, "kind": "auth", "error": "unsupported"})
- data: lnurl.LnurlResponseModel = lnurl.get(url.url)
- if not data.ok:
+ r = httpx.get(url.url)
+ if r.is_error:
return jsonify({"domain": domain, "error": "failed to get parameters"})
+ try:
+ jdata = json.loads(r.text)
+ data: lnurl.LnurlResponseModel = lnurl.LnurlResponse.from_dict(jdata)
+ except (json.decoder.JSONDecodeError, lnurl.exceptions.LnurlResponseException):
+ return jsonify({"domain": domain, "error": f"got invalid response '{r.text[:200]}'"})
+
if type(data) is lnurl.LnurlChannelResponse:
return jsonify({"domain": domain, "kind": "channel", "error": "unsupported"})
- params = data.dict()
+ params: Dict = data.dict()
if type(data) is lnurl.LnurlWithdrawResponse:
params.update(kind="withdraw", fixed=data.min_withdrawable == data.max_withdrawable)
if type(data) is lnurl.LnurlPayResponse:
params.update(kind="pay", fixed=data.min_sendable == data.max_sendable)
+ params.update(description=data.metadata.text)
+ if data.metadata.images:
+ image = min(data.metadata.images, key=lambda image: len(image[1]))
+ data_uri = "data:" + image[0] + "," + image[1]
+ params.update(image=data_uri)
params.update(domain=domain)
return jsonify(params)
->>>>>>> da8fd9a... send/create buttons wip.
From bc2207ba278ef6597b9361965e74847b69def1a7 Mon Sep 17 00:00:00 2001
From: fiatjaf
Date: Mon, 12 Oct 2020 18:15:27 -0300
Subject: [PATCH 04/10] actually paying and withdrawing with lnurl.
---
lnbits/core/services.py | 9 ++-
lnbits/core/static/js/wallet.js | 100 +++++++++++++++++------
lnbits/core/templates/core/wallet.html | 9 ++-
lnbits/core/views/api.py | 105 ++++++++++++++++++++++++-
lnbits/static/js/base.js | 19 ++++-
5 files changed, 207 insertions(+), 35 deletions(-)
diff --git a/lnbits/core/services.py b/lnbits/core/services.py
index 8bdb73a..d4d4d8c 100644
--- a/lnbits/core/services.py
+++ b/lnbits/core/services.py
@@ -51,7 +51,12 @@ def create_invoice(
def pay_invoice(
- *, wallet_id: str, payment_request: str, max_sat: Optional[int] = None, extra: Optional[Dict] = None
+ *,
+ wallet_id: str,
+ payment_request: str,
+ max_sat: Optional[int] = None,
+ extra: Optional[Dict] = None,
+ description: str = "",
) -> str:
temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}"
@@ -79,7 +84,7 @@ def pay_invoice(
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=-invoice.amount_msat,
- memo=invoice.description or "",
+ memo=description or invoice.description or "",
extra=extra,
)
diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js
index b39a25d..72f54d4 100644
--- a/lnbits/core/static/js/wallet.js
+++ b/lnbits/core/static/js/wallet.js
@@ -256,16 +256,36 @@ new Vue({
.createInvoice(
this.g.wallet,
this.receive.data.amount,
- this.receive.data.memo
+ this.receive.data.memo,
+ this.receive.lnurl && this.receive.lnurl.callback
)
.then(response => {
this.receive.status = 'success'
this.receive.paymentReq = response.data.payment_request
- if (this.receive.lnurl) {
- // send invoice to lnurl callback
- console.log('sending', this.receive.lnurl)
- LNbits.api.sendInvoiceToLnurlWithdraw(this.receive.paymentReq)
+ if (response.data.lnurl_response !== null) {
+ if (response.data.lnurl_response === false) {
+ response.data.lnurl_response = `Unable to connect`
+ }
+
+ if (typeof response.data.lnurl_response === 'string') {
+ // failure
+ this.$q.notify({
+ timeout: 5000,
+ type: 'negative',
+ message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
+ caption: response.data.lnurl_response
+ })
+ return
+ } else if (response.data.lnurl_response === true) {
+ // success
+ this.$q.notify({
+ timeout: 5000,
+ type: 'positive',
+ message: `Invoice sent to ${this.receive.lnurl.domain}!`,
+ spinner: true
+ })
+ }
}
this.receive.paymentChecker = setInterval(() => {
@@ -274,6 +294,7 @@ new Vue({
.then(response => {
if (response.data.paid) {
this.fetchPayments()
+ this.fetchBalance()
this.receive.show = false
clearInterval(this.receive.paymentChecker)
}
@@ -314,12 +335,11 @@ new Vue({
let data = response.data
if (data.status === 'ERROR') {
- Quasar.plugins.Notify.create({
+ this.$q.notify({
timeout: 5000,
type: 'warning',
- message: data.reason,
- caption: `${data.domain} returned an error to the lnurl call.`,
- icon: null
+ message: `${data.domain} lnurl call failed.`,
+ caption: data.reason
})
return
}
@@ -331,13 +351,16 @@ new Vue({
this.parse.show = false
this.receive.show = true
this.receive.status = 'pending'
- this.receive.data.amount = data.maxWithdrawable
+ this.paymentReq = null
+ this.receive.data.amount = data.maxWithdrawable / 1000
this.receive.data.memo = data.defaultDescription
- this.receive.minMax = [data.minWithdrawable, data.maxWithdrawable]
+ this.receive.minMax = [
+ data.minWithdrawable / 1000,
+ data.maxWithdrawable / 1000
+ ]
this.receive.lnurl = {
domain: data.domain,
callback: data.callback,
- k1: data.k1,
fixed: data.fixed
}
}
@@ -353,8 +376,7 @@ new Vue({
timeout: 3000,
type: 'warning',
message: error + '.',
- caption: '400 BAD REQUEST',
- icon: null
+ caption: '400 BAD REQUEST'
})
this.parse.show = false
return
@@ -390,8 +412,7 @@ new Vue({
payInvoice: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
- message: 'Processing payment...',
- icon: null
+ message: 'Processing payment...'
})
LNbits.api
@@ -406,6 +427,7 @@ new Vue({
clearInterval(this.parse.paymentChecker)
dismissPaymentMsg()
this.fetchPayments()
+ this.fetchBalance()
}
})
}, 2000)
@@ -418,22 +440,55 @@ new Vue({
payLnurl: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
- message: 'Processing payment...',
- icon: null
+ message: 'Processing payment...'
})
LNbits.api
- .payInvoice(this.g.wallet, this.parse.data.bolt11)
+ .payLnurl(
+ this.g.wallet,
+ this.parse.lnurlpay.callback,
+ this.parse.lnurlpay.description_hash,
+ this.parse.data.amount * 1000,
+ this.parse.lnurlpay.description.slice(0, 120)
+ )
.then(response => {
+ this.parse.show = false
+
this.parse.paymentChecker = setInterval(() => {
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(res => {
if (res.data.paid) {
- this.parse.show = false
- clearInterval(this.parse.paymentChecker)
dismissPaymentMsg()
+ clearInterval(this.parse.paymentChecker)
this.fetchPayments()
+ this.fetchBalance()
+
+ // show lnurlpay success action
+ if (response.data.success_action) {
+ switch (response.data.success_action.tag) {
+ case 'url':
+ this.$q.notify({
+ message: `${response.data.success_action.url}`,
+ caption: response.data.success_action.description,
+ html: true,
+ type: 'info',
+ timeout: 0,
+ closeBtn: true
+ })
+ break
+ case 'message':
+ this.$q.notify({
+ message: response.data.success_action.message,
+ type: 'info',
+ timeout: 0,
+ closeBtn: true
+ })
+ break
+ case 'aes':
+ break
+ }
+ }
}
})
}, 2000)
@@ -475,8 +530,7 @@ new Vue({
checkPendingPayments: function () {
var dismissMsg = this.$q.notify({
timeout: 0,
- message: 'Checking pending transactions...',
- icon: null
+ message: 'Checking pending transactions...'
})
this.fetchPayments(true).then(() => {
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index 405f970..4edf134 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -130,7 +130,7 @@
{{ props.row.fsat }}
-
+
{{ props.row.fee }}
@@ -266,8 +266,8 @@
v-model.number="receive.data.amount"
type="number"
label="Amount (sat) *"
- min="receive.minMax[0]"
- max="receive.minMax[1]"
+ :min="receive.minMax[0]"
+ :max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
>
- {{ parse.lnurlpay.maxSendable | msatoshiFormat }}
+ {{ parse.lnurlpay.domain }} is requesting
+ {{ parse.lnurlpay.maxSendable | msatoshiFormat }} sat
{{ parse.lnurlpay.domain }} is requesting
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 5be279e..7ff1e88 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -3,11 +3,11 @@ import json
import lnurl
import httpx
import traceback
+from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
from quart import g, jsonify, request, make_response
from http import HTTPStatus
from binascii import unhexlify
-from urllib.parse import urlparse
-from typing import Dict
+from typing import Dict, Union
from lnbits import bolt11
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
@@ -51,6 +51,7 @@ async def api_payments():
"amount": {"type": "integer", "min": 1, "required": True},
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
+ "lnurl_callback": {"type": "string", "empty": False, "required": False},
}
)
async def api_payments_create_invoice():
@@ -70,6 +71,23 @@ async def api_payments_create_invoice():
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
invoice = bolt11.decode(payment_request)
+
+ lnurl_response: Union[None, bool, str] = None
+ if "lnurl_callback" in g.data:
+ print(g.data["lnurl_callback"])
+ try:
+ r = httpx.get(g.data["lnurl_callback"], params={"pr": payment_request}, timeout=10)
+ if r.is_error:
+ lnurl_response = r.text
+ else:
+ resp = json.loads(r.text)
+ if resp["status"] != "OK":
+ lnurl_response = resp["reason"]
+ else:
+ lnurl_response = True
+ except httpx.RequestError:
+ lnurl_response = False
+
return (
jsonify(
{
@@ -77,6 +95,7 @@ async def api_payments_create_invoice():
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
+ "lnurl_response": lnurl_response,
}
),
HTTPStatus.CREATED,
@@ -117,6 +136,74 @@ async def api_payments_create():
return await api_payments_create_invoice()
+@core_app.route("/api/v1/payments/lnurl", methods=["POST"])
+@api_check_wallet_key("admin")
+@api_validate_post_request(
+ schema={
+ "description_hash": {"type": "string", "empty": False, "required": True},
+ "callback": {"type": "string", "empty": False, "required": True},
+ "amount": {"type": "number", "empty": False, "required": True},
+ "description": {"type": "string", "empty": True, "required": False},
+ }
+)
+async def api_payments_pay_lnurl():
+ try:
+ r = httpx.get(g.data["callback"], params={"amount": g.data["amount"]}, timeout=20)
+ if r.is_error:
+ return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
+ except httpx.RequestError:
+ return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
+
+ params = json.loads(r.text)
+ if params.get("status") == "ERROR":
+ domain = urlparse(g.data["callback"]).netloc
+ return jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}), HTTPStatus.BAD_REQUEST
+
+ invoice = bolt11.decode(params["pr"])
+ if invoice.amount_msat != g.data["amount"]:
+ return (
+ jsonify(
+ {
+ "message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
+ }
+ ),
+ HTTPStatus.BAD_REQUEST,
+ )
+ if invoice.description_hash != g.data["description_hash"]:
+ return (
+ jsonify(
+ {
+ "message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
+ }
+ ),
+ HTTPStatus.BAD_REQUEST,
+ )
+
+ try:
+ payment_hash = pay_invoice(
+ wallet_id=g.wallet.id,
+ payment_request=params["pr"],
+ description=g.data.get("description", ""),
+ extra={"success_action": params.get("successAction")},
+ )
+ except Exception as exc:
+ traceback.print_exc(7)
+ g.db.rollback()
+ return jsonify({"message": str(exc)}), HTTPStatus.INTERNAL_SERVER_ERROR
+
+ return (
+ jsonify(
+ {
+ "success_action": params.get("successAction"),
+ "payment_hash": payment_hash,
+ # maintain backwards compatibility with API clients:
+ "checking_id": payment_hash,
+ }
+ ),
+ HTTPStatus.CREATED,
+ )
+
+
@core_app.route("/api/v1/payments/", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_payment(payment_hash):
@@ -216,10 +303,20 @@ async def api_lnurlscan(code: str):
params: Dict = data.dict()
if type(data) is lnurl.LnurlWithdrawResponse:
- params.update(kind="withdraw", fixed=data.min_withdrawable == data.max_withdrawable)
+ params.update(kind="withdraw")
+ params.update(fixed=data.min_withdrawable == data.max_withdrawable)
+
+ # callback with k1 already in it
+ url: ParseResult = urlparse(data.callback)
+ qs: Dict = parse_qs(url.query)
+ qs["k1"] = data.k1
+ url = url._replace(query=urlencode(qs, doseq=True))
+ params.update(callback=urlunparse(url))
if type(data) is lnurl.LnurlPayResponse:
- params.update(kind="pay", fixed=data.min_sendable == data.max_sendable)
+ params.update(kind="pay")
+ params.update(fixed=data.min_sendable == data.max_sendable)
+ params.update(description_hash=data.metadata.h)
params.update(description=data.metadata.text)
if data.metadata.images:
image = min(data.metadata.images, key=lambda image: len(image[1]))
diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js
index abaec60..9f1bb28 100644
--- a/lnbits/static/js/base.js
+++ b/lnbits/static/js/base.js
@@ -16,11 +16,12 @@ var LNbits = {
data: data
})
},
- createInvoice: function (wallet, amount, memo) {
+ createInvoice: function (wallet, amount, memo, lnurlCallback = null) {
return this.request('post', '/api/v1/payments', wallet.inkey, {
out: false,
amount: amount,
- memo: memo
+ memo: memo,
+ lnurl_callback: lnurlCallback
})
},
payInvoice: function (wallet, bolt11) {
@@ -29,6 +30,20 @@ var LNbits = {
bolt11: bolt11
})
},
+ payLnurl: function (
+ wallet,
+ callback,
+ description_hash,
+ amount,
+ description = ''
+ ) {
+ return this.request('post', '/api/v1/payments/lnurl', wallet.adminkey, {
+ callback,
+ description_hash,
+ amount,
+ description
+ })
+ },
getWallet: function (wallet) {
return this.request('get', '/api/v1/wallet', wallet.inkey)
},
From 69063190ab89f2fa790745456bd342b5d907f638 Mon Sep 17 00:00:00 2001
From: fiatjaf
Date: Mon, 12 Oct 2020 22:25:55 -0300
Subject: [PATCH 05/10] also catch httpx.ConnectError whenever we do catch
httpx.RequestError.
---
lnbits/core/views/api.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 7ff1e88..25c1ff4 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -85,7 +85,7 @@ async def api_payments_create_invoice():
lnurl_response = resp["reason"]
else:
lnurl_response = True
- except httpx.RequestError:
+ except (httpx.ConnectError, httpx.RequestError):
lnurl_response = False
return (
@@ -151,7 +151,7 @@ async def api_payments_pay_lnurl():
r = httpx.get(g.data["callback"], params={"amount": g.data["amount"]}, timeout=20)
if r.is_error:
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
- except httpx.RequestError:
+ except (httpx.ConnectError, httpx.RequestError):
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
params = json.loads(r.text)
From 8d135489abd4eaf24a3e53a0594184a363569775 Mon Sep 17 00:00:00 2001
From: fiatjaf
Date: Mon, 12 Oct 2020 23:17:50 -0300
Subject: [PATCH 06/10] fix: invoice parsing JS bug from the lnurl
implementation.
---
lnbits/core/static/js/wallet.js | 4 ++--
lnbits/core/views/api.py | 5 ++---
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js
index 72f54d4..21f058d 100644
--- a/lnbits/core/static/js/wallet.js
+++ b/lnbits/core/static/js/wallet.js
@@ -370,7 +370,7 @@ new Vue({
let invoice
try {
- invoice = decode(this.parse.data.bolt11)
+ invoice = decode(this.parse.data.request)
} catch (error) {
this.$q.notify({
timeout: 3000,
@@ -416,7 +416,7 @@ new Vue({
})
LNbits.api
- .payInvoice(this.g.wallet, this.parse.data.bolt11)
+ .payInvoice(this.g.wallet, this.parse.data.request)
.then(response => {
this.parse.paymentChecker = setInterval(() => {
LNbits.api
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 25c1ff4..1e292dc 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -51,7 +51,7 @@ async def api_payments():
"amount": {"type": "integer", "min": 1, "required": True},
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
- "lnurl_callback": {"type": "string", "empty": False, "required": False},
+ "lnurl_callback": {"type": "string", "nullable": True, "required": False},
}
)
async def api_payments_create_invoice():
@@ -73,8 +73,7 @@ async def api_payments_create_invoice():
invoice = bolt11.decode(payment_request)
lnurl_response: Union[None, bool, str] = None
- if "lnurl_callback" in g.data:
- print(g.data["lnurl_callback"])
+ if g.data.get("lnurl_callback"):
try:
r = httpx.get(g.data["lnurl_callback"], params={"pr": payment_request}, timeout=10)
if r.is_error:
From cf0bd7ece8d449d801fc3c6633529393c3d4fa2c Mon Sep 17 00:00:00 2001
From: fiatjaf
Date: Tue, 13 Oct 2020 13:57:26 -0300
Subject: [PATCH 07/10] displaying lnurlpay success_actions.
---
lnbits/core/static/js/wallet.js | 26 ++++++++++++--
lnbits/core/views/api.py | 4 +--
lnbits/static/js/base.js | 50 +++++++++++++++++++--------
lnbits/static/js/components.js | 60 +++++++++++++++++++++++++++++++--
lnbits/wallets/lnpay.py | 2 +-
lnbits/wallets/spark.py | 2 ++
6 files changed, 121 insertions(+), 23 deletions(-)
diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js
index 21f058d..661d2e2 100644
--- a/lnbits/core/static/js/wallet.js
+++ b/lnbits/core/static/js/wallet.js
@@ -1,4 +1,4 @@
-/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */
+/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart, decryptLnurlPayAES */
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
@@ -248,7 +248,7 @@ new Vue({
var checker = this.parse.paymentChecker
setTimeout(() => {
clearInterval(checker)
- }, 1000)
+ }, 10000)
},
createInvoice: function () {
this.receive.status = 'loading'
@@ -469,7 +469,7 @@ new Vue({
switch (response.data.success_action.tag) {
case 'url':
this.$q.notify({
- message: `${response.data.success_action.url}`,
+ message: `${response.data.success_action.url}`,
caption: response.data.success_action.description,
html: true,
type: 'info',
@@ -486,6 +486,26 @@ new Vue({
})
break
case 'aes':
+ LNbits.api
+ .getPayment(this.g.wallet, response.data.payment_hash)
+ .then(
+ ({data: payment}) =>
+ console.log(payment) ||
+ decryptLnurlPayAES(
+ response.data.success_action,
+ payment.preimage
+ )
+ )
+ .then(value => {
+ this.$q.notify({
+ message: value,
+ caption: response.data.success_action.description,
+ html: true,
+ type: 'info',
+ timeout: 0,
+ closeBtn: true
+ })
+ })
break
}
}
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 1e292dc..90526d7 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -211,14 +211,14 @@ async def api_payment(payment_hash):
if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
elif not payment.pending:
- return jsonify({"paid": True}), HTTPStatus.OK
+ return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
try:
payment.check_pending()
except Exception:
return jsonify({"paid": False}), HTTPStatus.OK
- return jsonify({"paid": not payment.pending}), HTTPStatus.OK
+ return jsonify({"paid": not payment.pending, "preimage": payment.preimage}), HTTPStatus.OK
@core_app.route("/api/v1/payments/sse", methods=["GET"])
diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js
index 9f1bb28..c1bdbb9 100644
--- a/lnbits/static/js/base.js
+++ b/lnbits/static/js/base.js
@@ -1,10 +1,8 @@
-/* globals moment, Vue, EventHub, axios, Quasar, _ */
+/* globals crypto, moment, Vue, axios, Quasar, _ */
-var LOCALE = 'en'
-
-var EventHub = new Vue()
-
-var LNbits = {
+window.LOCALE = 'en'
+window.EventHub = new Vue()
+window.LNbits = {
api: {
request: function (method, url, apiKey, data) {
return axios({
@@ -106,7 +104,7 @@ var LNbits = {
)
obj.msat = obj.balance
obj.sat = Math.round(obj.balance / 1000)
- obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
+ obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
return obj
},
@@ -134,7 +132,7 @@ var LNbits = {
obj.msat = obj.amount
obj.sat = obj.msat / 1000
obj.tag = obj.extra.tag
- obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
+ obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
obj.isIn = obj.amount > 0
obj.isOut = obj.amount < 0
obj.isPaid = obj.pending === 0
@@ -157,13 +155,13 @@ var LNbits = {
})
},
formatCurrency: function (value, currency) {
- return new Intl.NumberFormat(LOCALE, {
+ return new Intl.NumberFormat(window.LOCALE, {
style: 'currency',
currency: currency
}).format(value)
},
formatSat: function (value) {
- return new Intl.NumberFormat(LOCALE).format(value)
+ return new Intl.NumberFormat(window.LOCALE).format(value)
},
notifyApiError: function (error) {
var types = {
@@ -246,7 +244,7 @@ var LNbits = {
}
}
-var windowMixin = {
+window.windowMixin = {
data: function () {
return {
g: {
@@ -276,17 +274,17 @@ var windowMixin = {
created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
if (window.user) {
- this.g.user = Object.freeze(LNbits.map.user(window.user))
+ this.g.user = Object.freeze(window.LNbits.map.user(window.user))
}
if (window.wallet) {
- this.g.wallet = Object.freeze(LNbits.map.wallet(window.wallet))
+ this.g.wallet = Object.freeze(window.LNbits.map.wallet(window.wallet))
}
if (window.extensions) {
var user = this.g.user
this.g.extensions = Object.freeze(
window.extensions
.map(function (data) {
- return LNbits.map.extension(data)
+ return window.LNbits.map.extension(data)
})
.map(function (obj) {
if (user) {
@@ -303,3 +301,27 @@ var windowMixin = {
}
}
}
+
+window.decryptLnurlPayAES = function (success_action, preimage) {
+ let keyb = new Uint8Array(
+ preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
+ )
+
+ return crypto.subtle
+ .importKey('raw', keyb, {name: 'AES-CBC', length: 256}, false, ['decrypt'])
+ .then(key => {
+ let ivb = Uint8Array.from(window.atob(success_action.iv), c =>
+ c.charCodeAt(0)
+ )
+ let ciphertextb = Uint8Array.from(
+ window.atob(success_action.ciphertext),
+ c => c.charCodeAt(0)
+ )
+
+ return crypto.subtle.decrypt({name: 'AES-CBC', iv: ivb}, key, ciphertextb)
+ })
+ .then(valueb => {
+ let decoder = new TextDecoder('utf-8')
+ return decoder.decode(valueb)
+ })
+}
diff --git a/lnbits/static/js/components.js b/lnbits/static/js/components.js
index 3d8c554..0d37f52 100644
--- a/lnbits/static/js/components.js
+++ b/lnbits/static/js/components.js
@@ -1,4 +1,4 @@
-/* global Vue, moment, LNbits, EventHub */
+/* global Vue, moment, LNbits, EventHub, decryptLnurlPayAES */
Vue.component('lnbits-fsat', {
props: {
@@ -199,10 +199,64 @@ Vue.component('lnbits-payment-details', {
Payment hash:
{{ payment.payment_hash }}
{{ success_action.message || success_action.description }}
+
+ {{ decryptedValue }}
+
+
- {{ parse.lnurlpay.domain }} is requesting - {{ parse.lnurlpay.maxSendable | msatoshiFormat }} sat + {{ parse.lnurlpay.domain }} is requesting {{ + parse.lnurlpay.maxSendable | msatoshiFormat }} sat
{{ parse.lnurlpay.domain }} is requesting
{{ parse.lnurlpay.domain }} is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }} sat
+
+
{{ parse.lnurlpay.domain }} is requesting {{ parse.lnurlpay.description }}
-
+ {{ parse.lnurlpay.description }}
+
+
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 90526d7..fce82ae 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -1,6 +1,6 @@
import trio # type: ignore
import json
-import lnurl
+import lnurl # type: ignore
import httpx
import traceback
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
@@ -306,11 +306,11 @@ async def api_lnurlscan(code: str):
params.update(fixed=data.min_withdrawable == data.max_withdrawable)
# callback with k1 already in it
- url: ParseResult = urlparse(data.callback)
- qs: Dict = parse_qs(url.query)
+ parsed_callback: ParseResult = urlparse(data.callback)
+ qs: Dict = parse_qs(parsed_callback.query)
qs["k1"] = data.k1
- url = url._replace(query=urlencode(qs, doseq=True))
- params.update(callback=urlunparse(url))
+ parsed_callback = parsed_callback._replace(query=urlencode(qs, doseq=True))
+ params.update(callback=urlunparse(parsed_callback))
if type(data) is lnurl.LnurlPayResponse:
params.update(kind="pay")
diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py
index 98f9137..d2486c7 100644
--- a/lnbits/wallets/base.py
+++ b/lnbits/wallets/base.py
@@ -32,7 +32,7 @@ class PaymentStatus(NamedTuple):
class Wallet(ABC):
@abstractmethod
- def status() -> StatusResponse:
+ def status(self) -> StatusResponse:
pass
@abstractmethod
diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py
index 65629f7..e1b6d44 100644
--- a/lnbits/wallets/lnpay.py
+++ b/lnbits/wallets/lnpay.py
@@ -23,7 +23,7 @@ class LNPayWallet(Wallet):
try:
r = httpx.get(url, headers=self.auth)
except (httpx.ConnectError, httpx.RequestError):
- return StatusResponse(f"Unable to connect to '{url}'")
+ return StatusResponse(f"Unable to connect to '{url}'", 0)
if r.is_error:
return StatusResponse(r.text[:250], 0)
diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py
index 8b772e7..4510e0d 100644
--- a/lnbits/wallets/opennode.py
+++ b/lnbits/wallets/opennode.py
@@ -24,7 +24,7 @@ class OpenNodeWallet(Wallet):
try:
r = httpx.get(f"{self.endpoint}/v1/account/balance", headers=self.auth)
except (httpx.ConnectError, httpx.RequestError):
- return StatusResponse(f"Unable to connect to '{self.endpoint}'")
+ return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
data = r.json()["message"]
if r.is_error:
From 1529ebb689f48745cda7338f0a5a471af37a5509 Mon Sep 17 00:00:00 2001
From: fiatjaf
+ and a {{parse.lnurlpay.commentAllowed}}-char comment
+
between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and
{{ parse.lnurlpay.maxSendable | msatoshiFormat }} sat
+
+
+ and a {{parse.lnurlpay.commentAllowed}}-char comment
+