feat: update to lnbits 1.0.0 (#66)

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
This commit is contained in:
dni ⚡ 2024-10-25 12:02:37 +02:00 committed by GitHub
commit c7623e4c5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1372 additions and 1278 deletions

View file

@ -2,7 +2,7 @@
"name": "Pay Links", "name": "Pay Links",
"short_description": "Make reusable LNURL pay links", "short_description": "Make reusable LNURL pay links",
"tile": "/lnurlp/static/image/lnurl-pay.png", "tile": "/lnurlp/static/image/lnurl-pay.png",
"min_lnbits_version": "0.12.4", "min_lnbits_version": "1.0.0",
"contributors": [ "contributors": [
{ {
"name": "arcbtc", "name": "arcbtc",

139
crud.py
View file

@ -1,7 +1,7 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import insert_query, update_query, urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from .models import CreatePayLinkData, LnurlpSettings, PayLink from .models import CreatePayLinkData, LnurlpSettings, PayLink
from .nostr.key import PrivateKey from .nostr.key import PrivateKey
@ -10,22 +10,19 @@ db = Database("ext_lnurlp")
async def get_or_create_lnurlp_settings() -> LnurlpSettings: async def get_or_create_lnurlp_settings() -> LnurlpSettings:
row = await db.fetchone("SELECT * FROM lnurlp.settings LIMIT 1") settings = await db.fetchone(
if row: "SELECT * FROM lnurlp.settings LIMIT 1", model=LnurlpSettings
return LnurlpSettings(**row) )
if settings:
return settings
else: else:
settings = LnurlpSettings(nostr_private_key=PrivateKey().hex()) settings = LnurlpSettings(nostr_private_key=PrivateKey().hex())
await db.execute( await db.insert("lnurlp.settings", settings)
insert_query("lnurlp.settings", settings), (*settings.dict().values(),)
)
return settings return settings
async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings: async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings:
await db.execute( await db.update("lnurlp.settings", settings, "")
update_query("lnurlp.settings", settings, where=""),
(*settings.dict().values(),),
)
return settings return settings
@ -34,110 +31,74 @@ async def delete_lnurlp_settings() -> None:
async def get_pay_link_by_username(username: str) -> Optional[PayLink]: async def get_pay_link_by_username(username: str) -> Optional[PayLink]:
row = await db.fetchone( return await db.fetchone(
"SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,) "SELECT * FROM lnurlp.pay_links WHERE username = :username",
{"username": username},
PayLink,
) )
return PayLink.from_row(row) if row else None
async def create_pay_link(data: CreatePayLinkData) -> PayLink: async def create_pay_link(data: CreatePayLinkData) -> PayLink:
link_id = urlsafe_short_hash()[:6] link_id = urlsafe_short_hash()[:6]
result = await db.execute( assert data.wallet, "Wallet is required"
"""
INSERT INTO lnurlp.pay_links (
id,
wallet,
description,
min,
max,
served_meta,
served_pr,
webhook_url,
webhook_headers,
webhook_body,
success_text,
success_url,
comment_chars,
currency,
fiat_base_multiplier,
username,
zaps
) link = PayLink(
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) id=link_id,
""", wallet=data.wallet,
( description=data.description,
link_id, min=data.min,
data.wallet, max=data.max,
data.description, served_meta=0,
data.min, served_pr=0,
data.max, username=data.username,
data.webhook_url, zaps=data.zaps,
data.webhook_headers, domain=None,
data.webhook_body, webhook_url=data.webhook_url,
data.success_text, webhook_headers=data.webhook_headers,
data.success_url, webhook_body=data.webhook_body,
data.comment_chars, success_text=data.success_text,
data.currency, success_url=data.success_url,
data.fiat_base_multiplier, currency=data.currency,
data.username, comment_chars=data.comment_chars,
data.zaps, fiat_base_multiplier=data.fiat_base_multiplier,
),
) )
assert result
link = await get_pay_link(link_id) await db.insert("lnurlp.pay_links", link)
assert link, "Newly created link couldn't be retrieved"
return link return link
async def get_address_data(username: str) -> Optional[PayLink]: async def get_address_data(username: str) -> Optional[PayLink]:
row = await db.fetchone( return await db.fetchone(
"SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,) "SELECT * FROM lnurlp.pay_links WHERE username = :username",
{"username": username},
PayLink,
) )
return PayLink.from_row(row) if row else None
async def get_pay_link(link_id: str) -> Optional[PayLink]: async def get_pay_link(link_id: str) -> Optional[PayLink]:
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,)) return await db.fetchone(
return PayLink.from_row(row) if row else None "SELECT * FROM lnurlp.pay_links WHERE id = :id",
{"id": link_id},
PayLink,
)
async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]: async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
q = ",".join(["?"] * len(wallet_ids)) return await db.fetchall(
rows = await db.fetchall( f"SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q}) ORDER BY Id",
f""" model=PayLink,
SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q})
ORDER BY Id
""",
(*wallet_ids,),
) )
return [PayLink.from_row(row) for row in rows]
async def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]: async def update_pay_link(link: PayLink) -> PayLink:
await db.update("lnurlp.pay_links", link)
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) return link
await db.execute(
f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
)
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
async def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
)
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
async def delete_pay_link(link_id: str) -> None: async def delete_pay_link(link_id: str) -> None:
await db.execute("DELETE FROM lnurlp.pay_links WHERE id = ?", (link_id,)) await db.execute("DELETE FROM lnurlp.pay_links WHERE id = :id", {"id": link_id})

