Browse Source

lnurlp extension.

aiosqlite
fiatjaf 4 years ago
parent
commit
a79ec8f4a4
  1. 1
      lnbits/extensions/lnurlp/README.md
  2. 8
      lnbits/extensions/lnurlp/__init__.py
  3. 10
      lnbits/extensions/lnurlp/config.json
  4. 74
      lnbits/extensions/lnurlp/crud.py
  5. 24
      lnbits/extensions/lnurlp/migrations.py
  6. 32
      lnbits/extensions/lnurlp/models.py
  7. 130
      lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
  8. 29
      lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html
  9. 54
      lnbits/extensions/lnurlp/templates/lnurlp/display.html
  10. 402
      lnbits/extensions/lnurlp/templates/lnurlp/index.html
  11. 28
      lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html
  12. 28
      lnbits/extensions/lnurlp/views.py
  13. 125
      lnbits/extensions/lnurlp/views_api.py
  14. 2
      lnbits/wallets/lnpay.py

1
lnbits/extensions/lnurlp/README.md

@ -0,0 +1 @@
# LNURLp

8
lnbits/extensions/lnurlp/__init__.py

@ -0,0 +1,8 @@
from flask import Blueprint
lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa

10
lnbits/extensions/lnurlp/config.json

@ -0,0 +1,10 @@
{
"name": "LNURLp",
"short_description": "Make reusable LNURL pay links",
"icon": "receipt",
"contributors": [
"arcbtc",
"eillarra",
"fiatjaf"
]
}

74
lnbits/extensions/lnurlp/crud.py

@ -0,0 +1,74 @@
from typing import List, Optional, Union
from lnbits.db import open_ext_db
from .models import PayLink
def create_pay_link(*, wallet_id: str, description: str, amount: int) -> PayLink:
with open_ext_db("lnurlp") as db:
with db.cursor() as c:
c.execute(
"""
INSERT INTO pay_links (
wallet,
description,
amount,
served_meta,
served_pr
)
VALUES (?, ?, ?, 0, 0)
""",
(wallet_id, description, amount),
)
return get_pay_link(c.lastrowid)
def get_pay_link(link_id: str) -> Optional[PayLink]:
with open_ext_db("lnurlp") as db:
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
def get_pay_link_by_hash(unique_hash: str) -> Optional[PayLink]:
with open_ext_db("lnurlp") as db:
row = db.fetchone("SELECT * FROM pay_links WHERE unique_hash = ?", (unique_hash,))
return PayLink.from_row(row) if row else None
def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("lnurlp") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM pay_links WHERE wallet IN ({q})", (*wallet_ids,))
return [PayLink.from_row(row) for row in rows]
def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("lnurlp") as db:
db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
with open_ext_db("lnurlp") as db:
db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
def delete_pay_link(link_id: str) -> None:
with open_ext_db("lnurlp") as db:
db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))

24
lnbits/extensions/lnurlp/migrations.py

@ -0,0 +1,24 @@
from lnbits.db import open_ext_db
def m001_initial(db):
"""
Initial pay table.
"""
db.execute(
"""
CREATE TABLE IF NOT EXISTS pay_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL,
description TEXT NOT NULL,
amount INTEGER NOT NULL,
served_meta INTEGER NOT NULL,
served_pr INTEGER NOT NULL
);
"""
)
def migrate():
with open_ext_db("lnurlp") as db:
m001_initial(db)

32
lnbits/extensions/lnurlp/models.py

@ -0,0 +1,32 @@
import json
from flask import url_for
from lnurl import Lnurl, encode as lnurl_encode
from lnurl.types import LnurlPayMetadata
from sqlite3 import Row
from typing import NamedTuple
from lnbits.settings import FORCE_HTTPS
class PayLink(NamedTuple):
id: str
wallet: str
description: str
amount: int
served_meta: int
served_pr: int
@classmethod
def from_row(cls, row: Row) -> "PayLink":
data = dict(row)
return cls(**data)
@property
def lnurl(self) -> Lnurl:
scheme = "https" if FORCE_HTTPS else None
url = url_for("lnurlp.api_lnurl_response", link_id=self.id, _external=True, _scheme=scheme)
return lnurl_encode(url)
@property
def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))

130
lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html

@ -0,0 +1,130 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List pay links">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /pay/api/v1/links</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;pay_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}pay/api/v1/links -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get a pay link">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/pay/api/v1/links/&lt;pay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}pay/api/v1/links/&lt;pay_id&gt; -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create a pay link"
>
<q-card>
<q-card-section>
<code><span class="text-green">POST</span> /pay/api/v1/links</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"description": &lt;string&gt; "amount": &lt;integer&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}pay/api/v1/links -d
'{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Update a pay link"
>
<q-card>
<q-card-section>
<code
><span class="text-green">PUT</span>
/pay/api/v1/links/&lt;pay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"description": &lt;string&gt;, "amount": &lt;integer&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.url_root }}pay/api/v1/links/&lt;pay_id&gt; -d
'{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a pay link"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/pay/api/v1/links/&lt;pay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root }}pay/api/v1/links/&lt;pay_id&gt;
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

29
lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html

@ -0,0 +1,29 @@
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
<q-card>
<q-card-section>
<p>
<b>WARNING: LNURL must be used over https or TOR</b><br />
LNURL is a range of lightning-network standards that allow us to use
lightning-network differently. An LNURL withdraw is the permission for
someone to pull a certain amount of funds from a lightning wallet. In
this extension time is also added - an amount can be withdraw over a
period of time. A typical use case for an LNURL withdraw is a faucet,
although it is a very powerful technology, with much further reaching
implications. For example, an LNURL withdraw could be minted to pay for
a subscription service.
</p>
<p>
Exploring LNURL and finding use cases, is really helping inform
lightning protocol development, rather than the protocol dictating how
lightning-network should be engaged with.
</p>
<small
>Check
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
>Awesome LNURL</a
>
for further information.</small
>
</q-card-section>
</q-card>
</q-expansion-item>

54
lnbits/extensions/lnurlp/templates/lnurlp/display.html

@ -0,0 +1,54 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<div class="text-center">
<a href="lightning:{{ link.lnurl }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
value="{{ link.lnurl }}"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText('{{ link.lnurl }}')"
>Copy LNURL</q-btn
>
</div>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">
LNbits LNURL-pay link
</h6>
<p class="q-my-none">
Use a LNURL compatible bitcoin wallet to claim the sats.
</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "lnurlp/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin]
})
</script>
{% endblock %}

402
lnbits/extensions/lnurlp/templates/lnurlp/index.html

