mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-19 18:11:30 +01:00
actually paying and withdrawing with lnurl.
This commit is contained in:
parent
3cd15c40fc
commit
bc2207ba27
@ -51,7 +51,12 @@ def create_invoice(
|
|||||||
|
|
||||||
|
|
||||||
def pay_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:
|
) -> str:
|
||||||
temp_id = f"temp_{urlsafe_short_hash()}"
|
temp_id = f"temp_{urlsafe_short_hash()}"
|
||||||
internal_id = f"internal_{urlsafe_short_hash()}"
|
internal_id = f"internal_{urlsafe_short_hash()}"
|
||||||
@ -79,7 +84,7 @@ def pay_invoice(
|
|||||||
payment_request=payment_request,
|
payment_request=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
amount=-invoice.amount_msat,
|
amount=-invoice.amount_msat,
|
||||||
memo=invoice.description or "",
|
memo=description or invoice.description or "",
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -256,16 +256,36 @@ new Vue({
|
|||||||
.createInvoice(
|
.createInvoice(
|
||||||
this.g.wallet,
|
this.g.wallet,
|
||||||
this.receive.data.amount,
|
this.receive.data.amount,
|
||||||
this.receive.data.memo
|
this.receive.data.memo,
|
||||||
|
this.receive.lnurl && this.receive.lnurl.callback
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.receive.status = 'success'
|
this.receive.status = 'success'
|
||||||
this.receive.paymentReq = response.data.payment_request
|
this.receive.paymentReq = response.data.payment_request
|
||||||
|
|
||||||
if (this.receive.lnurl) {
|
if (response.data.lnurl_response !== null) {
|
||||||
// send invoice to lnurl callback
|
if (response.data.lnurl_response === false) {
|
||||||
console.log('sending', this.receive.lnurl)
|
response.data.lnurl_response = `Unable to connect`
|
||||||
LNbits.api.sendInvoiceToLnurlWithdraw(this.receive.paymentReq)
|
}
|
||||||
|
|
||||||
|
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(() => {
|
this.receive.paymentChecker = setInterval(() => {
|
||||||
@ -274,6 +294,7 @@ new Vue({
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.paid) {
|
if (response.data.paid) {
|
||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
|
this.fetchBalance()
|
||||||
this.receive.show = false
|
this.receive.show = false
|
||||||
clearInterval(this.receive.paymentChecker)
|
clearInterval(this.receive.paymentChecker)
|
||||||
}
|
}
|
||||||
@ -314,12 +335,11 @@ new Vue({
|
|||||||
let data = response.data
|
let data = response.data
|
||||||
|
|
||||||
if (data.status === 'ERROR') {
|
if (data.status === 'ERROR') {
|
||||||
Quasar.plugins.Notify.create({
|
this.$q.notify({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: data.reason,
|
message: `${data.domain} lnurl call failed.`,
|
||||||
caption: `${data.domain} returned an error to the lnurl call.`,
|
caption: data.reason
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -331,13 +351,16 @@ new Vue({
|
|||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
this.receive.show = true
|
this.receive.show = true
|
||||||
this.receive.status = 'pending'
|
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.data.memo = data.defaultDescription
|
||||||
this.receive.minMax = [data.minWithdrawable, data.maxWithdrawable]
|
this.receive.minMax = [
|
||||||
|
data.minWithdrawable / 1000,
|
||||||
|
data.maxWithdrawable / 1000
|
||||||
|
]
|
||||||
this.receive.lnurl = {
|
this.receive.lnurl = {
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
callback: data.callback,
|
callback: data.callback,
|
||||||
k1: data.k1,
|
|
||||||
fixed: data.fixed
|
fixed: data.fixed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,8 +376,7 @@ new Vue({
|
|||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: error + '.',
|
message: error + '.',
|
||||||
caption: '400 BAD REQUEST',
|
caption: '400 BAD REQUEST'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
return
|
return
|
||||||
@ -390,8 +412,7 @@ new Vue({
|
|||||||
payInvoice: function () {
|
payInvoice: function () {
|
||||||
let dismissPaymentMsg = this.$q.notify({
|
let dismissPaymentMsg = this.$q.notify({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Processing payment...',
|
message: 'Processing payment...'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
@ -406,6 +427,7 @@ new Vue({
|
|||||||
clearInterval(this.parse.paymentChecker)
|
clearInterval(this.parse.paymentChecker)
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
|
this.fetchBalance()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@ -418,22 +440,55 @@ new Vue({
|
|||||||
payLnurl: function () {
|
payLnurl: function () {
|
||||||
let dismissPaymentMsg = this.$q.notify({
|
let dismissPaymentMsg = this.$q.notify({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Processing payment...',
|
message: 'Processing payment...'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
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 => {
|
.then(response => {
|
||||||
|
this.parse.show = false
|
||||||
|
|
||||||
this.parse.paymentChecker = setInterval(() => {
|
this.parse.paymentChecker = setInterval(() => {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.getPayment(this.g.wallet, response.data.payment_hash)
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.data.paid) {
|
if (res.data.paid) {
|
||||||
this.parse.show = false
|
|
||||||
clearInterval(this.parse.paymentChecker)
|
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
|
clearInterval(this.parse.paymentChecker)
|
||||||
this.fetchPayments()
|
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: `<a target="_blank" style="color:inherit" href="${response.data.success_action.url}">${response.data.success_action.url}</a>`,
|
||||||
|
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)
|
}, 2000)
|
||||||
@ -475,8 +530,7 @@ new Vue({
|
|||||||
checkPendingPayments: function () {
|
checkPendingPayments: function () {
|
||||||
var dismissMsg = this.$q.notify({
|
var dismissMsg = this.$q.notify({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Checking pending transactions...',
|
message: 'Checking pending transactions...'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.fetchPayments(true).then(() => {
|
this.fetchPayments(true).then(() => {
|
||||||
|
@ -130,7 +130,7 @@
|
|||||||
<q-td auto-width key="sat" :props="props">
|
<q-td auto-width key="sat" :props="props">
|
||||||
{{ props.row.fsat }}
|
{{ props.row.fsat }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width key="sat" :props="props">
|
<q-td auto-width key="fee" :props="props">
|
||||||
{{ props.row.fee }}
|
{{ props.row.fee }}
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
@ -266,8 +266,8 @@
|
|||||||
v-model.number="receive.data.amount"
|
v-model.number="receive.data.amount"
|
||||||
type="number"
|
type="number"
|
||||||
label="Amount (sat) *"
|
label="Amount (sat) *"
|
||||||
min="receive.minMax[0]"
|
:min="receive.minMax[0]"
|
||||||
max="receive.minMax[1]"
|
:max="receive.minMax[1]"
|
||||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
@ -347,7 +347,8 @@
|
|||||||
{% raw %}
|
{% raw %}
|
||||||
<q-form @submit="payLnurl" class="q-gutter-md">
|
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||||
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||||
{{ parse.lnurlpay.maxSendable | msatoshiFormat }}
|
<b>{{ parse.lnurlpay.domain }}</b> is requesting
|
||||||
|
{{ parse.lnurlpay.maxSendable | msatoshiFormat }} sat
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="q-my-none text-h6 text-center">
|
<p v-else class="q-my-none text-h6 text-center">
|
||||||
<b>{{ parse.lnurlpay.domain }}</b> is requesting <br />
|
<b>{{ parse.lnurlpay.domain }}</b> is requesting <br />
|
||||||
|
@ -3,11 +3,11 @@ import json
|
|||||||
import lnurl
|
import lnurl
|
||||||
import httpx
|
import httpx
|
||||||
import traceback
|
import traceback
|
||||||
|
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
||||||
from quart import g, jsonify, request, make_response
|
from quart import g, jsonify, request, make_response
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from urllib.parse import urlparse
|
from typing import Dict, Union
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
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},
|
"amount": {"type": "integer", "min": 1, "required": True},
|
||||||
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
||||||
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
||||||
|
"lnurl_callback": {"type": "string", "empty": False, "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_payments_create_invoice():
|
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
|
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
invoice = bolt11.decode(payment_request)
|
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 (
|
return (
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
@ -77,6 +95,7 @@ async def api_payments_create_invoice():
|
|||||||
"payment_request": payment_request,
|
"payment_request": payment_request,
|
||||||
# maintain backwards compatibility with API clients:
|
# maintain backwards compatibility with API clients:
|
||||||
"checking_id": invoice.payment_hash,
|
"checking_id": invoice.payment_hash,
|
||||||
|
"lnurl_response": lnurl_response,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
HTTPStatus.CREATED,
|
HTTPStatus.CREATED,
|
||||||
@ -117,6 +136,74 @@ async def api_payments_create():
|
|||||||
return await api_payments_create_invoice()
|
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/<payment_hash>", methods=["GET"])
|
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_payment(payment_hash):
|
async def api_payment(payment_hash):
|
||||||
@ -216,10 +303,20 @@ async def api_lnurlscan(code: str):
|
|||||||
|
|
||||||
params: Dict = data.dict()
|
params: Dict = data.dict()
|
||||||
if type(data) is lnurl.LnurlWithdrawResponse:
|
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:
|
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)
|
params.update(description=data.metadata.text)
|
||||||
if data.metadata.images:
|
if data.metadata.images:
|
||||||
image = min(data.metadata.images, key=lambda image: len(image[1]))
|
image = min(data.metadata.images, key=lambda image: len(image[1]))
|
||||||
|
@ -16,11 +16,12 @@ var LNbits = {
|
|||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createInvoice: function (wallet, amount, memo) {
|
createInvoice: function (wallet, amount, memo, lnurlCallback = null) {
|
||||||
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
||||||
out: false,
|
out: false,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo
|
memo: memo,
|
||||||
|
lnurl_callback: lnurlCallback
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payInvoice: function (wallet, bolt11) {
|
payInvoice: function (wallet, bolt11) {
|
||||||
@ -29,6 +30,20 @@ var LNbits = {
|
|||||||
bolt11: bolt11
|
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) {
|
getWallet: function (wallet) {
|
||||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user