View file

@ -174,3 +174,10 @@ async def m009_add_settings(db):
); );
""" """
) )
async def m010_add_pay_link_domain(db):
"""
Add domain to pay links
"""
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN domain TEXT;")

View file

@ -1,9 +1,7 @@
import json import json
from sqlite3 import Row
from typing import Optional from typing import Optional
from fastapi import Request from fastapi import Query, Request
from fastapi.param_functions import Query
from lnurl import encode as lnurl_encode from lnurl import encode as lnurl_encode
from lnurl.types import LnurlPayMetadata from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel from pydantic import BaseModel
@ -61,14 +59,6 @@ class PayLink(BaseModel):
max: float max: float
fiat_base_multiplier: int fiat_base_multiplier: int
@classmethod
def from_row(cls, row: Row) -> "PayLink":
data = dict(row)
if data["currency"] and data["fiat_base_multiplier"]:
data["min"] /= data["fiat_base_multiplier"]
data["max"] /= data["fiat_base_multiplier"]
return cls(**data)
def lnurl(self, req: Request) -> str: def lnurl(self, req: Request) -> str:
url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id) url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id)
url_str = str(url) url_str = str(url)

2270
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ authors = ["Alan Bits <alan@lnbits.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10 | ^3.9" python = "^3.10 | ^3.9"
lnbits = "*" lnbits = {version = "*", allow-prereleases = true}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^24.3.0" black = "^24.3.0"

View file

@ -1,7 +1,5 @@
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ /* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
Vue.component(VueQrcode.name, VueQrcode)
const locationPath = [ const locationPath = [
window.location.protocol, window.location.protocol,
'//', '//',
@ -11,19 +9,17 @@ const locationPath = [
const mapPayLink = obj => { const mapPayLink = obj => {
obj._data = _.clone(obj) obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate( obj.date = LNbits.utils.formatDate(obj.time)
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount) obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.print_url = [locationPath, 'print/', obj.id].join('') obj.print_url = [locationPath, 'print/', obj.id].join('')
obj.pay_url = [locationPath, 'link/', obj.id].join('') obj.pay_url = [locationPath, 'link/', obj.id].join('')
return obj return obj
} }
new Vue({ window.app = Vue.createApp({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [window.windowMixin],
computed: { computed: {
endpoint: function () { endpoint: function () {
return `/lnurlp/api/v1/settings?usr=${this.g.user.id}` return `/lnurlp/api/v1/settings?usr=${this.g.user.id}`
@ -240,7 +236,7 @@ new Vue({
} }
}, },
created() { created() {
if (this.g.user.wallets.length) { if (this.g.user.wallets?.length) {
var getPayLinks = this.getPayLinks var getPayLinks = this.getPayLinks
getPayLinks() getPayLinks()
this.checker = setInterval(() => { this.checker = setInterval(() => {

View file

@ -5,9 +5,8 @@ from threading import Thread
from typing import List from typing import List
import httpx import httpx
from lnbits.core.crud import update_payment_extra from lnbits.core.crud import get_payment, update_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from loguru import logger from loguru import logger
from websocket import WebSocketApp from websocket import WebSocketApp
@ -19,7 +18,7 @@ from .nostr.event import Event
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name()) register_invoice_listener(invoice_queue, "ext_lnurlp")
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()
@ -27,7 +26,8 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment): async def on_invoice_paid(payment: Payment):
if payment.extra.get("tag") != "lnurlp":
if not payment.extra or payment.extra.get("tag") != "lnurlp":
return return
if payment.extra.get("wh_status"): if payment.extra.get("wh_status"):
@ -65,8 +65,10 @@ async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
"payment_hash": payment.payment_hash, "payment_hash": payment.payment_hash,
"payment_request": payment.bolt11, "payment_request": payment.bolt11,
"amount": payment.amount, "amount": payment.amount,
"comment": payment.extra.get("comment"), "comment": payment.extra.get("comment") if payment.extra else None,
"webhook_data": payment.extra.get("webhook_data") or "", "webhook_data": (
payment.extra.get("webhook_data") if payment.extra else None
),
"lnurlp": pay_link.id, "lnurlp": pay_link.id,
"body": ( "body": (
json.loads(pay_link.webhook_body) json.loads(pay_link.webhook_body)
@ -80,7 +82,7 @@ async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
if pay_link.webhook_headers if pay_link.webhook_headers
else None else None
), ),
timeout=40, timeout=6,
) )
await mark_webhook_sent( await mark_webhook_sent(
payment.payment_hash, payment.payment_hash,
@ -99,20 +101,19 @@ async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
async def mark_webhook_sent( async def mark_webhook_sent(
payment_hash: str, status: int, is_success: bool, reason_phrase="", text="" payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
) -> None: ) -> None:
await update_payment_extra( payment = await get_payment(payment_hash)
payment_hash, extra = payment.extra or {}
{ extra["wh_status"] = status # keep for backwards compability
"wh_status": status, # keep for backwards compability extra["wh_success"] = is_success
"wh_success": is_success, extra["wh_message"] = reason_phrase
"wh_message": reason_phrase, extra["wh_response"] = text
"wh_response": text, payment.extra = extra
}, await update_payment(payment)
)
# NIP-57 - load the zap request # NIP-57 - load the zap request
async def send_zap(payment: Payment): async def send_zap(payment: Payment):
nostr = payment.extra.get("nostr") nostr = payment.extra.get("nostr") if payment.extra else None
if not nostr: if not nostr:
return return

View file

@ -19,7 +19,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key: >curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key:
{{ user.wallets[0].inkey }}" <span v-text="g.user.wallets[0].inkey"></span> "
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -41,7 +41,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links/&lt;pay_id&gt; >curl -X GET {{ request.base_url }}lnurlp/api/v1/links/&lt;pay_id&gt;
-H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "X-Api-Key: <span v-text="g.user.wallets[0].inkey"></span>"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -74,7 +74,7 @@
'{"description": &lt;string&gt;, "amount": &lt;integer&gt;, "max": '{"description": &lt;string&gt;, "amount": &lt;integer&gt;, "max":
&lt;integer&gt;, "min": &lt;integer&gt;, "comment_chars": &lt;integer&gt;, "min": &lt;integer&gt;, "comment_chars":
&lt;integer&gt;}' -H "Content-type: application/json" -H "X-Api-Key: &lt;integer&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{ user.wallets[0].adminkey }}" <span v-text="g.user.wallets[0].adminkey"></span>"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -103,8 +103,8 @@
<code <code
>curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/&lt;pay_id&gt; >curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/&lt;pay_id&gt;
-d '{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H -d '{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{ "Content-type: application/json" -H "X-Api-Key:
user.wallets[0].adminkey }}" <span v-text="g.user.wallets[0].adminkey"></span>"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -129,8 +129,8 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.base_url >curl -X DELETE {{ request.base_url
}}lnurlp/api/v1/links/&lt;pay_id&gt; -H "X-Api-Key: {{ }}lnurlp/api/v1/links/&lt;pay_id&gt; -H "X-Api-Key:
user.wallets[0].adminkey }}" <span v-text="g.user.wallets[0].adminkey"></span>"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

View file

@ -5,13 +5,7 @@
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div class="text-center"> <div class="text-center">
<a class="text-secondary" href="lightning:{{ lnurl }}"> <a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md"> <lnbits-qrcode value="lightning:{{ lnurl }}"></lnbits-qrcode>
<qrcode
value="lightning:{{ lnurl }}"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a> </a>
</div> </div>
<div class="row q-mt-lg q-gutter-sm"> <div class="row q-mt-lg q-gutter-sm">
@ -44,11 +38,9 @@
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) window.app = Vue.createApp({
new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin] mixins: [window.windowMixin]
}) })
</script> </script>
{% endblock %} {% endblock %}

View file

@ -25,11 +25,10 @@
<q-table <q-table
dense dense
flat flat
:data="payLinks" :rows="payLinks"
row-key="id" row-key="id"
:pagination.sync="payLinksTable.pagination" v-model:pagination="payLinksTable.pagination"
> >
{% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr class="text-left" :props="props"> <q-tr class="text-left" :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
@ -53,6 +52,7 @@
type="a" type="a"
:href="props.row.pay_url" :href="props.row.pay_url"
target="_blank" target="_blank"
class="q-ml-sm"
><q-tooltip>Shareable Page</q-tooltip></q-btn ><q-tooltip>Shareable Page</q-tooltip></q-btn
> >
<q-btn <q-btn
@ -61,26 +61,33 @@
size="xs" size="xs"
icon="visibility" icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
class="q-ml-sm"
@click="openQrCodeDialog(props.row.id)" @click="openQrCodeDialog(props.row.id)"
><q-tooltip>View Link</q-tooltip></q-btn ><q-tooltip>View Link</q-tooltip></q-btn
> >
</q-td> </q-td>
<q-td auto-width>{{ props.row.description }}</q-td> <q-td auto-width v-text="props.row.description"></q-td>
<q-td auto-width> <q-td auto-width>
<span v-if="props.row.min == props.row.max"> <span
{{ props.row.min }} v-if="props.row.min == props.row.max"
</span> v-text="props.row.min"
<span v-else>{{ props.row.min }} - {{ props.row.max }}</span> ></span>
<span
v-else
v-text="props.row.min + ' - ' + props.row.max"
></span>
</q-td> </q-td>
<q-td>{{ props.row.currency || 'sat' }}</q-td> <q-td v-text="props.row.currency || 'sat'"></q-td>
<q-td <q-td
auto-width auto-width
:class="(props.row.username) ? 'text-normal' : 'text-grey'" :class="(props.row.username) ? 'text-normal' : 'text-grey'"
>{{ props.row.username || 'None' }}</q-td v-text="props.row.username || 'None'"
> ></q-td>
<q-td> <q-td>
<q-icon v-if="props.row.webhook_url" size="14px" name="http"> <q-icon v-if="props.row.webhook_url" size="14px" name="http">
<q-tooltip>Webhook to {{ props.row.webhook_url }}</q-tooltip> <q-tooltip
>Webhook to <span v-text="props.row.webhook_url"></span
></q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon
v-if="props.row.success_text || props.row.success_url" v-if="props.row.success_text || props.row.success_url"
@ -88,9 +95,13 @@
name="call_to_action" name="call_to_action"
> >
<q-tooltip> <q-tooltip>
On success, show message '{{ props.row.success_text }}' On success, show message '<span
v-text="props.row.success_text"
></span
>'
<span v-if="props.row.success_url" <span v-if="props.row.success_url"
>and URL '{{ props.row.success_url }}'</span >and URL '<span v-text="props.row.success_url"></span
>'</span
> >
</q-tooltip> </q-tooltip>
</q-icon> </q-icon>
@ -100,7 +111,8 @@
name="insert_comment" name="insert_comment"
> >
<q-tooltip> <q-tooltip>
{{ props.row.comment_chars }}-char comment allowed <span v-text="props.row.comment_chars"></span>-char comment
allowed
</q-tooltip> </q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
@ -127,7 +139,6 @@
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
{% endraw %}
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -184,7 +195,7 @@
</div> </div>
<div class="col" style="margin-top: 10px"> <div class="col" style="margin-top: 10px">
<span class="label"> <span class="label">
&nbsp; @ {% raw %} {{ domain }} {% endraw %} &nbsp;@&nbsp;<span v-text="domain"></span>
</span> </span>
</div> </div>
</div> </div>
@ -357,34 +368,37 @@
<q-dialog v-model="qrCodeDialog.show" position="top"> <q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card"> <q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
{% raw %} <lnbits-qrcode
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> :value="'lightning:' + qrCodeDialog.data.lnurl"
<qrcode ></lnbits-qrcode>
:value="'lightning:' + qrCodeDialog.data.lnurl"
:options="{width: 800}"
class="rounded-borders"
>
</qrcode>
</q-responsive>
<p style="word-break: break-all"> <p style="word-break: break-all">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br /> <strong>ID:</strong> <span v-text="qrCodeDialog.data.id"></span><br />
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }}<br /> <strong>Amount:</strong> <span v-text="qrCodeDialog.data.amount"></span
><br />
<span v-if="qrCodeDialog.data.currency" <span v-if="qrCodeDialog.data.currency"
><strong>{{ qrCodeDialog.data.currency }} price:</strong> {{ ><strong
fiatRates[qrCodeDialog.data.currency] ? ><span v-text="qrCodeDialog.data.currency"></span> price:</strong
fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}<br >
<span
v-if="fiatRates[qrCodeDialog.data.currency]"
v-text="fiatRates[qrCodeDialog.data.currency] + 'sat'"
></span>
<span v-else>Loading...</span>
<br
/></span> /></span>
<strong>Accepts comments:</strong> {{ qrCodeDialog.data.comments }}<br /> <strong>Accepts comments:</strong>
<strong>Dispatches webhook to:</strong> {{ qrCodeDialog.data.webhook <span v-text="qrCodeDialog.data.comments"></span><br />
}}<br /> <strong>Dispatches webhook to:</strong>
<strong>On success:</strong> {{ qrCodeDialog.data.success }}<br /> <span v-text="qrCodeDialog.data.webhook"></span><br />
<strong>On success:</strong>
<span v-text="qrCodeDialog.data.success"></span><br />
<span v-if="qrCodeDialog.data.username"> <span v-if="qrCodeDialog.data.username">
<strong>Lightning Address: </strong> <strong>Lightning Address: </strong>
{{ qrCodeDialog.data.username }}@{{ domain }} <span v-text="qrCodeDialog.data.username+'@'+domain"></span>
<br /> <br />
</span> </span>
</p> </p>
{% endraw %}
<div class="row q-mt-lg q-gutter-sm"> <div class="row q-mt-lg q-gutter-sm">
<q-btn <q-btn
outline outline

View file

@ -1,7 +1,7 @@
{% extends "print.html" %} {% block page %} {% extends "print.html" %} {% block page %}
<div class="row justify-center"> <div class="row justify-center">
<div class="qr"> <div class="qr">
<qrcode value="lightning:{{ lnurl }}" :options="{width}"></qrcode> <lnbits-qrcode value="lightning:{{ lnurl }}"></lnbits-qrcode>
</div> </div>
</div> </div>
{% endblock %} {% block styles %} {% endblock %} {% block styles %}
@ -12,14 +12,12 @@
</style> </style>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) window.app = Vue.createApp({
new Vue({
el: '#vue', el: '#vue',
created: function () { created() {
window.print() window.print()
}, },
data: function () { data() {
return {width: window.innerWidth * 0.5} return {width: window.innerWidth * 0.5}
} }
}) })

View file

@ -1,7 +1,6 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
@ -17,13 +16,10 @@ def lnurlp_renderer():
return template_renderer(["lnurlp/templates"]) return template_renderer(["lnurlp/templates"])
templates = Jinja2Templates(directory="templates")
@lnurlp_generic_router.get("/", response_class=HTMLResponse) @lnurlp_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurlp_renderer().TemplateResponse( return lnurlp_renderer().TemplateResponse(
"lnurlp/index.html", {"request": request, "user": user.dict()} "lnurlp/index.html", {"request": request, "user": user.json()}
) )

View file

@ -8,7 +8,6 @@ from lnbits.core.crud import get_user, get_wallet
from lnbits.core.models import WalletTypeInfo from lnbits.core.models import WalletTypeInfo
from lnbits.decorators import ( from lnbits.decorators import (
check_admin, check_admin,
get_key_type,
require_admin_key, require_admin_key,
require_invoice_key, require_invoice_key,
) )
@ -41,13 +40,13 @@ async def api_list_currencies_available():
@lnurlp_api_router.get("/api/v1/links", status_code=HTTPStatus.OK) @lnurlp_api_router.get("/api/v1/links", status_code=HTTPStatus.OK)
async def api_links( async def api_links(
req: Request, req: Request,
wallet: WalletTypeInfo = Depends(get_key_type), key_info: WalletTypeInfo = Depends(require_invoice_key),
all_wallets: bool = Query(False), all_wallets: bool = Query(False),
): ):
wallet_ids = [wallet.wallet.id] wallet_ids = [key_info.wallet.id]
if all_wallets: if all_wallets:
user = await get_user(wallet.wallet.user) user = await get_user(key_info.wallet.user)
wallet_ids = user.wallet_ids if user else [] wallet_ids = user.wallet_ids if user else []
try: try:
@ -189,7 +188,10 @@ async def api_link_create_or_update(
if data.username and data.username != link.username: if data.username and data.username != link.username:
await check_username_exists(data.username) await check_username_exists(data.username)
link = await update_pay_link(**data.dict(), link_id=link_id) for k, v in data.dict().items():
setattr(link, k, v)
link = await update_pay_link(link)
else: else:
if data.username: if data.username:
await check_username_exists(data.username) await check_username_exists(data.username)
@ -201,7 +203,9 @@ async def api_link_create_or_update(
@lnurlp_api_router.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @lnurlp_api_router.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_delete(link_id: str, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_link_delete(
link_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
):
link = await get_pay_link(link_id) link = await get_pay_link(link_id)
if not link: if not link:
@ -210,9 +214,9 @@ async def api_link_delete(link_id: str, wallet: WalletTypeInfo = Depends(get_key
) )
# admins are allowed to delete paylinks beloging to regular users # admins are allowed to delete paylinks beloging to regular users
user = await get_user(wallet.wallet.user) user = await get_user(key_info.wallet.user)
admin_user = user.admin if user else False admin_user = user.admin if user else False
if not admin_user and link.wallet != wallet.wallet.id: if not admin_user and link.wallet != key_info.wallet.id:
raise HTTPException( raise HTTPException(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
) )

View file

@ -19,7 +19,8 @@ from pydantic import parse_obj_as
from .crud import ( from .crud import (
get_address_data, get_address_data,
get_or_create_lnurlp_settings, get_or_create_lnurlp_settings,
increment_pay_link, get_pay_link,
update_pay_link,
) )
lnurlp_lnurl_router = APIRouter() lnurlp_lnurl_router = APIRouter()
@ -36,11 +37,13 @@ async def api_lnurl_callback(
amount: int = Query(...), amount: int = Query(...),
webhook_data: str = Query(None), webhook_data: str = Query(None),
): ):
link = await increment_pay_link(link_id, served_pr=1) link = await get_pay_link(link_id)
if not link: if not link:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
) )
link.served_pr = 1
await update_pay_link(link)
mininum = link.min mininum = link.min
maximum = link.max maximum = link.max
@ -100,7 +103,7 @@ async def api_lnurl_callback(
# we take the zap request as the description instead of the metadata if present # we take the zap request as the description instead of the metadata if present
unhashed_description = nostr.encode() if nostr else link.lnurlpay_metadata.encode() unhashed_description = nostr.encode() if nostr else link.lnurlpay_metadata.encode()
_, payment_request = await create_invoice( payment = await create_invoice(
wallet_id=link.wallet, wallet_id=link.wallet,
amount=int(amount / 1000), amount=int(amount / 1000),
memo=link.description, memo=link.description,
@ -120,7 +123,7 @@ async def api_lnurl_callback(
message = parse_obj_as(Max144Str, link.success_text) message = parse_obj_as(Max144Str, link.success_text)
action = MessageAction(message=message) action = MessageAction(message=message)
invoice = parse_obj_as(LightningInvoice, LightningInvoice(payment_request)) invoice = parse_obj_as(LightningInvoice, LightningInvoice(payment.bolt11))
resp = LnurlPayActionResponse(pr=invoice, successAction=action, routes=[]) resp = LnurlPayActionResponse(pr=invoice, successAction=action, routes=[])
return resp.dict() return resp.dict()
@ -136,13 +139,15 @@ async def api_lnurl_callback(
name="lnurlp.api_lnurl_response", name="lnurlp.api_lnurl_response",
) )
async def api_lnurl_response( async def api_lnurl_response(
request: Request, link_id, webhook_data: Optional[str] = Query(None) request: Request, link_id: str, webhook_data: Optional[str] = Query(None)
): ):
link = await increment_pay_link(link_id, served_meta=1) link = await get_pay_link(link_id)
if not link: if not link:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
) )
link.served_meta = 1
await update_pay_link(link)
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id) url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)