fix: populate lnurl/lnurl_url in nostr-transport handlers (#1)
Some checks failed
lint.yml / fix: populate lnurl/lnurl_url in nostr-transport handlers (#1) (push) Failing after 0s
Some checks failed
lint.yml / fix: populate lnurl/lnurl_url in nostr-transport handlers (#1) (push) Failing after 0s
The HTTP views populate `link.lnurl` and `link.lnurl_url` from `request.url_for(...)`; the nostr-transport RPC handlers had no `Request` and so left both fields as `None`. Consumers (ATMs over nostr) were forced to provision a separate `LNBITS_HTTP_URL` env var and compose the LNURL callback themselves. Add `helpers.create_lnurl_from_baseurl(link)` that mirrors `create_lnurl` but composes the callback URL from `settings.lnbits_baseurl` instead, and thread it through the create/get/update/list RPC handlers via a `_populate_lnurl` shim so the response shape matches the HTTP path. Encoding errors are swallowed (fields stay `None`) so a misconfigured baseurl falls back to current behavior rather than failing the RPC. Closes #1.
This commit is contained in:
parent
82a6d4a894
commit
e9d911e593
2 changed files with 54 additions and 13 deletions
26
helpers.py
26
helpers.py
|
|
@ -1,4 +1,5 @@
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from lnbits.settings import settings
|
||||||
from lnurl import Lnurl
|
from lnurl import Lnurl
|
||||||
from lnurl import encode as lnurl_encode
|
from lnurl import encode as lnurl_encode
|
||||||
from shortuuid import uuid
|
from shortuuid import uuid
|
||||||
|
|
@ -26,3 +27,28 @@ def create_lnurl(link: WithdrawLink, req: Request) -> Lnurl:
|
||||||
f"Error creating LNURL with url: `{url!s}`, "
|
f"Error creating LNURL with url: `{url!s}`, "
|
||||||
"check your webserver proxy configuration."
|
"check your webserver proxy configuration."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def create_lnurl_from_baseurl(link: WithdrawLink) -> Lnurl:
|
||||||
|
"""
|
||||||
|
Same shape as `create_lnurl`, but composes the callback URL from
|
||||||
|
`settings.lnbits_baseurl` instead of a FastAPI `Request`. Used by
|
||||||
|
the nostr-transport RPC handlers, which have no HTTP request to
|
||||||
|
derive a base URL from.
|
||||||
|
"""
|
||||||
|
base = settings.lnbits_baseurl.rstrip("/")
|
||||||
|
if link.is_unique:
|
||||||
|
usescssv = link.usescsv.split(",")
|
||||||
|
tohash = link.id + link.unique_hash + usescssv[link.number]
|
||||||
|
multihash = uuid(name=tohash)
|
||||||
|
url = f"{base}/withdraw/api/v1/lnurl/{link.unique_hash}/{multihash}"
|
||||||
|
else:
|
||||||
|
url = f"{base}/withdraw/api/v1/lnurl/{link.unique_hash}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return lnurl_encode(url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Error creating LNURL with url: `{url!s}`, "
|
||||||
|
"check your `LNBITS_BASEURL` configuration."
|
||||||
|
) from e
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,11 @@ link_id:...})` enforce ownership without core importing this module.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from shortuuid import uuid
|
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets
|
from lnbits.core.crud.wallets import get_wallets
|
||||||
from lnbits.core.models import Account
|
from lnbits.core.models import Account
|
||||||
from lnbits.core.models.wallets import WalletTypeInfo
|
from lnbits.core.models.wallets import WalletTypeInfo
|
||||||
from lnbits.core.services.nostr_transport.models import NostrRpcRequest
|
from lnbits.core.services.nostr_transport.models import NostrRpcRequest
|
||||||
|
from shortuuid import uuid
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_withdraw_link,
|
create_withdraw_link,
|
||||||
|
|
@ -34,7 +33,8 @@ from .crud import (
|
||||||
get_withdraw_links,
|
get_withdraw_links,
|
||||||
update_withdraw_link,
|
update_withdraw_link,
|
||||||
)
|
)
|
||||||
from .models import CreateWithdrawData
|
from .helpers import create_lnurl_from_baseurl
|
||||||
|
from .models import CreateWithdrawData, WithdrawLink
|
||||||
|
|
||||||
|
|
||||||
async def handle_lnurlw_create_link(
|
async def handle_lnurlw_create_link(
|
||||||
|
|
@ -43,7 +43,7 @@ async def handle_lnurlw_create_link(
|
||||||
body = request.body or {}
|
body = request.body or {}
|
||||||
data = CreateWithdrawData(**body)
|
data = CreateWithdrawData(**body)
|
||||||
link = await create_withdraw_link(data, auth.wallet.id)
|
link = await create_withdraw_link(data, auth.wallet.id)
|
||||||
return _to_dict(link)
|
return _to_dict(_populate_lnurl(link))
|
||||||
|
|
||||||
|
|
||||||
async def handle_lnurlw_get_link(
|
async def handle_lnurlw_get_link(
|
||||||
|
|
@ -51,7 +51,7 @@ async def handle_lnurlw_get_link(
|
||||||
) -> dict:
|
) -> dict:
|
||||||
link_id = _require_id(request)
|
link_id = _require_id(request)
|
||||||
link = await _require_owned_link(link_id, auth.wallet.id)
|
link = await _require_owned_link(link_id, auth.wallet.id)
|
||||||
return _to_dict(link)
|
return _to_dict(_populate_lnurl(link))
|
||||||
|
|
||||||
|
|
||||||
async def handle_lnurlw_update_link(
|
async def handle_lnurlw_update_link(
|
||||||
|
|
@ -77,7 +77,7 @@ async def handle_lnurlw_update_link(
|
||||||
if k in _MUTABLE:
|
if k in _MUTABLE:
|
||||||
setattr(link, k, v)
|
setattr(link, k, v)
|
||||||
updated = await update_withdraw_link(link)
|
updated = await update_withdraw_link(link)
|
||||||
return _to_dict(updated)
|
return _to_dict(_populate_lnurl(updated))
|
||||||
|
|
||||||
|
|
||||||
async def handle_lnurlw_delete_link(
|
async def handle_lnurlw_delete_link(
|
||||||
|
|
@ -89,9 +89,7 @@ async def handle_lnurlw_delete_link(
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
async def handle_lnurlw_list_links(
|
async def handle_lnurlw_list_links(auth: Account, request: NostrRpcRequest) -> dict:
|
||||||
auth: Account, request: NostrRpcRequest
|
|
||||||
) -> dict:
|
|
||||||
"""List withdraw links across all wallets owned by the calling account.
|
"""List withdraw links across all wallets owned by the calling account.
|
||||||
Useful for ATMs to re-discover their links after a reconnect.
|
Useful for ATMs to re-discover their links after a reconnect.
|
||||||
|
|
||||||
|
|
@ -114,7 +112,7 @@ async def handle_lnurlw_list_links(
|
||||||
|
|
||||||
page = await get_withdraw_links(wallet_ids, limit, offset)
|
page = await get_withdraw_links(wallet_ids, limit, offset)
|
||||||
return {
|
return {
|
||||||
"data": [_to_dict(link) for link in page.data],
|
"data": [_to_dict(_populate_lnurl(link)) for link in page.data],
|
||||||
"total": page.total,
|
"total": page.total,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,12 +197,29 @@ async def _require_owned_link(link_id: str, wallet_id: str):
|
||||||
if link is None:
|
if link is None:
|
||||||
raise ValueError(f"withdraw: link not found: {link_id}")
|
raise ValueError(f"withdraw: link not found: {link_id}")
|
||||||
if link.wallet != wallet_id:
|
if link.wallet != wallet_id:
|
||||||
raise PermissionError(
|
raise PermissionError("withdraw: link does not belong to caller's wallet")
|
||||||
"withdraw: link does not belong to caller's wallet"
|
return link
|
||||||
)
|
|
||||||
|
|
||||||
|
def _populate_lnurl(link: WithdrawLink) -> WithdrawLink:
|
||||||
|
"""
|
||||||
|
Compose `lnurl` / `lnurl_url` from `settings.lnbits_baseurl` so
|
||||||
|
nostr-transport responses match the HTTP `views_api` shape, where
|
||||||
|
these fields are populated from `request.url_for(...)`. Without
|
||||||
|
this, consumers (ATMs, etc.) would have to re-derive the callback
|
||||||
|
URL themselves from a separately-provisioned LNbits HTTPS URL —
|
||||||
|
duplicating state LNbits already knows. See aiolabs/withdraw#1.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
encoded = create_lnurl_from_baseurl(link)
|
||||||
|
link.lnurl = str(encoded.bech32)
|
||||||
|
link.lnurl_url = str(encoded.url)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
|
||||||
def _to_dict(link) -> dict:
|
def _to_dict(link) -> dict:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
return json.loads(link.json())
|
return json.loads(link.json())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue