lnurlcash-conformance 0.1.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
4
+ may add or tighten checks that a previously-passing mint now fails; pin an
5
+ exact version if you gate CI on the grade.
6
+
7
+ ## 0.1.0 - 2026-08-20
8
+
9
+ First release. Three things, usable independently.
10
+
11
+ - **`vectors/`** - language-neutral JSON test vectors, generated and frozen.
12
+ Load them from a suite in any language; the release gate regenerates them
13
+ and refuses to publish if a single byte moved.
14
+ - **`mock-mint/`** - a real HTTP mint that misbehaves on demand, in every
15
+ way the spec warns about: destroyed responses, never-settling melts,
16
+ wrong-`k1` echoes, mutation on non-GET, secrets served before settlement.
17
+ Typed for TypeScript consumers.
18
+ - **`runner/`** - `lnurlcash-conform <mint>` grades a live service and exits
19
+ non-zero if it is non-compliant. Read-only by default; `--spend` opts into
20
+ the mutating checks, `--note`/`--paid` adds the minted-value check without
21
+ spending anything.
22
+
23
+ Checks in this release cover the six LUD-25 endpoints, the fee algebra on
24
+ mutations (base fee from split change, `(n-1)` refunds on merge, insufficient
25
+ value), the adversarial mutation shapes a mint must refuse, that a mint never
26
+ mutates on a non-GET request, and that LUD-21 verify serves no secret before
27
+ settlement.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheCryptoDonkey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # lnurlcash-conformance
2
+
3
+ Conformance vectors, an adversarial mock mint, and a grader for LNURLcash
4
+ ([LUD-25 draft](https://github.com/lnurl/luds/pull/301)) implementations.
5
+
6
+ Three things, all usable independently:
7
+
8
+ | | |
9
+ | --- | --- |
10
+ | **`vectors/`** | language-neutral JSON. Load them directly from your test suite, in any language. |
11
+ | **`mock-mint/`** | a real HTTP mint that can be told to misbehave, on demand, in every way the spec warns about. |
12
+ | **`runner/`** | `lnurlcash-conform <mint>` — grades a live service and exits non-zero if it is non-compliant. |
13
+
14
+ If you are writing an LNURLcash implementation in any language, run these
15
+ before you run real sats through it.
16
+
17
+ ## Why this exists
18
+
19
+ LNURLcash notes are bearer instruments: whoever holds the `k1` can spend it,
20
+ and a mistake is silent, immediate and irreversible. The wire protocol is
21
+ small enough that anyone can implement it in an afternoon, which is exactly
22
+ the problem — the protocol is easy and the *discipline* is not. Ambiguous
23
+ mutations, melt semantics, who generates a replacement secret, which end of
24
+ a signature carries the recovery id: get any of those wrong and it works
25
+ perfectly until it costs somebody their money.
26
+
27
+ These vectors are the shared statement of what the discipline is, so that
28
+ every implementation can be wrong in the same place at the same time, and
29
+ find out about it in CI rather than in production.
30
+
31
+ ## Using the vectors
32
+
33
+ ```bash
34
+ npm install --save-dev lnurlcash-conformance
35
+ ```
36
+
37
+ They are plain JSON — load them from any language, no dependency required:
38
+
39
+ ```js
40
+ const cases = JSON.parse(readFileSync('vectors/signature.json', 'utf8')).cases
41
+ for (const c of cases) {
42
+ assert.equal(verify(c.k1, c.amountMsat, c.signature, c.mintPubkey), c.valid)
43
+ }
44
+ ```
45
+
46
+ | File | Covers |
47
+ | --- | --- |
48
+ | `signature.json` | offline verification, both recovery-id orderings, malformed input |
49
+ | `bech32.json` | LUD-01 encoding, round trips, corrupted checksums |
50
+ | `url-admission.json` | which URLs may be fetched, and why `data:` must never be |
51
+ | `input-resolution.json` | bech32, LUD-17, Lightning Addresses, bare domains |
52
+ | `note-url.json` | parsing and building note URLs, secret casing, stale signatures |
53
+ | `fees.json` | fee advertisement, application, gross-up minimality, overflow |
54
+ | `bolt11.json` | amount extraction, invoice equality, preimage shape |
55
+ | `callbacks.json` | the exact query each operation puts on the wire |
56
+ | `responses.json` | classifying every reply, including the ambiguous ones |
57
+ | `withdraw-info.json` | the informational GET, and what makes a response invalid |
58
+ | `pay-request.json` | minting, LUD-11 disposable, LUD-21 verify |
59
+ | `lifecycle.json` | behavioural requirements, as scenarios to drive |
60
+
61
+ Regenerate with `npm run generate`; check them with `npm test`, which
62
+ verifies every digest recomputes, every declared signature really does
63
+ verify, and every fee expectation follows from the formula.
64
+
65
+ ## The mock mint
66
+
67
+ ```bash
68
+ npx lnurlcash-mock-mint --port=8899
69
+ ```
70
+
71
+ Prints a lightning address, a spendable 21 sat note, and its pubkey.
72
+ Nothing is payable — it invents its invoices, and a conformance run must
73
+ never be mistakable for a mainnet one.
74
+
75
+ Every misbehaviour is a flag, and each reproduces a real failure a holder
76
+ must survive:
77
+
78
+ | Flag | What it does |
79
+ | --- | --- |
80
+ | `--dropAfterMutation` | applies the mutation, then hangs up. The outcome is genuinely unknowable. |
81
+ | `--unconfirmedMutation` | replies 200 with a body confirming nothing |
82
+ | `--malformedJson` | replies with something that is not JSON |
83
+ | `--echoWrongK1` | answers the informational GET with a different `k1` |
84
+ | `--lieAboutValue=N` | reports a `maxWithdrawable` it never signed |
85
+ | `--signatureLayout=leading` | emits the recovery id at the other end |
86
+ | `--signatures=false` | issues no signatures at all |
87
+ | `--serverGeneratedSecrets` | hands back a secret it generated — the exposure `h` exists to close |
88
+ | `--meltNeverSettles` | holds every melt in flight, so notes stay `pending` |
89
+ | `--meltAlwaysFails` | fails every payment, restoring the note |
90
+ | `--slowMs=N` | delays every response |
91
+ | `--sunset` | refuses anything that grows its liabilities |
92
+ | `--baseFeeMsat=N --feePpm=N` | advertises and withholds a mint fee |
93
+ | `--roundFeeToSat` | rounds the withheld fee up to a whole sat — the note mints short of the formula |
94
+ | `--verifyLeaksEarly` | serves the preimage from verify before settlement — the bearer secret, to anyone with the hash |
95
+ | `--verify=false` | no LUD-21 endpoint at all, not merely unadvertised |
96
+
97
+ As a library, for your own test suite:
98
+
99
+ ```js
100
+ import {createMockMint} from 'lnurlcash-conformance/mock-mint'
101
+
102
+ const mint = await createMockMint({dropAfterMutation: true})
103
+ mint.state.creditNote(k1, 21000)
104
+ // ... drive your client against mint.url, then
105
+ await mint.close()
106
+ ```
107
+
108
+ `mint.state` exposes `creditNote`, `noteState`, `settleMelt`, `failMelt` and
109
+ the raw note and invoice maps, so a test can assert what the SERVICE
110
+ actually did rather than what it said.
111
+
112
+ ## The grader
113
+
114
+ ```bash
115
+ npx lnurlcash-conform mint@example.com
116
+ ```
117
+
118
+ Read-only by default: resolves the payRequest, checks the `withdrawLink`,
119
+ the fee advertisement, invoice amounts, that LUD-21 verify serves no
120
+ preimage before settlement (on a mint that value IS the bearer secret, and
121
+ everyone on the payment's route knows the payment hash), whether an
122
+ unknown note is reported distinguishably from a spent one, and the
123
+ experimental mint address.
124
+
125
+ One check needs a real payment, which the runner cannot make on its own.
126
+ Given a freshly minted, never-rotated note and what its mint invoice was
127
+ paid at, it compares the note's value against the fee formula - msat-exact,
128
+ so a fee implementation that quietly rounds up to whole sats is caught:
129
+
130
+ ```bash
131
+ npx lnurlcash-conform mint@example.com --note='lnurlw://...?k1=...' --paid=500000
132
+ # or --pr=<the mint invoice>, when it carries an amount
133
+ ```
134
+
135
+ This is still read-only. The full run spends:
136
+
137
+ ```bash
138
+ npx lnurlcash-conform mint@example.com --note='lnurlw://...?k1=...' --spend
139
+ ```
140
+
141
+ It burns the note it is given and prints where the value ended up. It
142
+ checks that the informational GET is idempotent and echoes the queried
143
+ `k1`, that the URL's own `amount` is ignored, that a rotate with no `h` is
144
+ refused, that a rotate returns no secret, that signatures verify against the
145
+ advertised `mintPubkey`, that split and merge conserve value - exactly,
146
+ under LUD-25's fee algebra, when the mint's fee advertisement is known -
147
+ and that a burned secret cannot be replayed. It also probes three adversarial shapes a
148
+ mint must refuse atomically: a duplicated `k1` (which a careless mint counts
149
+ twice, minting money from nothing), an output hash that collides with an
150
+ existing note id (minting over it hands the output to whoever already knows
151
+ that id's preimage), and a split whose `h` equals `h2` (one id cannot carry
152
+ two notes). And it replays the callback as a POST and as an OPTIONS
153
+ preflight - real HTTP stacks send both on their own initiative, so the
154
+ mutating endpoint must answer GET only. After every refusal it confirms the refused note is still
155
+ spendable. Use a small note. Exit code is non-zero if anything failed.
156
+
157
+ The grader shares no code with any LNURLcash library — it is written against
158
+ `fetch` and `@noble` directly. A grader that shared an implementation with
159
+ the thing it grades would agree with that implementation's mistakes, which
160
+ is the one thing it must never do.
161
+
162
+ ## Scope and neutrality
163
+
164
+ This repo takes no position on whose implementation is correct. Where the
165
+ vectors and an implementation disagree, either may be wrong, and the LUD-25
166
+ PR is where that gets settled.
167
+
168
+ Spec and reference implementations, all by dni, all MIT:
169
+
170
+ - [LUD-25 draft](https://github.com/lnurl/luds/pull/301)
171
+ - [lnurl-mint](https://github.com/dni/lnurl-mint) — the reference service
172
+ - [lnurl-wallet](https://github.com/dni/lnurl-wallet) — the reference wallet
173
+
174
+ Contributions of vectors are welcome, particularly from implementers who
175
+ found a case these missed. See [CONTRIBUTING.md](CONTRIBUTING.md).
176
+
177
+ ## License
178
+
179
+ MIT.
package/llms.txt ADDED
@@ -0,0 +1,73 @@
1
+ # lnurlcash-conformance
2
+
3
+ Test vectors, an adversarial mock mint, and a grader for LNURLcash (LUD-25).
4
+ MIT. Use these when implementing LNURLcash in ANY language.
5
+
6
+ npm install --save-dev lnurlcash-conformance
7
+
8
+ ## Vectors
9
+
10
+ Plain JSON in vectors/, loadable from any language, listed in
11
+ vectors/index.json:
12
+
13
+ signature.json offline verification, both recovery-id orderings
14
+ bech32.json LUD-01 encoding
15
+ url-admission.json which URLs may be fetched (https, or http to loopback/.onion)
16
+ input-resolution.json bech32, LUD-17, Lightning Address, bare domain
17
+ note-url.json parsing/building note URLs
18
+ fees.json fee advertisement, application, gross-up, overflow case
19
+ bolt11.json amount extraction, invoice equality
20
+ callbacks.json exact query per operation
21
+ responses.json classifying replies incl. ambiguous outcomes
22
+ withdraw-info.json the informational GET
23
+ pay-request.json minting, LUD-11, LUD-21
24
+ lifecycle.json behavioural scenarios
25
+
26
+ ## Mock mint
27
+
28
+ import {createMockMint} from 'lnurlcash-conformance/mock-mint'
29
+ const mint = await createMockMint({dropAfterMutation: true})
30
+ mint.state.creditNote(k1, 21000) // then drive your client at mint.url
31
+ await mint.close()
32
+
33
+ Misbehaviour flags: dropAfterMutation, unconfirmedMutation, malformedJson,
34
+ echoWrongK1, lieAboutValue, signatureLayout=leading, signatures=false,
35
+ serverGeneratedSecrets, meltNeverSettles, meltAlwaysFails, slowMs, sunset,
36
+ baseFeeMsat, feePpm, verify=false.
37
+
38
+ CLI: npx lnurlcash-mock-mint --port=8899 (nothing is payable)
39
+
40
+ ## Grader
41
+
42
+ npx lnurlcash-conform mint@example.com read-only
43
+ npx lnurlcash-conform mint@example.com --note=<url> --spend full, burns the note
44
+
45
+ Exit code non-zero on any failure.
46
+
47
+ ## The signature scheme, canonically
48
+
49
+ note_id = hex(sha256(k1))
50
+ message = "LNURLcash:" + amount_msat + ":" + note_id
51
+ digest = sha256(sha256("Lightning Signed Message:" + message))
52
+ sig = 65 bytes, r || s || recovery_id (wire format)
53
+ pubkey = 33-byte compressed, hex
54
+
55
+ Recover from digest with NO further hashing (prehash off). Verifiers MUST
56
+ also accept recovery_id || r || s. Library layouts differ:
57
+ @noble recid-leading; coincurve trailing; Rust secp256k1 64-byte compact +
58
+ RecoveryId; Go btcec recid+27 leading.
59
+
60
+ ## Fee arithmetic
61
+
62
+ apply(gross, fee) = max(0, gross - base - floor(gross*ppm/1e6))
63
+ Compute the proportional term as floor(gross/1e6)*ppm + floor((gross%1e6)*ppm/1e6):
64
+ a direct multiply overflows u64 at realistic amounts (2.1e15 msat * 999999 ppm).
65
+ grossUp(net, fee) = the SMALLEST gross where apply(gross) == net. Use binary
66
+ search; apply is non-decreasing with steps of 0 or 1. An estimate-then-walk
67
+ implementation is unbounded at 999999 ppm and returns non-minimal answers.
68
+
69
+ ## Spec
70
+
71
+ https://github.com/lnurl/luds/pull/301
72
+ Reference mint: https://github.com/dni/lnurl-mint
73
+ Reference wallet: https://github.com/dni/lnurl-wallet
@@ -0,0 +1,75 @@
1
+ // Types for the mock mint, so TypeScript consumers get the misbehaviour flags
2
+ // checked rather than reaching for `any`. Hand-written to match index.mjs;
3
+ // the flag list here is the same one DEFAULTS declares there.
4
+
5
+ export type SignatureLayout = 'trailing' | 'leading'
6
+
7
+ export type NoteState = 'outstanding' | 'pending' | 'burned'
8
+
9
+ export interface MockMintOptions {
10
+ username?: string
11
+ minSendableMsat?: number
12
+ maxSendableMsat?: number
13
+ /** withheld on minting, advertised in the payRequest metadata */
14
+ baseFeeMsat?: number
15
+ feePpm?: number
16
+ /**
17
+ * 'trailing' is the LUD-25 wire format (r || s || recovery id). 'leading'
18
+ * reproduces the layout lnurl-mint once emitted, forwarding its node's
19
+ * signmessage output unreordered.
20
+ */
21
+ signatureLayout?: SignatureLayout
22
+ /** withhold sig/sig2 entirely, as a SERVICE with no funding source does */
23
+ signatures?: boolean
24
+ /** LUD-21 verify endpoint. Off means 404, not merely unadvertised. */
25
+ verify?: boolean
26
+ privateKey?: string
27
+
28
+ // ---- misbehaviour ----
29
+ /** answer the informational GET with a k1 other than the one queried */
30
+ echoWrongK1?: boolean
31
+ /** report a maxWithdrawable that is not what the note is worth */
32
+ lieAboutValue?: number
33
+ /** hang up mid-mutation, leaving the outcome unknown while it still lands */
34
+ dropAfterMutation?: boolean
35
+ /** reply 200 with a body that confirms nothing */
36
+ unconfirmedMutation?: boolean
37
+ /** reply with a body that is not JSON at all */
38
+ malformedJson?: boolean
39
+ /** hold every melt in flight forever, so the note stays locked as pending */
40
+ meltNeverSettles?: boolean
41
+ /** fail every melt's payment, restoring the note */
42
+ meltAlwaysFails?: boolean
43
+ /** non-compliant: generate the replacement secret SERVICE-side and hand it back */
44
+ serverGeneratedSecrets?: boolean
45
+ /** delay before responding, in milliseconds */
46
+ slowMs?: number
47
+ /** reject splits and mints, as a mint winding down does */
48
+ sunset?: boolean
49
+ /** expose /_test/ endpoints. Never enable against anything real. */
50
+ testHooks?: boolean
51
+ }
52
+
53
+ export interface MockMintState {
54
+ notes: Map<string, {amountMsat: number; state: NoteState}>
55
+ invoices: Map<string, {amountMsat: number; preimage: string; settled: boolean}>
56
+ pubkey: string
57
+ opts: Required<MockMintOptions>
58
+ /**
59
+ * Fund a note directly, bypassing the minting flow. Returns the note's
60
+ * signature, or undefined when this mint was started with `signatures:
61
+ * false` (as a SERVICE with no funding source behaves).
62
+ */
63
+ creditNote(k1: string, amountMsat: number): string | undefined
64
+ noteState(k1: string): NoteState | null
65
+ settleMelt(k1: string): void
66
+ }
67
+
68
+ export interface MockMint {
69
+ url: string
70
+ port: number
71
+ state: MockMintState
72
+ close(): Promise<void>
73
+ }
74
+
75
+ export function createMockMint(options?: MockMintOptions): Promise<MockMint>