Skip to content

feat: support LUD-25 named mint outputs (lnurl-mint v0.4.0) - #5

Merged
tompro merged 12 commits into
mainfrom
feat/named-mint-outputs
Aug 31, 2026
Merged

tompro merged 12 commits into
mainfrom
feat/named-mint-outputs

Conversation

@tompro

@tompro tompro commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Why

lnurl-mint v0.4.0 (live on lnurl.21linz.at) made the LUD-25 output-naming change mandatory: a mint quote without comment = hex(sha256(secret)) is refused with no invoice issued, and the minted note is keyed by the wallet-chosen secret instead of the payment preimage. The wallet's kit 0.1.1 could not send that parameter, so minting against the upgraded mint was fully broken.

What

  • Deps: lnurlcash-kit 0.1.1 → 0.5.0, lnurlcash-conformance 0.1.1 → 0.4.0 (exact pins kept).
  • Dual-mode minting (ops/mint.ts): the mint's generation is detected per mint from the payRequest advertisement (commentAllowed >= 64 or mintToHash). Named mints get a quote bound to a wallet-drawn secret (sent as both comment and h), and the claim polls the note itself — no preimage, no verify URL, and no rotate, since the secret never rode an invoice. Unnamed mints get the byte-identical request the wallet always sent (backwards compatible, pinned by new wire canaries in mintContract.test.ts).
  • Rescue path: a mint that takes the hash but credits the preimage anyway (the kit's mintToHashIgnoresH adversary) is caught by the claim poll and recovered through the quote's verify URL, with the rotate that path exists for. If it serves no verify, the failure is loud rather than silent.
  • Transfer (ops/transfer.ts): named targets need no verify URL — the note's appearance at the wallet's secret is the settlement proof. The claim material carries the secret plus the unverified note from the start, so a melt settling after the poll budget is never lost; the source-note oracle classification is unchanged.
  • NWC (nwc/invoices.ts): named mints claim by secret and hand out the verify preimage as an ordinary receipt — but the rotated guard stays, because the rescue path claims through the preimage flow where a failed rotate leaves that preimage live.
  • Receive dialog: verify URL only required for unnamed mints.

Fund-safety notes

  • The skip-rotate decision for named mints was reviewed adversarially (incl. an Oracle pass): the secret appears in no invoice (only its hash at quote time) and is disclosed only to the custodian mint over TLS at claim — no observer race remains. The rotate stays mandatory on every unnamed path and on the rescue path.
  • noteSecret lives in memory only, never persisted or logged — same ephemeral lifetime the invoice and verify URL always had.
  • conformance-mockmint.d.ts augments the conformance package's published 0.4.0 typings, which are stale relative to their own runtime (commentAllowed/commentRefusesMalformed/verifyOnUnnamedMint are implemented but undeclared) — worth reporting upstream.

Verification

  • npx vitest run: 329/329 (16 new tests: named mint/claim/rescue/adversary, named transfers, named NWC, wire canaries)
  • npx vue-tsc --noEmit: clean; eslint: clean

tompro added 8 commits August 31, 2026 12:22
… 0.4.0

The kit release line adds the LUD-25 output-naming protocol (comment/h on
the pay callback, claim polling, merge batching); the mock mint now
defaults to plain https withdraw links and serves the originally issued
invoice on verify, which the two adapted tests pinned the old shape of.
pollVerifyUntilSettled keeps its exact behavior and messages, reimplemented
on a generic pollUntil so flows that watch something other than a LUD-21
verify (a named mint output appearing) share the same backoff, budget and
abort semantics.
lnurl-mint >= 0.4 refuses a mint quote without comment=sha256(secret) and
keys the note by that secret instead of the payment preimage. prepareMint
detects the capability per mint (commentAllowed >= 64 or mintToHash),
binds the quote to a wallet-drawn secret, and claimMintedNote polls the
note itself - no preimage, no rotate, since the secret never rode an
invoice. Mints that never advertised the capability get the byte-identical
old request and stay on the preimage path; a mint that took the hash but
credited the preimage is caught by the claim poll and rescued through the
quote's verify URL. The d.ts augments the conformance package's stale
published declarations for its comment-naming flags.
A named target needs no verify URL: the note appearing at the wallet's own
secret is the settlement proof, and it is claimed without a rotate. The
claim material carries the secret - and the unverified note at it - from
the start, so a melt settling after the poll budget is never a lost note;
the source-note oracle classification is unchanged.
An unknown-still-pending transfer to a named mint carries the note at the
wallet's own secret in its claim material - persist it unverified like the
settled-claim-failed path already does, instead of dropping it with the
result object.
The named-mint branch claims by the wallet's secret and treats the verify
preimage as an ordinary payment receipt - but keeps the rotated guard
before handing it out, because the rescue path (a mint that credited the
preimage despite the binding) claims through claimFromPreimage, where a
failed rotate leaves that preimage a live note secret. possibleCopy is
committed exactly like the unnamed path.
Named mints claim through the wallet's own secret, so a missing LUD-21
verify URL no longer blocks receiving; unnamed mints still require it -
there the preimage it reveals IS the claim.
Reword the rotate-after-claim invariant for named outputs and fix the
test-harness pointer (ops.testHarness.ts, not test-utils.ts).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Named-mode claim handling does not currently verify the mint’s returned k1 matches the wallet-requested secret, which can cause incorrect note construction and potential fund loss if a service misbehaves.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates the LNURLcash protocol engine and app flows to support LUD-25 “named mint outputs” required by lnurl-mint v0.4.0, while keeping backwards compatibility with unnamed (preimage-keyed) mints and adding adversarial/rescue handling.

Changes:

  • Add dual-mode mint/claim logic (named-by-wallet-secret vs unnamed-by-preimage) and generalized polling utilities.
  • Extend inter-mint transfer and NWC invoice settlement to support named mints (including rescue via verify/preimage where available).
  • Upgrade lnurlcash-kit/conformance pins and add/adjust unit tests + wire-contract canaries for the new behavior.
File summaries
File Description
src/lnurlcash/ops/mint.ts Implements named vs unnamed minting, adds named-claim polling and rescue logic.
src/lnurlcash/ops/transfer.ts Adds named-target transfer claiming via secret (and verify-based rescue).
src/lnurlcash/ops/shared.ts Introduces generic pollUntil and refactors verify polling onto it.
src/lnurlcash/ops.ts Updates fund-safety invariants/docs for named vs unnamed rotate behavior.
src/lnurlcash/ops.transfer.cases.ts Adds transfer coverage for named targets and rescue adversary.
src/lnurlcash/ops.test.ts Includes the new named-mint test suite.
src/lnurlcash/ops.mint-receive.cases.ts Adjusts claim-based assertions and adds invoice-mismatch verify test.
src/lnurlcash/ops.mint-named.cases.ts New test suite for named minting, claim, and rescue/failure modes.
src/lnurlcash/nwc/invoices.ts Adds named-mode settle+claim flow and receipt (preimage) handling rules.
src/lnurlcash/nwc.invoice-a.cases.ts Adds NWC tests for named mint settle/receipt and rescue/rotate guard.
src/lnurlcash/mintContract.test.ts Updates wire-contract assertions; adds canaries for comment/h output naming.
src/lnurlcash/conformance-mockmint.d.ts Adds local typing augmentation for conformance mock mint options.
src/lnurlcash/AGENTS.md Updates fund-critical invariants documentation for named outputs.
src/composables/useReceiveLightningDialog.ts Allows named mints without verify URLs; still requires verify for unnamed.
src/composables/useMoveFundsPage.ts Persists named-target claim material note unverified for late settlement recovery.
package.json Bumps pinned lnurlcash-kit and lnurlcash-conformance versions.
package-lock.json Updates lockfile for the new pinned dependency versions.
Review details
  • Files reviewed: 15/17 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lnurlcash/ops/mint.ts
Comment on lines +248 to +252
if (claim.amountMsat === null || !claim.callback) {
throw new Error('The mint did not return the minted note details.')
}
const note: NewBearer = {
url: buildNoteUrl(target.withdrawLink, claim.k1, claim.amountMsat),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6bb07ff with an explicit claim.k1 === noteSecret assert before any note is built. For the record: the kit already guarantees this twice over — claimMintedNote returns the queried secret by construction (never the service's echo), and fetchNoteInfo throws ProtocolError when the withdrawRequest echoes a different k1 than queried (the conformance suite's echoWrongK1 case). So the new assert cannot trip through the kit as published; it is defense-in-depth so a future kit regression fails loudly instead of tracking a wrong secret.

Comment thread src/lnurlcash/ops/transfer.ts Outdated
// proof - the target only credits it once the melt's payment landed
try {
const claim = await pollMintClaim(prepared.withdrawLink, noteSecret, poll, options)
if (claim.state !== 'minted' || claim.amountMsat === null || !claim.callback) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6bb07ff with an explicit claim.k1 === noteSecret assert before any note is built. For the record: the kit already guarantees this twice over — claimMintedNote returns the queried secret by construction (never the service's echo), and fetchNoteInfo throws ProtocolError when the withdrawRequest echoes a different k1 than queried (the conformance suite's echoWrongK1 case). So the new assert cannot trip through the kit as published; it is defense-in-depth so a future kit regression fails loudly instead of tracking a wrong secret.

tompro added 4 commits August 31, 2026 12:54
Review feedback: bind the claim answer to the secret that was polled
before building a note from it. The kit already guarantees this -
claimMintedNote returns the queried secret by construction and
fetchNoteInfo rejects a mismatched k1 echo - so the assert cannot trip
through the kit (and is therefore not separately testable); it exists so
a future kit regression fails loudly instead of tracking a wrong secret.
@mint.forgesworn.dev is shutting down; @moneyer.dev takes its place in the
curated quick-start list.
…efusal

The redeem callback is a GET and HTTP stacks retry GETs, so a mint can
execute the first attempt and refuse the repeat as an already-spent input
(LUD-25 says SHOULD replay; real mints refuse). The kit attaches the fresh
output secrets to every refusal - probeMutationOutput tells a landed
mutation from a genuine refusal. The harness wrapper reproduces the retry
against the mock.
A split, merge or rotate whose answer is 'Invalid or already spent k1.'
may be the refusal of a retried request that already LANDED - believing
it strands the fresh outputs server-side and desyncs the wallet (the
input is burned at the mint but still spendable locally, so every later
operation fails). All three flows now probe a would-be output when a
refusal carries fresh secrets: live outputs are adopted (or surfaced
unverified when the probe can't tell), a genuine refusal still fails.
@tompro
tompro merged commit f3d5cc4 into main Aug 31, 2026
2 checks passed
@tompro
tompro deleted the feat/named-mint-outputs branch August 31, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants