diff --git a/helpers.py b/helpers.py index 51eb948..31efcff 100644 --- a/helpers.py +++ b/helpers.py @@ -1,4 +1,5 @@ from fastapi import Request +from lnbits.settings import settings from lnurl import Lnurl from lnurl import encode as lnurl_encode from shortuuid import uuid @@ -26,3 +27,28 @@ def create_lnurl(link: WithdrawLink, req: Request) -> Lnurl: f"Error creating LNURL with url: `{url!s}`, " "check your webserver proxy configuration." ) 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 diff --git a/transport_rpcs.py b/transport_rpcs.py index cfd8f63..193d7c3 100644 --- a/transport_rpcs.py +++ b/transport_rpcs.py @@ -20,12 +20,11 @@ link_id:...})` enforce ownership without core importing this module. from __future__ import annotations -from shortuuid import uuid - from lnbits.core.crud.wallets import get_wallets from lnbits.core.models import Account from lnbits.core.models.wallets import WalletTypeInfo from lnbits.core.services.nostr_transport.models import NostrRpcRequest +from shortuuid import uuid from .crud import ( create_withdraw_link, @@ -34,7 +33,8 @@ from .crud import ( get_withdraw_links, 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( @@ -43,7 +43,7 @@ async def handle_lnurlw_create_link( body = request.body or {} data = CreateWithdrawData(**body) 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( @@ -51,7 +51,7 @@ async def handle_lnurlw_get_link( ) -> dict: link_id = _require_id(request) 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( @@ -77,7 +77,7 @@ async def handle_lnurlw_update_link( if k in _MUTABLE: setattr(link, k, v) updated = await update_withdraw_link(link) - return _to_dict(updated) + return _to_dict(_populate_lnurl(updated)) async def handle_lnurlw_delete_link( @@ -89,9 +89,7 @@ async def handle_lnurlw_delete_link( return {"ok": True} -async def handle_lnurlw_list_links( - auth: Account, request: NostrRpcRequest -) -> dict: +async def handle_lnurlw_list_links(auth: Account, request: NostrRpcRequest) -> dict: """List withdraw links across all wallets owned by the calling account. 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) return { - "data": [_to_dict(link) for link in page.data], + "data": [_to_dict(_populate_lnurl(link)) for link in page.data], "total": page.total, } @@ -199,12 +197,29 @@ async def _require_owned_link(link_id: str, wallet_id: str): if link is None: raise ValueError(f"withdraw: link not found: {link_id}") if link.wallet != wallet_id: - raise PermissionError( - "withdraw: link does not belong to caller's wallet" - ) + raise PermissionError("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 def _to_dict(link) -> dict: import json + return json.loads(link.json())