npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lnurlcash-conformance

v0.15.0

Published

Language-neutral conformance vectors, an adversarial mock mint, and a grader for LNURLcash (LUD-25) implementations

Readme

lnurlcash-conformance

Conformance vectors, an adversarial mock mint, and a grader for LNURLcash (LUD-25 draft) implementations.

Three things, all usable independently:

| | | | --- | --- | | vectors/ | language-neutral JSON. Load them directly from your test suite, in any language. | | mock-mint/ | a real HTTP mint that can be told to misbehave, on demand, in every way the spec warns about. | | runner/ | lnurlcash-conform <mint> — grades a live service and exits non-zero if it is non-compliant. |

If you are writing an LNURLcash implementation in any language, run these before you run real sats through it.

Why this exists

LNURLcash notes are bearer instruments: whoever holds the k1 can spend it, and a mistake is silent, immediate and irreversible. The wire protocol is small enough that anyone can implement it in an afternoon, which is exactly the problem — the protocol is easy and the discipline is not. Ambiguous mutations, melt semantics, who generates a replacement secret, which end of a signature carries the recovery id: get any of those wrong and it works perfectly until it costs somebody their money.

These vectors are the shared statement of what the discipline is, so that every implementation can be wrong in the same place at the same time, and find out about it in CI rather than in production.

Using the vectors

npm install --save-dev lnurlcash-conformance

They are plain JSON — load them from any language, no dependency required:

const cases = JSON.parse(readFileSync('vectors/signature.json', 'utf8')).cases
for (const c of cases) {
  assert.equal(verify(c.k1, c.amountMsat, c.signature, c.mintPubkey), c.valid)
}

| File | Covers | | --- | --- | | signature.json | offline verification, both recovery-id orderings, malformed input | | derivation.json | deterministic note secrets from a BIP39 seed | | cash-derivation.json | LUD-25's seed-recoverable note secrets under m/139', with BIP-32's own vector 1 | | spec-vectors.json | LUD-25's own published test vectors 1-5, transcribed: derivation, the domain-bound address proof, a key-path ck1 with every SigMsg field, a mint certificate, and a bearer note from h to cw1 | | spends.json | the unified taproot model: bearer notes, ck1s across domains, a three-leaf script tree with its control blocks and verdicts, a CHECKSIG leaf's script-path sighash, time claims, leaf policy, malformed cw1s and the short forms | | part2.json | key-path notes: cp1, the domain-bound ck1, amount-bearing cs1 certificates over hex(Q), cx1, the per-note key tweak and address proofs, on the m/139'/d1..d4 address path | | bech32.json | LUD-01 encoding, round trips, corrupted checksums | | url-admission.json | which URLs may be fetched, and why data: must never be | | input-resolution.json | bech32, LUD-17, Lightning Addresses, bare domains | | note-url.json | parsing and building note URLs, secret casing, stale signatures | | fees.json | fee advertisement, application, gross-up minimality, overflow | | bolt11.json | amount extraction, invoice equality, preimage shape | | callbacks.json | the exact query each operation puts on the wire | | responses.json | classifying every reply, including the ambiguous ones | | withdraw-info.json | the informational GET, and what makes a response invalid | | pay-request.json | minting, LUD-11 disposable, LUD-21 verify | | payment-request.json | lnurlcashreq1: one holder asking another for value | | settle-for-value.json | the decision table a server works through to take a note as payment | | retried-mutation.json | what makes a repeated mutation a retry rather than a double-spend | | mint-to-hash.json | additive mintToHash compatibility and optional bound LUD-21 receipts; not baseline LUD-25 | | nostr-seed.json | a key-path address branch rooted in a Nostr identity key, for a holder with no BIP39 words (heartwood-esp32, lnurlcash-kit), with domain-bound ck1s; an extension, not LUD-25 | | lifecycle.json | behavioural requirements, as scenarios to drive | | threat-suite.json | the transport/exposure scorecard — candidate spec options against fixed attacks (non-normative) |

Regenerate with npm run generate; check them with npm test, which verifies every digest recomputes, every declared signature really does verify, every fee expectation follows from the formula, and every value in spec-vectors.json is the hex 25.md itself publishes.

The upstreamable wire text and compatibility matrix for the optional receipt are in docs/BOUND-MINT-RECEIPTS.md.

The mock mint

npx lnurlcash-mock-mint --port=8899

Prints a lightning address, a spendable 21 sat note, and its pubkey. Nothing is payable — it invents its invoices, and a conformance run must never be mistakable for a mainnet one.

