Compare commits

...

9 commits

Author SHA1 Message Date
Vlad Stan
b97be0d792 fix: update statement 2024-10-04 12:21:05 +03:00
Vlad Stan
0d2b2cdd79 fix: some extra checks 2024-10-04 12:12:59 +03:00
Vlad Stan
8d2008074e fix: q-table 2024-10-04 11:19:25 +03:00
Vlad Stan
60b3c4d46d fix: add domain column 2024-10-04 11:14:24 +03:00
Vlad Stan
09331f6bbc fix: better date fromat 2024-10-04 11:14:13 +03:00
Vlad Stan
4578a4e746 fix: date format 2024-10-04 11:12:08 +03:00
Vlad Stan
1bae6340fa fix: ui issues 2024-10-04 10:50:50 +03:00
Vlad Stan
fb5a357eb8 fix: old api 2024-10-04 10:41:04 +03:00
dni ⚡
fe6cbe4e2d
feat: update to lnbits 1.0.0 2024-09-07 21:21:37 +02:00
15 changed files with 933 additions and 907 deletions

View file

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

123
crud.py
View file

@ -1,7 +1,7 @@
from typing import List, Optional, Union
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 .nostr.key import PrivateKey
@ -15,17 +15,12 @@ async def get_or_create_lnurlp_settings() -> LnurlpSettings:
return LnurlpSettings(**row)
else:
settings = LnurlpSettings(nostr_private_key=PrivateKey().hex())
await db.execute(
insert_query("lnurlp.settings", settings), (*settings.dict().values(),)
)
await db.insert("lnurlp.settings", settings)
return settings
async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings:
await db.execute(
update_query("lnurlp.settings", settings, where=""),
(*settings.dict().values(),),
)
await db.update("lnurlp.settings", settings)
return settings
@ -35,109 +30,73 @@ async def delete_lnurlp_settings() -> None:
async def get_pay_link_by_username(username: str) -> Optional[PayLink]:
row = await db.fetchone(
"SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,)
"SELECT * FROM lnurlp.pay_links WHERE username = :username",
{"username": username},
)
return PayLink.from_row(row) if row else None
return PayLink(**row) if row else None
async def create_pay_link(data: CreatePayLinkData) -> PayLink:
link_id = urlsafe_short_hash()[:6]
result = await db.execute(
"""
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
assert data.wallet, "Wallet is required"
link = PayLink(
id=link_id,
wallet=data.wallet,
description=data.description,
min=data.min,
max=data.max,
served_meta=0,
served_pr=0,
username=data.username,
zaps=data.zaps,
domain=None,
webhook_url=data.webhook_url,
webhook_headers=data.webhook_headers,
webhook_body=data.webhook_body,
success_text=data.success_text,
success_url=data.success_url,
currency=data.currency,
comment_chars=data.comment_chars,
fiat_base_multiplier=data.fiat_base_multiplier,
)
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
link_id,
data.wallet,
data.description,
data.min,
data.max,
data.webhook_url,
data.webhook_headers,
data.webhook_body,
data.success_text,
data.success_url,
data.comment_chars,
data.currency,
data.fiat_base_multiplier,
data.username,
data.zaps,
),
)
assert result
link = await get_pay_link(link_id)
assert link, "Newly created link couldn't be retrieved"
await db.insert("lnurlp.pay_links", link)
return link
async def get_address_data(username: str) -> Optional[PayLink]:
row = await db.fetchone(
"SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,)
"SELECT * FROM lnurlp.pay_links WHERE username = :username",
{"username": username},
)
return PayLink.from_row(row) if row else None
return PayLink(**row) if row else None
async def get_pay_link(link_id: str) -> Optional[PayLink]:
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
row = await db.fetchone(
"SELECT * FROM lnurlp.pay_links WHERE id = :id", {"id": link_id}
)
return PayLink(**row) if row else None
async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
rows = await db.fetchall(
f"""
SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q})
ORDER BY Id
""",
(*wallet_ids,),
f"SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q}) ORDER BY Id"
)
return [PayLink.from_row(row) for row in rows]
return [PayLink(**row) for row in rows]
async def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
q = ", ".join([f"{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 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 update_pay_link(link: PayLink) -> PayLink:
await db.update("lnurlp.pay_links", link)
return link
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
from sqlite3 import Row
from typing import Optional
from fastapi import Request
from fastapi.param_functions import Query
from fastapi import Query, Request
from lnurl import encode as lnurl_encode
from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
@ -61,14 +59,6 @@ class PayLink(BaseModel):
max: float
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:
url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id)
url_str = str(url)

1588
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]
python = "^3.10 | ^3.9"
lnbits = "*"
lnbits = {version = "*", allow-prereleases = true}
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"

View file

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

View file

@ -27,6 +27,9 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment):
if not payment.extra:
return
if payment.extra.get("tag") != "lnurlp":
return
@ -108,7 +111,7 @@ async def mark_webhook_sent(
# NIP-57 - load the zap request
async def send_zap(payment: Payment):
nostr = payment.extra.get("nostr")
nostr = payment.extra.get("nostr") if payment.extra else None
if not nostr:
return

View file

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

View file

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

View file

@ -25,9 +25,9 @@
<q-table
dense
flat
:data="payLinks"
:rows="payLinks"
row-key="id"
:pagination.sync="payLinksTable.pagination"
v-model:pagination="payLinksTable.pagination"
>
{% raw %}
<template v-slot:header="props">
@ -53,6 +53,7 @@
type="a"
:href="props.row.pay_url"
target="_blank"
class="q-ml-sm"
><q-tooltip>Shareable Page</q-tooltip></q-btn
>
<q-btn
@ -61,6 +62,7 @@
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
class="q-ml-sm"
@click="openQrCodeDialog(props.row.id)"
><q-tooltip>View Link</q-tooltip></q-btn
>
@ -359,12 +361,8 @@
<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="'lightning:' + qrCodeDialog.data.lnurl"
:options="{width: 800}"
class="rounded-borders"
>
</qrcode>
<lnbits-qrcode :value="'lightning:' + qrCodeDialog.data.lnurl">
</lnbits-qrcode>
</q-responsive>
<p style="word-break: break-all">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />

View file

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

View file

@ -1,7 +1,6 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
@ -17,13 +16,10 @@ def lnurlp_renderer():
return template_renderer(["lnurlp/templates"])
templates = Jinja2Templates(directory="templates")
@lnurlp_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
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.decorators import (
check_admin,
get_key_type,
require_admin_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)
async def api_links(
req: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
key_info: WalletTypeInfo = Depends(require_invoice_key),
all_wallets: bool = Query(False),
):
wallet_ids = [wallet.wallet.id]
wallet_ids = [key_info.wallet.id]
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 []
try:
@ -189,7 +188,10 @@ async def api_link_create_or_update(
if data.username and data.username != link.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:
if 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)
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)
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
user = await get_user(wallet.wallet.user)
user = await get_user(key_info.wallet.user)
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(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)

View file

@ -19,7 +19,8 @@ from pydantic import parse_obj_as
from .crud import (
get_address_data,
get_or_create_lnurlp_settings,
increment_pay_link,
get_pay_link,
update_pay_link,
)
lnurlp_lnurl_router = APIRouter()
@ -36,11 +37,13 @@ async def api_lnurl_callback(
amount: int = Query(...),
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:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
)
link.served_pr = 1
await update_pay_link(link)
mininum = link.min
maximum = link.max
@ -136,13 +139,15 @@ async def api_lnurl_callback(
name="lnurlp.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:
raise HTTPException(
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
url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)