Skip to content

Nostr zaps: LNURL server-side (allowsNostr, nostrPubkey, zap-request → description) [NIP-57] #30

Description

@Chemaclass

Context

Nostr zaps are defined by NIP-57 (built on LUD-06, which this repo already implements). LUD-16 is only the lightning-address resolution layer, not the zap spec. This issue covers the dependency-free server-side surface: advertise zap support, accept a zap request, and turn it into the invoice description. Signature verification + zap-receipt publishing (which need crypto + relay/WebSocket + a payment-settled hook) are the follow-up issue.

Relevant code:

  • src/Invoice/Application/CallbackUrl.php — pay-params response (add the two fields here).
  • src/Invoice/Infrastructure/Controller/InvoiceController.php — reads only amount today; also read nostr.
  • src/Invoice/InvoiceFacade.phpsrc/Invoice/Application/InvoiceGenerator.php — thread the zap request through.
  • src/Invoice/Domain/BackendInvoice/LnbitsBackendInvoice.php — already builds unhashed_description/description_hash via sha256; feed it the zap JSON.
  • src/Shared/Value/LnurlPayMetadata.php — the normal-metadata builder (a zap request replaces it).

Goal

Make the server produce a spec-correct zap invoice: advertise allowsNostr/nostrPubkey, accept the nostr zap request on the callback, structurally validate it, and set the invoice description_hash = sha256(zap-request JSON).

Scope / Tasks

  • Config: add a nostrPubkey setting (server's x-only BIP-340 pubkey, 64 hex chars) via LightningConfig/InvoiceConfig/ConfigKey. Advertising it needs no crypto — it's a static string.
  • Pay params (CallbackUrl): when nostrPubkey is configured, add allowsNostr => true and nostrPubkey => <hex> to the response.
  • Callback param: InvoiceController reads the nostr query param, URI-decode → JSON-decode into the kind-9734 event; thread it through the facade into InvoiceGenerator.
  • Structural validation (NIP-57 Appendix D rules 2–8, no crypto):
    • has tags; exactly one p tag; 0 or 1 e tag; 0 or 1 P tag;
    • a relays tag SHOULD be present (store it for the receipt step);
    • if an amount tag exists it MUST equal the amount query param (both millisats);
    • if an a tag exists it MUST be a valid event coordinate;
    • kind MUST be 9734.
      Reject invalid requests with the LNURL {status:ERROR, reason} shape.
  • Invoice description: when a valid zap request is present, the invoice's unhashed description and description_hash MUST be built from the exact zap-request JSON string (as received, post-URI-decode) — not the normal LnurlPayMetadata. Store the raw JSON verbatim (needed later for the receipt so SHA256(description) matches). When absent, behavior is unchanged.
  • A zap-request value object in src/Shared/Value/ (mirroring LnurlPayMetadata) is a good home for parsing/validation/description.
  • Tests: advertised fields present/absent by config; each validation rule (accept + reject); description_hash derived from the zap JSON.

Implementation notes

  • Do NOT re-serialize the zap request — hash and later-embed the same bytes received, or SHA256(description) won't match the receipt (NIP-57 Appendix E).
  • amount is already handled as millisats through the controller/generator.

Acceptance criteria

  • With nostrPubkey set, pay params include allowsNostr:true + nostrPubkey; without it, unchanged.
  • A structurally valid nostr request yields an invoice whose description_hash == sha256(zap-request JSON); invalid requests return an LNURL error.
  • Existing (non-zap) flow unchanged.
  • composer test-all green.

Out of scope (→ follow-up issue)

  • Zap-request signature verification (Appendix D rule 1) — needs secp256k1/BIP-340.
  • Zap receipt (kind 9735) signing + relay publishing + payment-settled detection.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions