feat: lnurlw_list_links + lnurlw_unique_hashes transport RPCs
Some checks failed
lint.yml / feat: lnurlw_list_links + lnurlw_unique_hashes transport RPCs (push) Failing after 0s
Some checks failed
lint.yml / feat: lnurlw_list_links + lnurlw_unique_hashes transport RPCs (push) Failing after 0s
Two additions surface withdraw-extension capabilities the ATM use case in aiolabs/lamassu-next (issues #24, #25) needs but couldn't reach over the nostr transport before: ## lnurlw_list_links (AUTH_ACCOUNT) Enumerate withdraw links across all wallets owned by the calling account, with `limit`/`offset` pagination matching the existing HTTP `/api/v1/links`. Lets an ATM (or any client) re-discover its links after a reconnect without having to keep its own index. If `request.wallet_id` is supplied and matches one of the account's wallets, narrows the listing to just that wallet — mirrors lnurlp's list semantics. Returns `{data: [...links], total: <int>}`. ## lnurlw_unique_hashes (AUTH_WALLET) For an `is_unique=True` link, return the per-use `id_unique_hash` values derived from each unredeemed slot in `link.usescsv`. Mirrors the formula in `helpers.py:create_lnurl:13`: id_unique_hash = shortuuid.uuid(name=link.id + link.unique_hash + index) Without this RPC an ATM that wants to generate distinct QR codes per use (lamassu-next #25) had to reimplement the derivation client-side — fragile if the extension's hash format ever changes upstream. With this RPC the ATM asks the server for the canonical list of unredeemed hashes; each one becomes the trailing path component of `/withdraw/api/v1/lnurl/<unique_hash>/<id_unique_hash>`. `is_unique=False` links return an empty `unredeemed_hashes` list; the base `unique_hash` alone identifies the callback path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
95ed17754d
commit
82a6d4a894
2 changed files with 96 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ def withdraw_start() -> None:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from lnbits.core.services.nostr_transport.dispatcher import (
|
from lnbits.core.services.nostr_transport.dispatcher import (
|
||||||
|
AUTH_ACCOUNT,
|
||||||
AUTH_WALLET,
|
AUTH_WALLET,
|
||||||
register_rpc,
|
register_rpc,
|
||||||
)
|
)
|
||||||
|
|
@ -46,12 +47,16 @@ def withdraw_start() -> None:
|
||||||
handle_lnurlw_create_link,
|
handle_lnurlw_create_link,
|
||||||
handle_lnurlw_delete_link,
|
handle_lnurlw_delete_link,
|
||||||
handle_lnurlw_get_link,
|
handle_lnurlw_get_link,
|
||||||
|
handle_lnurlw_list_links,
|
||||||
|
handle_lnurlw_unique_hashes,
|
||||||
handle_lnurlw_update_link,
|
handle_lnurlw_update_link,
|
||||||
resolve_withdraw_owner,
|
resolve_withdraw_owner,
|
||||||
)
|
)
|
||||||
|
|
||||||
register_rpc("lnurlw_create_link", handle_lnurlw_create_link, AUTH_WALLET)
|
register_rpc("lnurlw_create_link", handle_lnurlw_create_link, AUTH_WALLET)
|
||||||
register_rpc("lnurlw_get_link", handle_lnurlw_get_link, AUTH_WALLET)
|
register_rpc("lnurlw_get_link", handle_lnurlw_get_link, AUTH_WALLET)
|
||||||
|
register_rpc("lnurlw_list_links", handle_lnurlw_list_links, AUTH_ACCOUNT)
|
||||||
|
register_rpc("lnurlw_unique_hashes", handle_lnurlw_unique_hashes, AUTH_WALLET)
|
||||||
register_rpc("lnurlw_update_link", handle_lnurlw_update_link, AUTH_WALLET)
|
register_rpc("lnurlw_update_link", handle_lnurlw_update_link, AUTH_WALLET)
|
||||||
register_rpc("lnurlw_delete_link", handle_lnurlw_delete_link, AUTH_WALLET)
|
register_rpc("lnurlw_delete_link", handle_lnurlw_delete_link, AUTH_WALLET)
|
||||||
register_link_owner_resolver(
|
register_link_owner_resolver(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ 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.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
|
||||||
|
|
||||||
|
|
@ -27,6 +31,7 @@ from .crud import (
|
||||||
create_withdraw_link,
|
create_withdraw_link,
|
||||||
delete_withdraw_link,
|
delete_withdraw_link,
|
||||||
get_withdraw_link,
|
get_withdraw_link,
|
||||||
|
get_withdraw_links,
|
||||||
update_withdraw_link,
|
update_withdraw_link,
|
||||||
)
|
)
|
||||||
from .models import CreateWithdrawData
|
from .models import CreateWithdrawData
|
||||||
|
|
@ -84,6 +89,92 @@ async def handle_lnurlw_delete_link(
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Body fields:
|
||||||
|
- limit: int (0 means no limit; default 0)
|
||||||
|
- offset: int (default 0)
|
||||||
|
If `request.wallet_id` is set and is one of the caller's wallets,
|
||||||
|
narrow to just that wallet.
|
||||||
|
"""
|
||||||
|
body = request.body or {}
|
||||||
|
limit = int(body.get("limit") or 0)
|
||||||
|
offset = int(body.get("offset") or 0)
|
||||||
|
|
||||||
|
wallets = await get_wallets(auth.id)
|
||||||
|
wallet_ids = [w.id for w in wallets]
|
||||||
|
if not wallet_ids:
|
||||||
|
return {"data": [], "total": 0}
|
||||||
|
if request.wallet_id and request.wallet_id in wallet_ids:
|
||||||
|
wallet_ids = [request.wallet_id]
|
||||||
|
|
||||||
|
page = await get_withdraw_links(wallet_ids, limit, offset)
|
||||||
|
return {
|
||||||
|
"data": [_to_dict(link) for link in page.data],
|
||||||
|
"total": page.total,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lnurlw_unique_hashes(
|
||||||
|
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
For a `is_unique=True` link, return the per-use `id_unique_hash`
|
||||||
|
values that the ATM uses to generate distinct QR codes — one per
|
||||||
|
unredeemed slot. Mirrors the formula in `helpers.py:create_lnurl`
|
||||||
|
exactly so an ATM never has to re-implement the derivation:
|
||||||
|
|
||||||
|
id_unique_hash = shortuuid.uuid(name=link.id + link.unique_hash + index)
|
||||||
|
|
||||||
|
`link.usescsv` is the canonical list of *unredeemed* slot indexes;
|
||||||
|
after a customer claims a slot it gets removed there (see
|
||||||
|
`crud.remove_unique_withdraw_link`). The hashes returned here are
|
||||||
|
therefore exactly the ones still claimable.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"link_id": str,
|
||||||
|
"unique_hash": str, # base hash
|
||||||
|
"is_unique": bool,
|
||||||
|
"unredeemed_hashes": [ # one entry per remaining slot
|
||||||
|
{"index": str, "id_unique_hash": str}, ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
For `is_unique=False` links the list is empty and `unique_hash`
|
||||||
|
alone identifies the callback path
|
||||||
|
(`/withdraw/api/v1/lnurl/<unique_hash>`). For `is_unique=True`
|
||||||
|
each callback path is
|
||||||
|
`/withdraw/api/v1/lnurl/<unique_hash>/<id_unique_hash>`.
|
||||||
|
"""
|
||||||
|
link_id = _require_id(request)
|
||||||
|
link = await _require_owned_link(link_id, auth.wallet.id)
|
||||||
|
|
||||||
|
unredeemed = []
|
||||||
|
if link.is_unique:
|
||||||
|
# usescsv is comma-separated; split and skip empties (after the
|
||||||
|
# last slot is consumed it becomes the empty string).
|
||||||
|
for index_str in [s for s in link.usescsv.split(",") if s.strip()]:
|
||||||
|
tohash = link.id + link.unique_hash + index_str
|
||||||
|
unredeemed.append(
|
||||||
|
{
|
||||||
|
"index": index_str.strip(),
|
||||||
|
"id_unique_hash": uuid(name=tohash),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"link_id": link.id,
|
||||||
|
"unique_hash": link.unique_hash,
|
||||||
|
"is_unique": link.is_unique,
|
||||||
|
"unredeemed_hashes": unredeemed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def resolve_withdraw_owner(link_id: str) -> str | None:
|
async def resolve_withdraw_owner(link_id: str) -> str | None:
|
||||||
"""For the core subscription module: link_id -> wallet_id (or None)."""
|
"""For the core subscription module: link_id -> wallet_id (or None)."""
|
||||||
link = await get_withdraw_link(link_id)
|
link = await get_withdraw_link(link_id)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue