Date: Wed, 22 Nov 2023 11:40:22 +0100
Subject: [PATCH 04/68] feat: add extension-settings instead of environs (#28)
* feat: add extension-settings instead of environs
---
__init__.py | 18 ----
crud.py | 26 ++++-
helpers.py | 8 ++
lnurl.py | 11 +-
migrations.py | 13 +++
models.py | 15 +++
static/js/index.js | 16 ++-
tasks.py | 205 ++++++++++++++++++++----------------
templates/lnurlp/index.html | 5 +-
views_api.py | 31 +++++-
10 files changed, 224 insertions(+), 124 deletions(-)
create mode 100644 helpers.py
diff --git a/__init__.py b/__init__.py
index 5c0ccdb..d1310c2 100644
--- a/__init__.py
+++ b/__init__.py
@@ -9,24 +9,6 @@ from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
-from .nostr.event import Event
-from .nostr.key import PrivateKey, PublicKey
-
-
-def generate_keys(private_key: str = ""):
- if private_key.startswith("nsec"):
- return PrivateKey.from_nsec(private_key)
- elif private_key:
- return PrivateKey(bytes.fromhex(private_key))
- else:
- return PrivateKey() # generate random key
-
-
-env = Env()
-env.read_env()
-nostr_privatekey = generate_keys(env.str("LNURLP_ZAP_NOSTR_PRIVATEKEY", default=""))
-nostr_publickey: PublicKey = nostr_privatekey.public_key
-logger.debug(f"LNURLP Zaps Nostr pubkey: {nostr_publickey.hex()}")
db = Database("ext_lnurlp")
diff --git a/crud.py b/crud.py
index 377484d..f80cebd 100644
--- a/crud.py
+++ b/crud.py
@@ -1,12 +1,30 @@
from typing import List, Optional, Union
-from lnbits.helpers import urlsafe_short_hash
+from lnbits.helpers import urlsafe_short_hash, insert_query, update_query
-from . import db # , maindb
-from .models import CreatePayLinkData, PayLink
+from . import db
+from .models import CreatePayLinkData, LnurlpSettings, PayLink
+from .nostr.key import PrivateKey
from .services import check_lnaddress_format
-# from loguru import logger
+
+async def get_or_create_lnurlp_settings() -> LnurlpSettings:
+ row = await db.fetchone("SELECT * FROM lnurlp.settings LIMIT 1")
+ if row:
+ return LnurlpSettings(**row)
+ else:
+ settings = LnurlpSettings(nostr_private_key=PrivateKey().hex())
+ await db.execute(insert_query("lnurlp.settings", settings), (*settings.model_dump().values(),))
+ return settings
+
+
+async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings:
+ await db.execute(update_query("lnurlp.settings", settings, where=""), (*settings.model_dump().values(),))
+ return settings
+
+
+async def delete_lnurlp_settings() -> None:
+ await db.execute("DELETE FROM lnurlp.settings")
async def check_lnaddress_not_exists(username: str) -> bool:
diff --git a/helpers.py b/helpers.py
new file mode 100644
index 0000000..599699e
--- /dev/null
+++ b/helpers.py
@@ -0,0 +1,8 @@
+from .nostr.key import PrivateKey
+
+
+def parse_nostr_private_key(key: str) -> PrivateKey:
+ if key.startswith("nsec"):
+ return PrivateKey.from_nsec(key)
+ else:
+ return PrivateKey(bytes.fromhex(key))
diff --git a/lnurl.py b/lnurl.py
index d3703b1..57e8d90 100644
--- a/lnurl.py
+++ b/lnurl.py
@@ -1,3 +1,4 @@
+import json
from http import HTTPStatus
from urllib.parse import urlparse
@@ -8,8 +9,11 @@ from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import get_fiat_rate_satoshis
-from . import lnurlp_ext, nostr_publickey
-from .crud import increment_pay_link
+from . import lnurlp_ext
+from .crud import (
+ get_or_create_lnurlp_settings,
+ increment_pay_link,
+)
@lnurlp_ext.get(
@@ -145,6 +149,7 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False):
params["commentAllowed"] = link.comment_chars
if link.zaps:
+ settings = await get_or_create_lnurlp_settings()
params["allowsNostr"] = True
- params["nostrPubkey"] = nostr_publickey.hex()
+ params["nostrPubkey"] = settings.public_key
return params
diff --git a/migrations.py b/migrations.py
index 705b55f..4bb4b2e 100644
--- a/migrations.py
+++ b/migrations.py
@@ -160,3 +160,16 @@ async def m008_add_zap_enabled_column(db):
Add Nostr zaps to pay links
"""
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN zaps BOOLEAN;")
+
+
+async def m009_add_settings(db):
+ """
+ Add extension settings table
+ """
+ await db.execute(
+ """
+ CREATE TABLE lnurlp.settings (
+ nostr_private_key TEXT NOT NULL
+ );
+ """
+ )
diff --git a/models.py b/models.py
index e710558..864900a 100644
--- a/models.py
+++ b/models.py
@@ -10,6 +10,21 @@ from pydantic import BaseModel
from lnbits.lnurl import encode as lnurl_encode
+from .helpers import parse_nostr_private_key
+from .nostr.key import PrivateKey
+
+
+class LnurlpSettings(BaseModel):
+ nostr_private_key: str
+
+ @property
+ def private_key(self) -> PrivateKey:
+ return parse_nostr_private_key(self.nostr_private_key)
+
+ @property
+ def public_key(self) -> str:
+ return self.private_key.public_key.hex()
+
class CreatePayLinkData(BaseModel):
description: str
diff --git a/static/js/index.js b/static/js/index.js
index ffbb53a..5a44726 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -2,14 +2,14 @@
Vue.component(VueQrcode.name, VueQrcode)
-var locationPath = [
+const locationPath = [
window.location.protocol,
'//',
window.location.host,
window.location.pathname
].join('')
-var mapPayLink = obj => {
+const mapPayLink = obj => {
obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
@@ -24,8 +24,20 @@ var mapPayLink = obj => {
new Vue({
el: '#vue',
mixins: [windowMixin],
+ computed: {
+ endpoint: function() {
+ return `/lnurlp/api/v1/settings?usr=${this.g.user.id}`
+ }
+ },
data() {
return {
+ settings: [
+ {
+ "type": "str",
+ "description": "Nostr private key used to zap",
+ "name": "nostr_private_key",
+ }
+ ],
domain: window.location.host,
currencies: [],
fiatRates: {},
diff --git a/tasks.py b/tasks.py
index d44948d..e1abc9d 100644
--- a/tasks.py
+++ b/tasks.py
@@ -13,8 +13,8 @@ from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
-from . import nostr_privatekey
-from .crud import get_pay_link
+from .crud import get_or_create_lnurlp_settings, get_pay_link
+from .models import PayLink
from .nostr.event import Event
@@ -35,102 +35,59 @@ async def on_invoice_paid(payment: Payment):
# this webhook has already been sent
return
- pay_link = await get_pay_link(payment.extra.get("link", -1))
- if pay_link and pay_link.webhook_url:
- async with httpx.AsyncClient() as client:
- try:
- r: httpx.Response = await client.post(
- pay_link.webhook_url,
- json={
- "payment_hash": payment.payment_hash,
- "payment_request": payment.bolt11,
- "amount": payment.amount,
- "comment": payment.extra.get("comment"),
- "lnurlp": pay_link.id,
- "body": json.loads(pay_link.webhook_body)
- if pay_link.webhook_body
- else "",
- },
- headers=json.loads(pay_link.webhook_headers)
- if pay_link.webhook_headers
- else None,
- timeout=40,
- )
- await mark_webhook_sent(
- payment.payment_hash,
- r.status_code,
- r.is_success,
- r.reason_phrase,
- r.text,
- )
- except Exception as ex:
- logger.error(ex)
- await mark_webhook_sent(
- payment.payment_hash, -1, False, "Unexpected Error", str(ex)
- )
+ pay_link_id = payment.extra.get("link")
+ if not pay_link_id:
+ logger.error("Invoice paid. But no pay link id found.")
+ return
- # NIP-57
- # load the zap request
- nostr = payment.extra.get("nostr")
- if pay_link and pay_link.zaps and nostr:
- event_json = json.loads(nostr)
-
- def get_tag(event_json, tag):
- res = [
- event_tag[1:] for event_tag in event_json["tags"] if event_tag[0] == tag
- ]
- return res[0] if res else None
-
- tags = []
- for t in ["p", "e"]:
- tag = get_tag(event_json, t)
- if tag:
- tags.append([t, tag[0]])
- tags.append(["bolt11", payment.bolt11])
- tags.append(["description", nostr])
- zap_receipt = Event(
- kind=9735, tags=tags, content=payment.extra.get("comment") or ""
+ pay_link = await get_pay_link(pay_link_id)
+ if not pay_link:
+ logger.error(
+ f"Invoice paid. But Pay link `{pay_link_id}` not found."
)
- nostr_privatekey.sign_event(zap_receipt)
+ return
- def send_zap(relay):
- def send_event(_):
- logger.debug(f"Sending zap to {ws.url}")
- ws.send(zap_receipt.to_message())
- time.sleep(2)
- ws.close()
+ await send_webhook(payment, pay_link)
- ws = WebSocketApp(relay, on_open=send_event)
- wst = Thread(target=ws.run_forever, name=f"LNURL zap {relay}")
- wst.daemon = True
- wst.start()
- return ws, wst
+ if pay_link.zaps:
+ await send_zap(payment)
- # list of all websockets
- wss: List[WebSocketApp] = []
- # list of all threads for these websockets
- wsts: List[Thread] = []
- # # send zap via nostrclient
- # ws, wst = send_zap(f"wss://localhost:{settings.port}/nostrclient/api/v1/relay")
- # wss += [ws]
- # wsts += [wst]
+async def send_webhook(payment: Payment, pay_link: PayLink):
+ if not pay_link.webhook_url:
+ return
- # send zap receipt to relays in zap request
- relays = get_tag(event_json, "relays")
- if relays:
- if len(relays) > 50:
- relays = relays[:50]
- for r in relays:
- ws, wst = send_zap(r)
- wss += [ws]
- wsts += [wst]
-
- await asyncio.sleep(10)
- for ws, wst in zip(wss, wsts):
- logger.debug(f"Closing websocket {ws.url}")
- ws.close()
- wst.join()
+ async with httpx.AsyncClient() as client:
+ try:
+ r: httpx.Response = await client.post(
+ pay_link.webhook_url,
+ json={
+ "payment_hash": payment.payment_hash,
+ "payment_request": payment.bolt11,
+ "amount": payment.amount,
+ "comment": payment.extra.get("comment"),
+ "lnurlp": pay_link.id,
+ "body": json.loads(pay_link.webhook_body)
+ if pay_link.webhook_body
+ else "",
+ },
+ headers=json.loads(pay_link.webhook_headers)
+ if pay_link.webhook_headers
+ else None,
+ timeout=40,
+ )
+ await mark_webhook_sent(
+ payment.payment_hash,
+ r.status_code,
+ r.is_success,
+ r.reason_phrase,
+ r.text,
+ )
+ except Exception as exc:
+ logger.error(exc)
+ await mark_webhook_sent(
+ payment.payment_hash, -1, False, "Unexpected Error", str(exc)
+ )
async def mark_webhook_sent(
@@ -145,3 +102,69 @@ async def mark_webhook_sent(
"wh_response": text,
},
)
+
+
+# NIP-57 - load the zap request
+async def send_zap(payment: Payment):
+ nostr = payment.extra.get("nostr")
+ if not nostr:
+ return
+
+ event_json = json.loads(nostr)
+
+ def get_tag(event_json, tag):
+ res = [event_tag[1:] for event_tag in event_json["tags"] if event_tag[0] == tag]
+ return res[0] if res else None
+
+ tags = []
+ for t in ["p", "e"]:
+ tag = get_tag(event_json, t)
+ if tag:
+ tags.append([t, tag[0]])
+ tags.append(["bolt11", payment.bolt11])
+ tags.append(["description", nostr])
+ zap_receipt = Event(
+ kind=9735, tags=tags, content=payment.extra.get("comment") or ""
+ )
+
+ settings = await get_or_create_lnurlp_settings()
+ settings.private_key.sign_event(zap_receipt)
+
+ def send(relay):
+ def send_event(_):
+ logger.debug(f"Sending zap to {ws.url}")
+ ws.send(zap_receipt.to_message())
+ time.sleep(2)
+ ws.close()
+
+ ws = WebSocketApp(relay, on_open=send_event)
+ wst = Thread(target=ws.run_forever, name=f"LNURL zap {relay}")
+ wst.daemon = True
+ wst.start()
+ return ws, wst
+
+ # list of all websockets
+ wss: List[WebSocketApp] = []
+ # list of all threads for these websockets
+ wsts: List[Thread] = []
+
+ # # send zap via nostrclient
+ # ws, wst = send(f"wss://localhost:{settings.port}/nostrclient/api/v1/relay")
+ # wss += [ws]
+ # wsts += [wst]
+
+ # send zap receipt to relays in zap request
+ relays = get_tag(event_json, "relays")
+ if relays:
+ if len(relays) > 50:
+ relays = relays[:50]
+ for r in relays:
+ ws, wst = send(r)
+ wss += [ws]
+ wsts += [wst]
+
+ await asyncio.sleep(10)
+ for ws, wst in zip(wss, wsts):
+ logger.debug(f"Closing websocket {ws.url}")
+ ws.close()
+ wst.join()
diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html
index 914b0fc..7c448ed 100644
--- a/templates/lnurlp/index.html
+++ b/templates/lnurlp/index.html
@@ -4,9 +4,8 @@
- New pay link
+ New pay link
+
diff --git a/views_api.py b/views_api.py
index 0a2266e..3bf5bf9 100644
--- a/views_api.py
+++ b/views_api.py
@@ -13,14 +13,18 @@ from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
from . import lnurlp_ext, scheduled_tasks
from .crud import (
create_pay_link,
+ delete_lnurlp_settings,
delete_pay_link,
get_address_data,
+ get_or_create_lnurlp_settings,
get_pay_link,
get_pay_links,
+ update_lnurlp_settings,
update_pay_link,
)
+from .helpers import parse_nostr_private_key
from .lnurl import api_lnurl_response
-from .models import CreatePayLinkData
+from .models import CreatePayLinkData, LnurlpSettings
# redirected from /.well-known/lnurlp
@@ -178,8 +182,8 @@ async def api_check_fiat_rate(currency):
return {"rate": rate}
-@lnurlp_ext.delete("/api/v1", status_code=HTTPStatus.OK)
-async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
+@lnurlp_ext.delete("/api/v1", dependencies=[Depends(check_admin)])
+async def api_stop():
for t in scheduled_tasks:
try:
t.cancel()
@@ -187,3 +191,24 @@ async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
logger.warning(ex)
return {"success": True}
+
+
+@lnurlp_ext.get("/api/v1/settings", dependencies=[Depends(check_admin)])
+async def api_get_or_create_settings() -> LnurlpSettings:
+ return await get_or_create_lnurlp_settings()
+
+
+@lnurlp_ext.put("/api/v1/settings", dependencies=[Depends(check_admin)])
+async def api_update_settings(data: LnurlpSettings) -> LnurlpSettings:
+ try:
+ parse_nostr_private_key(data.nostr_private_key)
+ except Exception:
+ raise HTTPException(
+ detail="Invalid Nostr private key.", status_code=HTTPStatus.BAD_REQUEST
+ )
+ return await update_lnurlp_settings(data)
+
+
+@lnurlp_ext.delete("/api/v1/settings", dependencies=[Depends(check_admin)])
+async def api_delete_settings() -> None:
+ await delete_lnurlp_settings()
From 84179e8eeae269f4a1e074dc903112310a589a96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Wed, 22 Nov 2023 11:53:44 +0100
Subject: [PATCH 05/68] feat: pass data through lnurl for webhook (#31)
* feat: pass through data with lnurl
---
lnurl.py | 54 ++++++++++++++++++++++++++++---------
tasks.py | 2 ++
templates/lnurlp/index.html | 3 ---
3 files changed, 43 insertions(+), 16 deletions(-)
diff --git a/lnurl.py b/lnurl.py
index 57e8d90..c461595 100644
--- a/lnurl.py
+++ b/lnurl.py
@@ -22,9 +22,11 @@ from .crud import (
name="lnurlp.api_lnurl_lnaddr_callback",
)
async def api_lnurl_lnaddr_callback(
- request: Request, link_id, amount: int = Query(...)
+ request: Request, link_id, amount: int = Query(...), webhook_data: str = Query(None)
):
- return await api_lnurl_callback(request, link_id, amount, lnaddress=True)
+ return await api_lnurl_callback(
+ request, link_id, amount, webhook_data, lnaddress=True
+ )
@lnurlp_ext.get(
@@ -33,7 +35,11 @@ async def api_lnurl_lnaddr_callback(
name="lnurlp.api_lnurl_callback",
)
async def api_lnurl_callback(
- request: Request, link_id, amount: int = Query(...), lnaddress=False
+ request: Request,
+ link_id,
+ amount: int = Query(...),
+ webhook_data: str = Query(None),
+ lnaddress=False,
):
link = await increment_pay_link(link_id, served_pr=1)
if not link:
@@ -64,11 +70,15 @@ async def api_lnurl_callback(
comment = request.query_params.get("comment")
if len(comment or "") > link.comment_chars:
return LnurlErrorResponse(
- reason=f"Got a comment with {len(comment)} characters, but can only accept {link.comment_chars}"
+ reason=(
+ f"Got a comment with {len(comment or '')} characters, "
+ f"but can only accept {link.comment_chars}"
+ )
).dict()
if lnaddress:
- # for lnaddress, we have to set this otherwise the metadata won't have the identifier
+ # for lnaddress, we have to set this otherwise
+ # the metadata won't have the identifier
link.domain = urlparse(str(request.url)).netloc
extra = {
@@ -80,6 +90,9 @@ async def api_lnurl_callback(
if comment:
extra["comment"] = (comment,)
+ if webhook_data:
+ extra["webhook_data"] = webhook_data
+
# nip 57
nostr = request.query_params.get("nostr")
if nostr:
@@ -88,13 +101,14 @@ async def api_lnurl_callback(
if lnaddress and link.username and link.domain:
extra["lnaddress"] = f"{link.username}@{link.domain}"
+ # 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()
+
payment_hash, payment_request = await create_invoice(
wallet_id=link.wallet,
amount=int(amount / 1000),
memo=link.description,
- unhashed_description=nostr.encode()
- if nostr # we take the zap request as the description instead of the LNURL metadata if present
- else link.lnurlpay_metadata.encode(),
+ unhashed_description=unhashed_description,
extra=extra,
)
@@ -110,7 +124,7 @@ async def api_lnurl_callback(
@lnurlp_ext.get(
- "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL)
+ "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes
status_code=HTTPStatus.OK,
name="lnurlp.api_lnurl_response.deprecated",
)
@@ -119,7 +133,9 @@ async def api_lnurl_callback(
status_code=HTTPStatus.OK,
name="lnurlp.api_lnurl_response",
)
-async def api_lnurl_response(request: Request, link_id, lnaddress=False):
+async def api_lnurl_response(
+ request: Request, link_id, webhook_data: str = Query(None), lnaddress=False
+):
link = await increment_pay_link(link_id, served_meta=1)
if not link:
raise HTTPException(
@@ -129,13 +145,25 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False):
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
if lnaddress:
- # for lnaddress, we have to set this otherwise the metadata won't have the identifier
+ # for lnaddress, we have to set this otherwise
+ # the metadata won't have the identifier
link.domain = urlparse(str(request.url)).netloc
callback = str(
- request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id)
+ request.url_for(
+ "lnurlp.api_lnurl_lnaddr_callback",
+ link_id=link.id,
+ )
)
else:
- callback = str(request.url_for("lnurlp.api_lnurl_callback", link_id=link.id))
+ callback = str(
+ request.url_for(
+ "lnurlp.api_lnurl_callback",
+ link_id=link.id,
+ )
+ )
+
+ if webhook_data:
+ callback += f"?webhook_data={webhook_data}"
resp = LnurlPayResponse(
callback=callback,
diff --git a/tasks.py b/tasks.py
index e1abc9d..f21651a 100644
--- a/tasks.py
+++ b/tasks.py
@@ -40,6 +40,7 @@ async def on_invoice_paid(payment: Payment):
logger.error("Invoice paid. But no pay link id found.")
return
+
pay_link = await get_pay_link(pay_link_id)
if not pay_link:
logger.error(
@@ -66,6 +67,7 @@ async def send_webhook(payment: Payment, pay_link: PayLink):
"payment_request": payment.bolt11,
"amount": payment.amount,
"comment": payment.extra.get("comment"),
+ "webhook_data": payment.extra.get("webhook_data") or "",
"lnurlp": pay_link.id,
"body": json.loads(pay_link.webhook_body)
if pay_link.webhook_body
diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html
index 7c448ed..dcc6ea1 100644
--- a/templates/lnurlp/index.html
+++ b/templates/lnurlp/index.html
@@ -175,9 +175,6 @@
label="Lightning Address"
@input="formDialog.data.username = formDialog.data.username.toLowerCase()"
/>
-
- @ {% raw %} {{ domain }} {% endraw %}
-
From c0017095bf46b3576089d465aa42118dd4c1863f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Wed, 22 Nov 2023 11:55:20 +0100
Subject: [PATCH 06/68] min version to `0.11.2` (#33)
---
config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config.json b/config.json
index a69b673..77c14f9 100644
--- a/config.json
+++ b/config.json
@@ -3,5 +3,5 @@
"short_description": "Make reusable LNURL pay links",
"tile": "/lnurlp/static/image/lnurl-pay.png",
"contributors": ["arcbtc", "eillarra", "fiatjaf", "callebtc"],
- "min_lnbits_version": "0.11.0"
+ "min_lnbits_version": "0.11.2"
}
From 4017706c1880bc2d59d0c8fdaa04249fbd597b79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 24 Nov 2023 13:15:55 +0100
Subject: [PATCH 07/68] hotfix: lnaddress are broken (#34)
lnurl validation error.
```
pydantic.error_wrappers.ValidationError: 2 validation errors for LnurlPayResponse
callback
URL invalid, extra characters found after valid URL: ' extra={}' (type=value_error.url.extra; extra= extra={})
callback
URL invalid, extra characters found after valid URL: ' extra={}' (type=value_error.url.extra; extra= extra={})
2023-11-24 12:54:05.36 | ERROR | lnbits.app:exception_handler:467 | Exception: 2 validation errors for LnurlPayResponse
callback
URL invalid, extra characters found after valid URL: ' extra={}' (type=value_error.url.extra; extra= extra={})
callback
URL invalid, extra characters found after valid URL: ' extra={}' (type=value_error.url.extra; extra= extra={})
2023-11-24 12:54:05.36 | INFO | 52.57.61.135:0 - "GET /lnurlp/api/v1/well-known/test HTTP/1.1" 500
```
---
lnurl.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/lnurl.py b/lnurl.py
index c461595..485c779 100644
--- a/lnurl.py
+++ b/lnurl.py
@@ -39,7 +39,7 @@ async def api_lnurl_callback(
link_id,
amount: int = Query(...),
webhook_data: str = Query(None),
- lnaddress=False,
+ lnaddress: bool = False,
):
link = await increment_pay_link(link_id, served_pr=1)
if not link:
@@ -152,6 +152,7 @@ async def api_lnurl_response(
request.url_for(
"lnurlp.api_lnurl_lnaddr_callback",
link_id=link.id,
+ webhook_data=webhook_data,
)
)
else:
@@ -159,12 +160,10 @@ async def api_lnurl_response(
request.url_for(
"lnurlp.api_lnurl_callback",
link_id=link.id,
+ webhook_data=webhook_data,
)
)
- if webhook_data:
- callback += f"?webhook_data={webhook_data}"
-
resp = LnurlPayResponse(
callback=callback,
min_sendable=round(link.min * rate) * 1000, # type: ignore
From 9b5a86485e47fd3c8b231194f71f4ff076ac5536 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 24 Nov 2023 17:56:14 +0100
Subject: [PATCH 08/68] hotfix2: include_query_params
---
lnurl.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/lnurl.py b/lnurl.py
index 485c779..28e3901 100644
--- a/lnurl.py
+++ b/lnurl.py
@@ -152,16 +152,14 @@ async def api_lnurl_response(
request.url_for(
"lnurlp.api_lnurl_lnaddr_callback",
link_id=link.id,
- webhook_data=webhook_data,
- )
+ ).include_query_params(webhook_data=webhook_data)
)
else:
callback = str(
request.url_for(
"lnurlp.api_lnurl_callback",
link_id=link.id,
- webhook_data=webhook_data,
- )
+ ).include_query_params(webhook_data=webhook_data)
)
resp = LnurlPayResponse(
From adb1f3f52b2ed49ebc93f04b4cdb25357b248daf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 24 Nov 2023 18:44:44 +0100
Subject: [PATCH 09/68] hotfix3: fix for good
---
lnurl.py | 20 +++++++-------------
1 file changed, 7 insertions(+), 13 deletions(-)
diff --git a/lnurl.py b/lnurl.py
index 28e3901..1e9d033 100644
--- a/lnurl.py
+++ b/lnurl.py
@@ -148,22 +148,16 @@ async def api_lnurl_response(
# for lnaddress, we have to set this otherwise
# the metadata won't have the identifier
link.domain = urlparse(str(request.url)).netloc
- callback = str(
- request.url_for(
- "lnurlp.api_lnurl_lnaddr_callback",
- link_id=link.id,
- ).include_query_params(webhook_data=webhook_data)
- )
+ url = request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id)
else:
- callback = str(
- request.url_for(
- "lnurlp.api_lnurl_callback",
- link_id=link.id,
- ).include_query_params(webhook_data=webhook_data)
- )
+ url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)
+
+ if webhook_data:
+ url = url.include_query_params(webhook_data=webhook_data)
+
resp = LnurlPayResponse(
- callback=callback,
+ callback=str(url),
min_sendable=round(link.min * rate) * 1000, # type: ignore
max_sendable=round(link.max * rate) * 1000, # type: ignore
metadata=link.lnurlpay_metadata,
From 31158584ae0503e2caa2539185a0fe5606139299 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 24 Nov 2023 19:08:04 +0100
Subject: [PATCH 10/68] hotfix4: backwards compat of model_dump
---
crud.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/crud.py b/crud.py
index f80cebd..d656e80 100644
--- a/crud.py
+++ b/crud.py
@@ -14,12 +14,18 @@ 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.model_dump().values(),))
+ await db.execute(
+ insert_query("lnurlp.settings", settings),
+ (*settings.dict().values(),)
+ )
return settings
async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings:
- await db.execute(update_query("lnurlp.settings", settings, where=""), (*settings.model_dump().values(),))
+ await db.execute(
+ update_query("lnurlp.settings", settings, where=""),
+ (*settings.dict().values(),)
+ )
return settings
From 17556ff1b6efc7b22c51118429e89e53c34f4ece Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 24 Nov 2023 19:53:39 +0100
Subject: [PATCH 11/68] fix: lnaddress with query params (#35)
---
lnurl.py | 41 +++++++++--------------------------------
views_api.py | 2 +-
2 files changed, 10 insertions(+), 33 deletions(-)
diff --git a/lnurl.py b/lnurl.py
index 1e9d033..4445e79 100644
--- a/lnurl.py
+++ b/lnurl.py
@@ -1,6 +1,5 @@
-import json
from http import HTTPStatus
-from urllib.parse import urlparse
+from typing import Optional
from fastapi import Query, Request
from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
@@ -16,19 +15,6 @@ from .crud import (
)
-@lnurlp_ext.get(
- "/api/v1/lnurl/cb/lnaddr/{link_id}",
- status_code=HTTPStatus.OK,
- name="lnurlp.api_lnurl_lnaddr_callback",
-)
-async def api_lnurl_lnaddr_callback(
- request: Request, link_id, amount: int = Query(...), webhook_data: str = Query(None)
-):
- return await api_lnurl_callback(
- request, link_id, amount, webhook_data, lnaddress=True
- )
-
-
@lnurlp_ext.get(
"/api/v1/lnurl/cb/{link_id}",
status_code=HTTPStatus.OK,
@@ -36,10 +22,9 @@ async def api_lnurl_lnaddr_callback(
)
async def api_lnurl_callback(
request: Request,
- link_id,
+ link_id: str,
amount: int = Query(...),
webhook_data: str = Query(None),
- lnaddress: bool = False,
):
link = await increment_pay_link(link_id, served_pr=1)
if not link:
@@ -76,10 +61,9 @@ async def api_lnurl_callback(
)
).dict()
- if lnaddress:
- # for lnaddress, we have to set this otherwise
- # the metadata won't have the identifier
- link.domain = urlparse(str(request.url)).netloc
+ # for lnaddress, we have to set this otherwise
+ # the metadata won't have the identifier
+ link.domain = request.url.netloc
extra = {
"tag": "lnurlp",
@@ -98,7 +82,7 @@ async def api_lnurl_callback(
if nostr:
extra["nostr"] = nostr # put it here for later publishing in tasks.py
- if lnaddress and link.username and link.domain:
+ if link.username and link.domain:
extra["lnaddress"] = f"{link.username}@{link.domain}"
# we take the zap request as the description instead of the metadata if present
@@ -134,7 +118,7 @@ async def api_lnurl_callback(
name="lnurlp.api_lnurl_response",
)
async def api_lnurl_response(
- request: Request, link_id, webhook_data: str = Query(None), lnaddress=False
+ request: Request, link_id, webhook_data: Optional[str] = Query(None)
):
link = await increment_pay_link(link_id, served_meta=1)
if not link:
@@ -143,18 +127,11 @@ async def api_lnurl_response(
)
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
-
- if lnaddress:
- # for lnaddress, we have to set this otherwise
- # the metadata won't have the identifier
- link.domain = urlparse(str(request.url)).netloc
- url = request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id)
- else:
- url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)
-
+ url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)
if webhook_data:
url = url.include_query_params(webhook_data=webhook_data)
+ link.domain = request.url.netloc
resp = LnurlPayResponse(
callback=str(url),
diff --git a/views_api.py b/views_api.py
index 3bf5bf9..b3dd01d 100644
--- a/views_api.py
+++ b/views_api.py
@@ -32,7 +32,7 @@ from .models import CreatePayLinkData, LnurlpSettings
async def lnaddress(username: str, request: Request):
address_data = await get_address_data(username)
assert address_data, "User not found"
- return await api_lnurl_response(request, address_data.id, lnaddress=True)
+ return await api_lnurl_response(request, address_data.id, webhook_data=None)
@lnurlp_ext.get("/api/v1/currencies")
From a8dc4ac5df91904759b584d5a2307f0a1ad23509 Mon Sep 17 00:00:00 2001
From: ToniValac
Date: Fri, 23 Feb 2024 08:49:46 +0100
Subject: [PATCH 12/68] increasing LNAddress length limit
---
services.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services.py b/services.py
index 641ffce..493810d 100644
--- a/services.py
+++ b/services.py
@@ -3,7 +3,7 @@ import re
async def check_lnaddress_format(username: str) -> bool:
# check username complies with lnaddress specification
- if not re.match("^[a-z0-9-_.]{3,15}$", username):
- assert False, "Only letters a-z0-9-_. allowed, min 3 and max 15 characters!"
+ if not re.match("^[a-z0-9-_.]{3,64}$", username):
+ assert False, "Only letters a-z0-9-_. allowed, min 3 and max 64 characters!"
return
return True
From f7b4b8d2ff2f1dfc68c16845e3b1f25a553bd0ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Sun, 31 Mar 2024 10:19:16 +0200
Subject: [PATCH 13/68] fix: issue with empty success url
closes #528
---
views_api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/views_api.py b/views_api.py
index b3dd01d..fd16853 100644
--- a/views_api.py
+++ b/views_api.py
@@ -128,7 +128,7 @@ async def api_link_create_or_update(
data.min *= data.fiat_base_multiplier
data.max *= data.fiat_base_multiplier
- if data.success_url is not None and not data.success_url.startswith("https://"):
+ if data.success_url and data.success_url != "" and not data.success_url.startswith("https://"):
raise HTTPException(
detail="Success URL must be secure https://...",
status_code=HTTPStatus.BAD_REQUEST,
From 4de7c1a4c00e0253ae84918368d2fce9c2472cee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Sun, 31 Mar 2024 10:48:55 +0200
Subject: [PATCH 14/68] refactor: do not validate username inside crud
check username on api level not inside crud. adds better error reporting for the api user also in frontend
---
crud.py | 19 ++++---------------
views_api.py | 25 +++++++++++++++++++++++++
2 files changed, 29 insertions(+), 15 deletions(-)
diff --git a/crud.py b/crud.py
index d656e80..f30fd1b 100644
--- a/crud.py
+++ b/crud.py
@@ -5,7 +5,6 @@ from lnbits.helpers import urlsafe_short_hash, insert_query, update_query
from . import db
from .models import CreatePayLinkData, LnurlpSettings, PayLink
from .nostr.key import PrivateKey
-from .services import check_lnaddress_format
async def get_or_create_lnurlp_settings() -> LnurlpSettings:
@@ -33,21 +32,14 @@ async def delete_lnurlp_settings() -> None:
await db.execute("DELETE FROM lnurlp.settings")
-async def check_lnaddress_not_exists(username: str) -> bool:
- # check if lnaddress username exists in the database when creating a new entry
- row = await db.fetchall(
- "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,)
+async def get_pay_link_by_username(username: str) -> Optional[PayLink]:
+ row = await db.fetchone(
+ "SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,)
)
- if row:
- raise Exception("Username already exists. Try a different one.")
- else:
- return True
+ return PayLink.from_row(row) if row else None
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
- if data.username:
- await check_lnaddress_format(data.username)
- await check_lnaddress_not_exists(data.username)
link_id = urlsafe_short_hash()[:6]
@@ -128,9 +120,6 @@ async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
async def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
- if "username" in kwargs and len(kwargs["username"] or "") > 0:
- await check_lnaddress_format(kwargs["username"])
- await check_lnaddress_not_exists(kwargs["username"])
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
diff --git a/views_api.py b/views_api.py
index b3dd01d..aa6da78 100644
--- a/views_api.py
+++ b/views_api.py
@@ -18,10 +18,12 @@ from .crud import (
get_address_data,
get_or_create_lnurlp_settings,
get_pay_link,
+ get_pay_link_by_username,
get_pay_links,
update_lnurlp_settings,
update_pay_link,
)
+from .services import check_lnaddress_format
from .helpers import parse_nostr_private_key
from .lnurl import api_lnurl_response
from .models import CreatePayLinkData, LnurlpSettings
@@ -84,6 +86,14 @@ async def api_link_retrieve(
return {**link.dict(), **{"lnurl": link.lnurl(r)}}
+async def check_username_exists(username: str):
+ prev_link = await get_pay_link_by_username(username)
+ if prev_link:
+ raise HTTPException(
+ detail="Username already taken.",
+ status_code=HTTPStatus.BAD_REQUEST,
+ )
+
@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update(
@@ -134,6 +144,14 @@ async def api_link_create_or_update(
status_code=HTTPStatus.BAD_REQUEST,
)
+ if data.username:
+ try:
+ await check_lnaddress_format(data.username)
+ except AssertionError as ex:
+ raise HTTPException(
+ detail=f"Invalid username: {ex}", status_code=HTTPStatus.BAD_REQUEST
+ )
+
if link_id:
link = await get_pay_link(link_id)
@@ -147,9 +165,16 @@ async def api_link_create_or_update(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)
+ 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)
else:
+ if data.username:
+ await check_username_exists(data.username)
+
link = await create_pay_link(data, wallet_id=wallet.wallet.id)
+
assert link
return {**link.dict(), "lnurl": link.lnurl(request)}
From 65732f7aa0e36458f632686c43f96c6eeac70f05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 22 Mar 2024 16:57:22 +0100
Subject: [PATCH 15/68] chore: remove unused env and new create unique task
remove old scheduled tasks approach
update min version
Update __init__.py
Co-authored-by: Vlad Stan
---
__init__.py | 17 +++++++++--------
config.json | 2 +-
views_api.py | 14 +-------------
3 files changed, 11 insertions(+), 22 deletions(-)
diff --git a/__init__.py b/__init__.py
index d1310c2..a78367f 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,13 +1,11 @@
import asyncio
from typing import List
-from environs import Env
from fastapi import APIRouter
-from loguru import logger
from lnbits.db import Database
from lnbits.helpers import template_renderer
-from lnbits.tasks import catch_everything_and_restart
+from lnbits.tasks import create_permanent_unique_task
db = Database("ext_lnurlp")
@@ -26,22 +24,25 @@ lnurlp_redirect_paths = [
}
]
-scheduled_tasks: List[asyncio.Task] = []
lnurlp_ext: APIRouter = APIRouter(prefix="/lnurlp", tags=["lnurlp"])
-
def lnurlp_renderer():
return template_renderer(["lnurlp/templates"])
-
from .lnurl import * # noqa: F401,F403
from .tasks import wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
+scheduled_tasks: List[asyncio.Task] = []
+
+
+def lnurlp_stop():
+ for task in scheduled_tasks:
+ task.cancel()
+
def lnurlp_start():
- loop = asyncio.get_event_loop()
- task = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+ task = create_permanent_unique_task("lnurlp", wait_for_paid_invoices)
scheduled_tasks.append(task)
diff --git a/config.json b/config.json
index 77c14f9..b8edf91 100644
--- a/config.json
+++ b/config.json
@@ -3,5 +3,5 @@
"short_description": "Make reusable LNURL pay links",
"tile": "/lnurlp/static/image/lnurl-pay.png",
"contributors": ["arcbtc", "eillarra", "fiatjaf", "callebtc"],
- "min_lnbits_version": "0.11.2"
+ "min_lnbits_version": "0.12.4"
}
diff --git a/views_api.py b/views_api.py
index b3dd01d..32ff0db 100644
--- a/views_api.py
+++ b/views_api.py
@@ -1,5 +1,4 @@
import json
-from asyncio.log import logger
from http import HTTPStatus
from fastapi import Depends, Query, Request
@@ -10,7 +9,7 @@ from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, check_admin, get_key_type
from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
-from . import lnurlp_ext, scheduled_tasks
+from . import lnurlp_ext
from .crud import (
create_pay_link,
delete_lnurlp_settings,
@@ -182,17 +181,6 @@ async def api_check_fiat_rate(currency):
return {"rate": rate}
-@lnurlp_ext.delete("/api/v1", dependencies=[Depends(check_admin)])
-async def api_stop():
- for t in scheduled_tasks:
- try:
- t.cancel()
- except Exception as ex:
- logger.warning(ex)
-
- return {"success": True}
-
-
@lnurlp_ext.get("/api/v1/settings", dependencies=[Depends(check_admin)])
async def api_get_or_create_settings() -> LnurlpSettings:
return await get_or_create_lnurlp_settings()
From 9c518b8e6d0664b4d42ba0d35898eda3ff9faf46 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Mon, 1 Apr 2024 09:22:40 +0200
Subject: [PATCH 16/68] fix: change wallet for paylink
closes #26
---
crud.py | 4 ++--
models.py | 1 +
static/js/index.js | 3 +--
views_api.py | 39 ++++++++++++++++++++++++++-------------
4 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/crud.py b/crud.py
index f30fd1b..837b56a 100644
--- a/crud.py
+++ b/crud.py
@@ -39,7 +39,7 @@ async def get_pay_link_by_username(username: str) -> Optional[PayLink]:
return PayLink.from_row(row) if row else None
-async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
+async def create_pay_link(data: CreatePayLinkData) -> PayLink:
link_id = urlsafe_short_hash()[:6]
@@ -69,7 +69,7 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
""",
(
link_id,
- wallet_id,
+ data.wallet,
data.description,
data.min,
data.max,
diff --git a/models.py b/models.py
index 864900a..070655f 100644
--- a/models.py
+++ b/models.py
@@ -28,6 +28,7 @@ class LnurlpSettings(BaseModel):
class CreatePayLinkData(BaseModel):
description: str
+ wallet: Optional[str] = None
min: float = Query(1, ge=0.01)
max: float = Query(1, ge=0.01)
currency: str = Query(None)
diff --git a/static/js/index.js b/static/js/index.js
index 5a44726..5feb767 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -123,8 +123,7 @@ new Vue({
const wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
- var data = _.omit(this.formDialog.data, 'wallet')
-
+ const data = _.clone(this.formDialog.data)
if (this.formDialog.fixedAmount) data.max = data.min
if (data.currency === 'satoshis') data.currency = null
if (isNaN(parseInt(data.comment_chars))) data.comment_chars = 0
diff --git a/views_api.py b/views_api.py
index d4e0dcf..6da2899 100644
--- a/views_api.py
+++ b/views_api.py
@@ -1,12 +1,13 @@
import json
from http import HTTPStatus
+from typing import Optional
from fastapi import Depends, Query, Request
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
from starlette.exceptions import HTTPException
-from lnbits.core.crud import get_user
-from lnbits.decorators import WalletTypeInfo, check_admin, get_key_type
+from lnbits.core.crud import get_user, get_wallet
+from lnbits.decorators import WalletTypeInfo, check_admin, get_key_type, require_admin_key, require_invoice_key
from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
from . import lnurlp_ext
@@ -68,7 +69,7 @@ async def api_links(
@lnurlp_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_retrieve(
- r: Request, link_id, wallet: WalletTypeInfo = Depends(get_key_type)
+ r: Request, link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key)
):
link = await get_pay_link(link_id)
@@ -77,7 +78,9 @@ async def api_link_retrieve(
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
)
- if link.wallet != wallet.wallet.id:
+ link_wallet = await get_wallet(link.wallet)
+
+ if link_wallet.user != key_info.wallet.user:
raise HTTPException(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)
@@ -98,8 +101,8 @@ async def check_username_exists(username: str):
async def api_link_create_or_update(
data: CreatePayLinkData,
request: Request,
- link_id=None,
- wallet: WalletTypeInfo = Depends(get_key_type),
+ link_id: Optional[str] = None,
+ key_info: WalletTypeInfo = Depends(require_admin_key),
):
if data.min > data.max:
raise HTTPException(
@@ -151,6 +154,21 @@ async def api_link_create_or_update(
detail=f"Invalid username: {ex}", status_code=HTTPStatus.BAD_REQUEST
)
+ # if wallet is not provided, use the wallet of the key
+ if not data.wallet:
+ data.wallet = key_info.wallet.id
+
+ new_wallet = await get_wallet(data.wallet)
+ if not new_wallet:
+ raise HTTPException(
+ detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN
+ )
+
+ if new_wallet.user != key_info.wallet.user:
+ raise HTTPException(
+ detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
+ )
+
if link_id:
link = await get_pay_link(link_id)
@@ -159,11 +177,6 @@ async def api_link_create_or_update(
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
)
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
if data.username and data.username != link.username:
await check_username_exists(data.username)
@@ -172,14 +185,14 @@ async def api_link_create_or_update(
if data.username:
await check_username_exists(data.username)
- link = await create_pay_link(data, wallet_id=wallet.wallet.id)
+ link = await create_pay_link(data)
assert link
return {**link.dict(), "lnurl": link.lnurl(request)}
@lnurlp_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+async def api_link_delete(link_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
link = await get_pay_link(link_id)
if not link:
From 6f69f67cb8ed431945a3a55292871ed45b81449d Mon Sep 17 00:00:00 2001
From: Tiago Vasconcelos
Date: Wed, 24 Apr 2024 08:57:22 +0100
Subject: [PATCH 17/68] change extension name (#47)
Change display name of the extension
---
config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config.json b/config.json
index b8edf91..ca65ebb 100644
--- a/config.json
+++ b/config.json
@@ -1,5 +1,5 @@
{
- "name": "LNURLp",
+ "name": "Pay Links",
"short_description": "Make reusable LNURL pay links",
"tile": "/lnurlp/static/image/lnurl-pay.png",
"contributors": ["arcbtc", "eillarra", "fiatjaf", "callebtc"],
From b9fee9b93b06e873501c193f7019136d9d31ddf1 Mon Sep 17 00:00:00 2001
From: benarc
Date: Wed, 15 May 2024 22:24:58 +0100
Subject: [PATCH 18/68] Added extended description
---
config.json | 46 +++++++++++++++++++++++++++++++++++++++++++--
description.md | 2 ++
static/image/1.jpg | Bin 0 -> 32658 bytes
static/image/1.png | Bin 0 -> 79213 bytes
static/image/2.png | Bin 0 -> 47109 bytes
static/image/3.png | Bin 0 -> 26190 bytes
static/image/4.png | Bin 0 -> 44239 bytes
toc.md | 22 ++++++++++++++++++++++
8 files changed, 68 insertions(+), 2 deletions(-)
create mode 100644 description.md
create mode 100644 static/image/1.jpg
create mode 100644 static/image/1.png
create mode 100644 static/image/2.png
create mode 100644 static/image/3.png
create mode 100644 static/image/4.png
create mode 100644 toc.md
diff --git a/config.json b/config.json
index ca65ebb..4db7b18 100644
--- a/config.json
+++ b/config.json
@@ -2,6 +2,48 @@
"name": "Pay Links",
"short_description": "Make reusable LNURL pay links",
"tile": "/lnurlp/static/image/lnurl-pay.png",
- "contributors": ["arcbtc", "eillarra", "fiatjaf", "callebtc"],
- "min_lnbits_version": "0.12.4"
+ "min_lnbits_version": "0.12.4",
+ "contributors": [
+ {
+ "name": "arcbtc",
+ "uri": "https://github.com/arcbtc",
+ "role": "Contributor"
+ },
+ {
+ "name": "eillarra",
+ "uri": "https://github.com/eillarra",
+ "role": "Contributor"
+ },
+ {
+ "name": "fiatjaf",
+ "uri": "https://github.com/fiatjaf",
+ "role": "Contributor"
+ },
+ {
+ "name": "callebtc",
+ "uri": "https://github.com/callebtc",
+ "role": "Contributor"
+ }
+ ],
+ "images": [
+ {
+ "uri": "https://raw.githubusercontent.com/lnbits/lnurlp/main/static/1.jpg",
+ "link": "https://www.youtube.com/embed/WZpK4xfGcuY?si=_T3gCqKBU8yt6_bD"
+ },
+ {
+ "uri": "https://raw.githubusercontent.com/lnbits/lnurlp/main/static/1.png"
+ },
+ {
+ "uri": "https://raw.githubusercontent.com/lnbits/lnurlp/main/static/2.png"
+ },
+ {
+ "uri": "https://raw.githubusercontent.com/lnbits/lnurlp/main/static/3.png"
+ },
+ {
+ "uri": "https://raw.githubusercontent.com/lnbits/lnurlp/main/static/4.png"
+ }
+ ],
+ "description_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/description.md",
+ "terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/toc.md",
+ "license": "MIT"
}
diff --git a/description.md b/description.md
new file mode 100644
index 0000000..aa889cc
--- /dev/null
+++ b/description.md
@@ -0,0 +1,2 @@
+Create a static LNURLp or LNaddress people can use to pay.
+
diff --git a/static/image/1.jpg b/static/image/1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..816ee976de997e344b589dd5558543a887ee78be
GIT binary patch
literal 32658
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
literal 0
HcmV?d00001
diff --git a/static/image/1.png b/static/image/1.png
new file mode 100644
index 0000000000000000000000000000000000000000..fdc4dae2a3e85e55788183fa2f38f49f645d7e57
GIT binary patch
literal 79213
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%0AXsD{XhR4^Io`__d#Psvg;>C-v
zMM}I^}YO?rzii
ze>GF5O>5(sGN-fb(c{Od+i&+vvlz`j>zF&^?6V2rBB!g1>*=#+Y}-V|#ZSNg{`y*U
z{?!W$okeA3*Petdm2=#z=TvAl_tWpcT>Sj}IozeZEt?V!HXTSW`Fb^+UF-=TKmYX0
zFQ=S;e)&Kn^VI3n#VzD_-(8n{yzk|&*X!SxGUl%|Ry_|&oe{eqZo8d#c}wPH70>Ij
z<#YSwY*&fLRXA#DYMwfMn!8G#m#ul$tXZH{v#i`=8a6gDD_5?pdeF#jl5%3g<9+gz
zOsc=-+$}iFyD4hzrwSXRnLf7vey>}*Hng^O@3XVB)mL9#)ziZhTle#+V){ac7yI>A
zNU}_N^4*(N^tDOm1)Hl|h@ztB-Q$(gNHH0sGvzIj2lh5;KYDTnP
zjW$Vt5*~6(U{2bJ41<*mlGaAe{W@!I?etT#x%{Nk+f}&G+y2#I2uK^-A;XTFcn)ACE~ZzMg8uUb-Gs
z#=5w$Tob*qHG6v6`FU%bnwlmYpVL_eDv~V<{v2TDXW6@M#fl5U{x*V}-kh4M&3wJ@
zN@cI@?>C!4V^90OEY&}mv~k6*T~Ye`ek7gv^XG2m-ngsRuV>%cQTXdYGrvkH-+}n@
zyQPj#>;62J|L(E1P+TXYDPdk}%>D*lFt(lkAJm;?5DyAE?BzxUXH&s