Every misbehaviour is a flag, and each reproduces a real failure a holder must survive:

| Flag | What it does | | --- | --- | | --dropAfterMutation | applies the mutation, then hangs up. The outcome is genuinely unknowable. | | --unconfirmedMutation | replies 200 with a body confirming nothing | | --malformedJson | replies with something that is not JSON | | --echoWrongK1 | answers the informational GET with a different k1 | | --lieAboutValue=N | reports a maxWithdrawable it never signed | | --signatureLayout=leading | emits the recovery id at the other end | | --signatures=false | certifies nothing. LUD-25 has a mint certify every note with a cs1 over hex(Q) as a SHOULD, so the grader warns | | --certificateOverH | certifies a bearer note over its h, the pre-taproot message, instead of hex(Q) | | --serverGeneratedSecrets | hands back a secret it generated: the exposure p1/p2 exist to close | | --meltNeverSettles | holds every melt in flight, so notes stay pending | | --meltAlwaysFails | fails every payment, restoring the note | | --slowMs=N | delays every response | | --sunset | refuses anything that grows its liabilities | | --baseFeeMsat=N --feePpm=N | advertises and withholds a mint fee | | --roundFeeToSat | rounds the withheld fee up to a whole sat — the note mints short of the formula | | --verifyLeaksEarly | serves a preimage before settlement, falsely claiming payment proof before payment happened | | --retriedMutation=refuse | deliberately answers an identical mutation retry as already spent instead of replaying its original success | | --replayMatchesStrings | matches a retry on the raw k1/p1/p2 strings, so the same spend spelt another way is refused as already spent | | --alreadyInUseReason=<text> | refuses a p1/p2 naming a note already in use with some reason other than LUD-25's exact already in use | | --leafVersionUnchecked | accepts a leaf version other than 0xc0, as consensus alone would | | --opSuccessUnchecked | accepts a leaf carrying an OP_SUCCESSx opcode | | --ignoresTimeClaims[=rule,...] | ignores a script path's time claim: every rule, or blockHeight, future, blockCount, relative | | --refusesLocktimes | over-strict: refuses every non-zero locktime, a past Unix time included | | --unverifiedCk1 | accepts any ck1 whose Q is outstanding without checking its signature, so one bound to another domain opens the note | | --infoSkipsVerification | answers the informational GET for a k1 from its Q alone, never verifying the spend | | --refusesCp1Outputs | refuses a cp1 wherever one may go, as a mint without key-path notes does | | --acceptsOffCurveCp1 | accepts a cp1 whose key is not a curve point | | --hashLookup=echoesK1 | answers a lookup by p but puts a k1 back in the response | | --hashLookup=answersUnknown | answers a lookup by p for a note it never registered | | --hashLookup=hidesSpent | incorrectly reports a retained spent note as unknown | | --hashLookup=acceptsBoth | accepts k1 and p together | | --mintToHashAcceptsMalformedH | claims mintToHash and invoices an h that is not 64 lowercase hex, so a wallet pays for a quote the mint will refuse | | --mintToHashAcceptsUsedH | claims it and invoices an h that already names a note, an invoice or another quote's output | | --mintToHashIgnoresH | claims it but accepts h and mandatory comment naming different outputs |

The three mintToHash* misbehaviours need --mintToHash alongside them; on their own they do nothing, because a mint that never offered the capability cannot misuse it.

The lookup by p (a cp1, or a bearer note's hex h; ?h= is still read as its older name) keeps the spend off the wire but reports spent state: --hashLookup=true distinguishes a burned note from an unknown one. The older --hashLookup=revealsSpent spelling remains an alias for this compliant behaviour.

The mock keys every note by its taproot output key Q and verifies every spend it is handed: a ck1 against the key-path sighash for the hostname it was reached at (or --domains=a,b), and a cw1 by the leaf rules, the time rules against its own clock, and then the script. It has no script interpreter, so of the scripts themselves it judges only a bearer hashlock (under any internal key, at any depth) and refuses any other as one it cannot verify.

The remaining flags enable optional features, legal wire variants, or make a conforming default explicit. Optional fields stay absent unless requested:

| Flag | What it does | | --- | --- | | --verify=false | provides no LUD-21 endpoint at all, rather than merely leaving it unadvertised | | --withdrawLinkForm=lnurlw | spells withdrawLink as lnurlw://host/w instead of the plain https://host/w the reference mint emits. Both are legal; a client has to take both | | --name --description --contact --tosUrl --motd --version | mint info on the experimental discovery endpoint: who runs this, how to reach them, the terms, and what the operator wants holders to know today | | --baseFeeMsat --feePpm | also publishes fees: {baseFeeMsat, feePpm} on that endpoint, the structured twin of the fee line in the payRequest metadata | | --stats | serves GET /stats: what the mint owes, what is in flight, what the node holds, and the coverage between them | | --localBalanceMsat=N | what the node behind a stats-publishing mock claims to hold, so a mock can be told to look under-covered | | --previousPubkeys=a,b | keys this mint has signed under before, so notes issued before a rotation still verify. Not a LUD-25 field: the spec carried it briefly and dropped it on 2026-09-04. Graded for shape where a mint offers it, never asked for | | --previousPrivateKey=<hex> | an old signing key the mock still holds. Its public half joins previousPubkeys on its own | | --signWithPreviousKey | issues every note under that old key while still advertising the new one: the mid-rotation state a mint passes through when the advertisement moves before the signer | | --retriedMutation=replay | answers a byte-identical repeat of a mutation with the original success. This is the conforming default; use refuse only as an adversarial fixture | | --hashLookup=false | models an older SERVICE with no lookup by p, which LUD-25 now makes a MUST | | --mintToHash | accepts h alongside the mandatory identical comment and enables the additive quote/receipt fields. Off by default; baseline comment-bound minting remains on | | --mintReceipt | with --mintToHash, adds the optional quote commitment and signed LUD-21 settlement receipt | | --mintToHashAdvertisedOn=quote | narrows which of the three places claim it (payRequest, mintAddress, quote); all three by default. Changes only what is claimed, never what the mint does |

As a library, for your own test suite:

import {createMockMint} from 'lnurlcash-conformance/mock-mint'

const mint = await createMockMint({dropAfterMutation: true})
mint.state.creditNote(k1, 21000)
// ... drive your client against mint.url, then
await mint.close()

mint.state exposes creditNote, creditOutput, noteState, settleMelt, failMelt and the raw note and invoice maps (notes keyed by hex(Q)), so a test can assert what the SERVICE actually did rather than what it said. creditNote takes any spend of the note (a 64-hex preimage, a ck1 or a cw1); creditOutput takes what names it (a cp1 or a bearer note's hex h). creditNote(k1, amount, {previousKey: true}) certifies that one note under previousPrivateKey, which is how a case puts one note under the old signing key and the rest under the new.

The grader

npx lnurlcash-conform [email protected]

Read-only by default: resolves the payRequest, checks the withdrawLink (either legal spelling, and the report says which one the mint uses), the fee advertisement, invoice amounts, mandatory commentAllowed: 64, pre-invoice rejection of missing or malformed mint comments (a cp1 whose key is not on the curve among them), whether an unknown note is reported distinguishably from a spent one, and the experimental mint address.

Current LUD-25 minting is always comment-bound. The wallet names the note it is buying as comment=cp1<Q>, or as a bearer note's hex h, and the payment preimage remains ordinary settlement proof. A mint that cannot accept either spelling, or that silently creates a preimage-backed note, fails grading.

mintToHash is retained as an additive compatibility field. When advertised, the runner sends h alongside the mandatory comment and requires both to name the same output. A malformed h must be rejected before invoice creation. The payRequest, experimental mint-address document and quote echo are checked separately because each claim has a different lifetime. Disagreement between those extension advertisements warns; failure to honour a claimed binding fails when the paid --preimage check proves where the note actually landed.

Three other things a mint may publish are graded softly, because none of them is in LUD-25: the mint info on the discovery endpoint, a /stats endpoint stating what the mint owes against what its node holds, and the signing keys it has used before. Publishing none of them costs nothing. Publishing one in the wrong shape is a warning, not a failure, because a wallet will try to render it and someone should say so. A mint whose node holds less than it owes warns too: whether it is fully backed is the operator's to disclose, and a mint that publishes an uncomfortable number is behaving better than one that publishes nothing.

One check needs a real payment, which the runner cannot make on its own. Given a freshly minted, never-rotated note and what its mint invoice was paid at, it compares the note's value against the advertised fee:

LUD-25 says nothing about whether that fee rounds, and the two live implementations differ. dni's lnurl-mint ceilings it to a whole sat on purpose, so the mint is never short a sat; moneyer withholds the msat-exact amount. Both pass. The check grades the range between them and names which it saw, and a msat outside it either way fails - a mint taking more than the ceilinged fee, or crediting more than it advertised.

npx lnurlcash-conform [email protected] --note='lnurlw://...?k1=...' --paid=500000
# or --pr=<the mint invoice>, when it carries an amount

The bound-mint check needs a payment too. Mint against a hash you chose yourself, then hand the runner your own secret and the preimage of the invoice you paid: the note must really be at your secret, and the preimage must open nothing.

npx lnurlcash-conform [email protected] --note='lnurlw://...?k1=<your secret>' \
  --preimage=<the preimage of the invoice you paid>

Both are still read-only. The full run spends:

npx lnurlcash-conform [email protected] --note='lnurlw://...?k1=...' --spend

It burns the note it is given and prints where the value ended up. The note's k1 may be any spend of it: a bearer note's 64-hex preimage, a ck1 or a cw1. It grades LUD-25 as of the unified taproot model (luds 6e865b1), where every note is a taproot output key Q, with the derivation purposes and certificate names of luds 50d740a: certificates are read from c and c2, and a mint that also sends the older sig and sig2 is not faulted for it.

On the note as given it checks that the informational GET is idempotent, echoes the queried k1 and ignores the URL's own amount; that a lookup by p answers by the note's cp1 and by its hex h, with no k1 in the reply, and tells a spent note from an unknown one; that a rotate with no p1 and a split with no p2 are refused; that a rotate returns no secret; that split and merge conserve value, exactly under LUD-25's fee algebra when the fee is known; and that a retried mutation is answered with the original success, byte for byte and when the retry spells the same spend (preimage or full cw1) or the same output (h or cp1) another way, while a burned note cannot be spent again. It probes the shapes a mint must refuse atomically: one note named twice in a merge, in the same or two spellings (a careless mint counts it twice, minting money from nothing); a p1 naming the burned note it was given, which must be refused as exactly already in use; a split whose p1 and p2 name one note; a split leaving change short of the base fee (insufficient value); and the callback replayed as a POST and as an OPTIONS preflight, since the mutating endpoint must answer GET only.

Then it moves the value through two notes of its own. The first is a three-leaf script tree under an internal key it holds: a bearer hashlock at leaf version 0xc0, the same shape at 0xc2, and a hashlock followed by OP_SUCCESS80. It is funded by the full cw1 of a bearer note, which must be the same spend as its preimage. On it the grader shows that a leaf version other than 0xc0 and an OP_SUCCESS leaf are refused, and that time claims are judged by the mint's own clock: a block-height locktime, a locktime in the future, a block-count relative lock and an unelapsed relative time lock are refused, and a locktime already past is accepted. The second is a key-path note, cp1<Q> with Q untweaked as a seeded wallet makes them: a ck1 signed for another domain is refused at the callback, the informational GET refuses it and a ck1 signed by another key, and the ck1 bound to the note URL's own hostname spends it.

Every refusal is confirmed to have left the value where it was, and every path has a way home (the tree's hashlock leaf, then its key path), so a compliant run ends holding a bearer note worth what it started with. Certificates are a SHOULD: a missing cs1 warns, but every cs1 returned, on a mutation or on the informational GET, must verify over hex(Q) and the note's value, and one over a bearer note's h (the pre-taproot message) fails. Use a small note. Exit code is non-zero if anything failed.

What the grader cannot reach. It never melts. Melting spends real sats against a real mint, which is not something a grading tool may decide to do, so pending on a k1 mid-melt, restoring the note when the outgoing payment fails, and the melt's own LUD-21 verify are all outside what a grade can say anything about. They are not unspecified and not untested: the mock mint implements every one of them (meltNeverSettles, meltAlwaysFails), so a client suite driving the mock covers the whole melt path. A clean grade means the read-only and non-melt mutating surface is compliant, no more.

The grader shares no code with any LNURLcash library — it is written against fetch and @noble directly. A grader that shared an implementation with the thing it grades would agree with that implementation's mistakes, which is the one thing it must never do.

Scope and neutrality

This repo takes no position on whose implementation is correct. Where the vectors and an implementation disagree, either may be wrong, and the LUD-25 PR is where that gets settled.

Spec and reference implementations, all by dni, all MIT:

Implementations to run these vectors against are indexed in awesome-lnurlcash.

Contributions of vectors are welcome, particularly from implementers who found a case these missed. See CONTRIBUTING.md.

License

MIT.