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.2.3

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 | | 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 | | 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, and every fee expectation follows from the formula.

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 | issues no signatures at all | | --serverGeneratedSecrets | hands back a secret it generated — the exposure h exists 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 the preimage from verify before settlement — the bearer secret, to anyone with the hash | | --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, echoes it back on the quote, and mints at the payment hash anyway, so the preimage is still the money |

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. | --verify=false | no LUD-21 endpoint at all, not merely 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 |

Five behaviours are outside LUD-25 and outside that table, because none of them is misbehaviour. All are absent or off unless you ask for them, so a mock started with no options answers exactly what it always answered:

| Flag | What it does | | --- | --- | | --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 | | --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 instead of already spent. The default, refuse, is what this mock has always done | | --mintToHash | takes an optional h on the pay callback and credits the minted note there, so the payment preimage is not the money. Off by default, and then h is not read at all | | --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, noteState, settleMelt, failMelt and the raw note and invoice maps, so a test can assert what the SERVICE actually did rather than what it said. creditNote(k1, amount, {previousKey: true}) signs 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, that LUD-21 verify serves no preimage before settlement (on a mint that value IS the bearer secret, and everyone on the payment's route knows the payment hash), whether an unknown note is reported distinguishably from a spent one, and the experimental mint address.

One more is graded softly, and it is the one that changes what a bearer note is. In LUD-25 a minted note's k1 is the payment preimage, so the preimage is the money, and every routing node on the payment path learns it, as does anyone who merely saw the invoice and polled LUD-21 verify with its payment hash. A QR on a desktop screen is exactly that. A mint may instead take an h on its pay callback, the sha256 of a secret the wallet chose, and credit the note there; the preimage is then an ordinary payment proof that opens nothing. The mint says so in three places, and they mean different things: mintToHash: true on the payRequest (every mint has one, so it is what a wallet decides from), the same on the experimental mint address document (corroboration), and the same echoed on the pay callback's own response when that quote was bound (the one that matters at the moment money moves, because the other two can be cached). Anything that is not exactly the boolean true is no.

A mint that says nothing anywhere is reported as not offering it and passes, which is every mint today. A mint that claims it is asked to prove the refusals: a malformed h must get no invoice at all, since a wallet that pays for a quote the mint will reject has bought nothing. Malformed means not 32 bytes of hex, in any casing. A wallet MUST send h as 64 lowercase hex and every client here does, but hex is case-insensitive, so a service SHOULD normalise before comparing and MUST NOT read AAAA... and aaaa... as two different outputs: keying the string it was handed files the note where the wallet will never look for it, and nobody is told. A service that refuses upper case outright is being strict rather than wrong, so the grader does not probe it either way. Where the three claims disagree, the grader names the disagreement rather than failing it: none of those loses anyone money on its own. What is failed is a mint that claims the capability and does not bind, because a wallet believing the claim stops rotating on sight; that one needs a settlement to see, so it rides on --preimage.

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. It checks that the informational GET is idempotent and echoes the queried k1, that the URL's own amount is ignored, that a rotate with no h is refused, that a rotate returns no secret, that signatures verify against the advertised mintPubkey or any key the mint still publishes as a previous one, that split and merge conserve value - exactly, under LUD-25's fee algebra, when the mint's fee advertisement is known - that a byte-identical repeat of a mutation is answered with the original success rather than as an already-spent input (a SHOULD, so a mint that has not implemented it is reported as such rather than failed), and that a burned secret cannot be replayed. It also probes three adversarial shapes a mint must refuse atomically: a duplicated k1 (which a careless mint counts twice, minting money from nothing), an output hash that collides with an existing note id (minting over it hands the output to whoever already knows that id's preimage), a split whose h equals h2 (one id cannot carry two notes), a split naming only one output hash (a mint that accepts it is generating the change secret itself), and a split leaving change one msat short of the advertised base fee (which LUD-25 says to refuse with insufficient value, not to serve at a loss). And it replays the callback as a POST and as an OPTIONS preflight - real HTTP stacks send both on their own initiative, so the mutating endpoint must answer GET only. After every refusal it confirms the refused note is still spendable. 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.