@ -0,0 +1,402 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New pay link</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Pay links</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="payLinks"
row-key="id"
:columns="payLinksTable.columns"
:pagination.sync="payLinksTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.pay_url"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deletePayLink(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
LNbits LNURL-pay extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "lnurlp/_api_docs.html" %}
<q-separator></q-separator>
{% include "lnurlp/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.description"
type="text"
label="Item description *"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount (sat) *"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update pay link</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="
formDialog.data.wallet == null ||
formDialog.data.description == null ||
(
formDialog.data.amount == null ||
formDialog.data.amount < 1
)"
type="submit"
>Create pay link</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
{% raw %}
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeDialog.data.lnurl"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<p style="word-break: break-all;">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }} sat<br />
</p>
{% endraw %}
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
class="q-ml-sm"
>Copy LNURL</q-btn
>
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
>Shareable link</q-btn
>
<q-btn
v-if="!qrCodeDialog.data.is_unique"
outline
color="grey"
icon="print"
type="a"
:href="qrCodeDialog.data.print_url"
target="_blank"
></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %}
{% block scripts %} {{ window_vars(user) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
var locationPath = [
window.location.protocol,
'//',
window.location.hostname,
window.location.pathname
].join('')
var mapPayLink = function(obj) {
obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.print_url = [locationPath, 'print/', obj.id].join('')
obj.pay_url = [locationPath, obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function() {
return {
checker: null,
payLinks: [],
payLinksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'amount',
align: 'right',
label: 'Amount (sat)',
field: 'amount'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
secondMultiplier: 'seconds',
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
data: {
is_unique: false
}
},
qrCodeDialog: {
show: false,
data: null
}
}
},
methods: {
getPayLinks: function() {
var self = this
LNbits.api
.request(
'GET',
'/lnurlp/api/v1/links?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function(response) {
self.payLinks = response.data.map(function(obj) {
return mapPayLink(obj)
})
})
.catch(function(error) {
clearInterval(self.checker)
LNbits.utils.notifyApiError(error)
})
},
closeFormDialog: function() {
this.formDialog.data = {
is_unique: false
}
},
openQrCodeDialog: function(linkId) {
var link = _.findWhere(this.payLinks, {id: linkId})
this.qrCodeDialog.data = _.clone(link)
this.qrCodeDialog.show = true
},
openUpdateDialog: function(linkId) {
var link = _.findWhere(this.payLinks, {id: linkId})
this.formDialog.data = _.clone(link._data)
this.formDialog.show = true
},
sendFormData: function() {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
var data = _.omit(this.formDialog.data, 'wallet')
data.wait_time =
data.wait_time *
{
seconds: 1,
minutes: 60,
hours: 3600
}[this.formDialog.secondMultiplier]
if (data.id) {
this.updatePayLink(wallet, data)
} else {
this.createPayLink(wallet, data)
}
},
updatePayLink: function(wallet, data) {
var self = this
LNbits.api
.request(
'PUT',
'/lnurlp/api/v1/links/' + data.id,
wallet.adminkey,
_.pick(data, 'description', 'amount')
)
.then(function(response) {
self.payLinks = _.reject(self.payLinks, function(obj) {
return obj.id === data.id
})
self.payLinks.push(mapPayLink(response.data))
self.formDialog.show = false
})
.catch(function(error) {
LNbits.utils.notifyApiError(error)
})
},
createPayLink: function(wallet, data) {
var self = this
LNbits.api
.request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
.then(function(response) {
self.payLinks.push(mapPayLink(response.data))
self.formDialog.show = false
})
.catch(function(error) {
LNbits.utils.notifyApiError(error)
})
},
deletePayLink: function(linkId) {
var self = this
var link = _.findWhere(this.payLinks, {id: linkId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function() {
LNbits.api
.request(
'DELETE',
'/lnurlp/api/v1/links/' + linkId,
_.findWhere(self.g.user.wallets, {id: link.wallet}).adminkey
)
.then(function(response) {
self.payLinks = _.reject(self.payLinks, function(obj) {
return obj.id === linkId
})
})
.catch(function(error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function() {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
}
},
created: function() {
if (this.g.user.wallets.length) {
var getPayLinks = this.getPayLinks
getPayLinks()
this.checker = setInterval(function() {
getPayLinks()
}, 20000)
}
}
})
</script>
{% endblock %}

28
lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html

@ -0,0 +1,28 @@
{% extends "print.html" %} {% block page %}
<div class="row justify-center">
<div class="qr">
<qrcode value="{{ link.lnurl }}" :options="{width}"></qrcode>
</div>
</div>
{% endblock %} {% block styles %}
<style>
.qr {
margin: auto;
}
</style>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
created: function() {
window.print()
},
data: function() {
return {width: window.innerWidth * 0.5}
}
})
</script>
{% endblock %}

28
lnbits/extensions/lnurlp/views.py

@ -0,0 +1,28 @@
from flask import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.lnurlp import lnurlp_ext
from .crud import get_pay_link
@lnurlp_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
return render_template("lnurlp/index.html", user=g.user)
@lnurlp_ext.route("/<link_id>")
def display(link_id):
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
return render_template("lnurlp/display.html", link=link)
@lnurlp_ext.route("/print/<link_id>")
def print_qr(link_id):
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
return render_template("lnurlp/print_qr.html", link=link)

125
lnbits/extensions/lnurlp/views_api.py

@ -0,0 +1,125 @@
from flask import g, jsonify, request, url_for
from http import HTTPStatus
from lnurl import LnurlPayResponse, LnurlPayActionResponse
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import FORCE_HTTPS
from lnbits.extensions.lnurlp import lnurlp_ext
from .crud import (
create_pay_link,
get_pay_link,
get_pay_links,
update_pay_link,
increment_pay_link,
delete_pay_link,
)
@lnurlp_ext.route("/api/v1/links", methods=["GET"])
@api_check_wallet_key("invoice")
def api_links():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
try:
return (
jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in get_pay_links(wallet_ids)]),
HTTPStatus.OK,
)
except LnurlInvalidUrl:
return (
jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}),
HTTPStatus.UPGRADE_REQUIRED,
)
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice")
def api_link_retrieve(link_id):
link = get_pay_link(link_id)
if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK
@lnurlp_ext.route("/api/v1/links", methods=["POST"])
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["PUT"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"description": {"type": "string", "empty": False, "required": True},
"amount": {"type": "integer", "min": 1, "required": True},
}
)
def api_link_create_or_update(link_id=None):
if link_id:
link = get_pay_link(link_id)
if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
link = update_pay_link(link_id, **g.data)
else:
link = create_pay_link(wallet_id=g.wallet.id, **g.data)
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK if link_id else HTTPStatus.CREATED
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
def api_link_delete(link_id):
link = get_pay_link(link_id)
if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
delete_pay_link(link_id)
return "", HTTPStatus.NO_CONTENT
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
def api_lnurl_response(link_id):
link = increment_pay_link(link_id, served_meta=1)
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
scheme = "https" if FORCE_HTTPS else None
url = url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True, _scheme=scheme)
resp = LnurlPayResponse(
callback=url, min_sendable=link.amount * 1000, max_sendable=link.amount * 1000, metadata=link.lnurlpay_metadata,
)
return jsonify(resp.dict()), HTTPStatus.OK
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
def api_lnurl_callback(link_id):
link = increment_pay_link(link_id, served_pr=1)
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
_, payment_request = create_invoice(
wallet_id=link.wallet, amount=link.amount, memo=None, description_hash=link.lnurlpay_metadata.encode("utf-8"),
)
resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[])
return jsonify(resp.dict()), HTTPStatus.OK

2
lnbits/wallets/lnpay.py

@ -26,7 +26,7 @@ class LNPayWallet(Wallet):
"description_hash": base64.b64encode(description_hash).decode("ascii"), "description_hash": base64.b64encode(description_hash).decode("ascii"),
}, },
) )
ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, None ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, r.text
if ok: if ok:
data = r.json() data = r.json()

Loading…
Cancel
Save