lnurlcash-conformance 0.2.2 → 0.3.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 +25 -0
- package/README.md +5 -0
- package/llms.txt +10 -0
- package/mock-mint/index.d.ts +8 -0
- package/mock-mint/index.mjs +39 -7
- package/package.json +1 -1
- package/runner/index.mjs +16 -0
- package/vectors/mint-to-hash.json +104 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
|
|
|
4
4
|
may add or tighten checks that a previously-passing mint now fails; pin an
|
|
5
5
|
exact version if you gate CI on the grade.
|
|
6
6
|
|
|
7
|
+
## 0.3.0 - 2026-08-24
|
|
8
|
+
|
|
9
|
+
- Add an optional bound-mint settlement receipt for sealed signers. A
|
|
10
|
+
receipt-capable quote commits to `mint: {h, amount}` before payment; its
|
|
11
|
+
settled LUD-21 response repeats the invoice, output and exact net value and
|
|
12
|
+
adds the ordinary LUD-25 note signature. Absence remains compatible with
|
|
13
|
+
the current preimage-and-rotate flow.
|
|
14
|
+
- `vectors/mint-to-hash.json` now carries the valid quote, unsettled and
|
|
15
|
+
settled shapes plus wrong-output, wrong-amount, premature-signature and
|
|
16
|
+
invalid-signature cases. `docs/BOUND-MINT-RECEIPTS.md` contains candidate
|
|
17
|
+
normative LUD-25 text and the compatibility matrix.
|
|
18
|
+
- The mock mint gains `mintReceipt`; when enabled it publishes the
|
|
19
|
+
verification key before payment, commits the quote and signs only after
|
|
20
|
+
settlement. Self-check and self-grade verify the complete lifecycle.
|
|
21
|
+
|
|
22
|
+
## 0.2.3 - 2026-08-22
|
|
23
|
+
|
|
24
|
+
- **New check: `keeps signatures off the informational endpoint`.** LUD-25
|
|
25
|
+
says signatures are only ever delivered in the `withdrawSuccessResponse`
|
|
26
|
+
of a rotate, split or merge, and that the informational endpoint never
|
|
27
|
+
returns one. Nothing graded it. A mint that hands one out there lets
|
|
28
|
+
anyone holding a note's public URL mint an offline certificate for it,
|
|
29
|
+
and invites a wallet to treat an online answer as offline proof. Both
|
|
30
|
+
known mints pass; the check exists so a third cannot quietly not.
|
|
31
|
+
|
|
7
32
|
## 0.2.2 - 2026-08-22
|
|
8
33
|
|
|
9
34
|
- The mock mint publishes `payLink` on a note's informational GET, as the
|
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ for (const c of cases) {
|
|
|
60
60
|
| `payment-request.json` | `lnurlcashreq1`: one holder asking another for value |
|
|
61
61
|
| `settle-for-value.json` | the decision table a server works through to take a note as payment |
|
|
62
62
|
| `retried-mutation.json` | what makes a repeated mutation a retry rather than a double-spend |
|
|
63
|
+
| `mint-to-hash.json` | wallet-chosen mint outputs and optional bound LUD-21 receipts |
|
|
63
64
|
| `lifecycle.json` | behavioural requirements, as scenarios to drive |
|
|
64
65
|
| `threat-suite.json` | the transport/exposure scorecard — candidate spec options against fixed attacks (non-normative) |
|
|
65
66
|
|
|
@@ -67,6 +68,9 @@ Regenerate with `npm run generate`; check them with `npm test`, which
|
|
|
67
68
|
verifies every digest recomputes, every declared signature really does
|
|
68
69
|
verify, and every fee expectation follows from the formula.
|
|
69
70
|
|
|
71
|
+
The upstreamable wire text and compatibility matrix for the optional receipt
|
|
72
|
+
are in [`docs/BOUND-MINT-RECEIPTS.md`](docs/BOUND-MINT-RECEIPTS.md).
|
|
73
|
+
|
|
70
74
|
## The mock mint
|
|
71
75
|
|
|
72
76
|
```bash
|
|
@@ -123,6 +127,7 @@ answered:
|
|
|
123
127
|
| `--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 |
|
|
124
128
|
| `--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 |
|
|
125
129
|
| `--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 |
|
|
130
|
+
| `--mintReceipt` | with `--mintToHash`, adds the optional quote commitment and signed LUD-21 settlement receipt |
|
|
126
131
|
| `--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 |
|
|
127
132
|
|
|
128
133
|
As a library, for your own test suite:
|
package/llms.txt
CHANGED
|
@@ -54,6 +54,8 @@ payRequest, the same on the mint address document, and the same echoed on
|
|
|
54
54
|
the pay callback's own response when THAT quote was bound.
|
|
55
55
|
mintToHashAdvertisedOn=payRequest,mintAddress,quote narrows which of the
|
|
56
56
|
three claim it (all three by default; it changes only what is claimed).
|
|
57
|
+
mintReceipt=true adds mint:{h,amount} to an honestly bound quote and adds
|
|
58
|
+
sig only to its settled LUD-21 response; it requires mintToHash.
|
|
57
59
|
Misbehaviours, each needing mintToHash as well: mintToHashAcceptsMalformedH,
|
|
58
60
|
mintToHashAcceptsUsedH, mintToHashIgnoresH.
|
|
59
61
|
|
|
@@ -133,6 +135,14 @@ withdraw callback gives, so no oracle appears. A WALLET MUST persist its
|
|
|
133
135
|
secret BEFORE asking for the invoice. Purely additive: without h, nothing
|
|
134
136
|
changes. Table in mint-to-hash.json.
|
|
135
137
|
|
|
138
|
+
For a sealed signer, mint-to-hash may add a bound settlement receipt. The
|
|
139
|
+
quote carries mint:{h,amount}, where amount is exact net msat and no sig is
|
|
140
|
+
allowed. Settled LUD-21 repeats the same h/amount and adds the ordinary
|
|
141
|
+
LUD-25 signature; the wallet matches pr/h/amount and verifies the signature
|
|
142
|
+
before PENDING -> CONFIRMED. Absence of mint is compatible and falls back to
|
|
143
|
+
the legacy preimage/import/rotate flow. Full proposed wire text and matrix:
|
|
144
|
+
docs/BOUND-MINT-RECEIPTS.md.
|
|
145
|
+
|
|
136
146
|
## The retried mutation
|
|
137
147
|
|
|
138
148
|
Every mutation is a GET, and HTTP stacks retry a GET on a dropped
|
package/mock-mint/index.d.ts
CHANGED
|
@@ -157,6 +157,12 @@ export interface MockMintOptions {
|
|
|
157
157
|
* two. A wallet still sends lowercase.
|
|
158
158
|
*/
|
|
159
159
|
mintToHash?: boolean
|
|
160
|
+
/**
|
|
161
|
+
* Emit the optional mint:{h,amount} quote commitment and add the normal
|
|
162
|
+
* note signature on settled LUD-21 verification. Requires mintToHash,
|
|
163
|
+
* verify and signatures. Off by default for baseline compatibility.
|
|
164
|
+
*/
|
|
165
|
+
mintReceipt?: boolean
|
|
160
166
|
/**
|
|
161
167
|
* non-compliant, and only reachable with mintToHash on: issue an
|
|
162
168
|
* invoice for an `h` that is not 64 lowercase hex, so a wallet pays for
|
|
@@ -220,6 +226,8 @@ export interface MockMintState {
|
|
|
220
226
|
settled: boolean
|
|
221
227
|
/** the output id this quote was bound to, when the wallet named one */
|
|
222
228
|
boundTo?: string
|
|
229
|
+
/** the exact invoice returned on the quote, for LUD-21 binding */
|
|
230
|
+
pr?: string
|
|
223
231
|
}
|
|
224
232
|
>
|
|
225
233
|
pubkey: string
|
package/mock-mint/index.mjs
CHANGED
|
@@ -187,6 +187,10 @@ const DEFAULTS = {
|
|
|
187
187
|
// which is the one that matters at the moment money moves: the other
|
|
188
188
|
// two can be cached or stale.
|
|
189
189
|
mintToHash: false,
|
|
190
|
+
// Add the optional bound LUD-21 receipt to an honestly bound quote and
|
|
191
|
+
// its verify response. Requires mintToHash, verify and signatures; off
|
|
192
|
+
// by default so the baseline mock remains the current LUD-25 wire.
|
|
193
|
+
mintReceipt: false,
|
|
190
194
|
// non-compliant, and only reachable with mintToHash on: issue an
|
|
191
195
|
// invoice for an `h` that is not 64 lowercase hex, so a wallet pays for
|
|
192
196
|
// a quote this mint was always going to refuse
|
|
@@ -461,7 +465,14 @@ export const createMockMint = async (options = {}) => {
|
|
|
461
465
|
// one a wallet should decide from. Spread in last and only when
|
|
462
466
|
// the option is on, so a mock started with no options answers
|
|
463
467
|
// exactly what it always answered.
|
|
464
|
-
...(mintToHashPlaces.has('payRequest') ? {mintToHash: true} : {})
|
|
468
|
+
...(mintToHashPlaces.has('payRequest') ? {mintToHash: true} : {}),
|
|
469
|
+
// A receipt verifier needs the signing key before payment. The
|
|
470
|
+
// baseline mock remains byte-for-byte unchanged when receipts are
|
|
471
|
+
// off; a real node-key signer can alternatively be recovered from
|
|
472
|
+
// the BOLT-11 invoice itself.
|
|
473
|
+
...(opts.mintReceipt && opts.verify && opts.signatures
|
|
474
|
+
? {mintPubkey: pubkey}
|
|
475
|
+
: {})
|
|
465
476
|
})
|
|
466
477
|
}
|
|
467
478
|
|
|
@@ -628,18 +639,28 @@ export const createMockMint = async (options = {}) => {
|
|
|
628
639
|
|
|
629
640
|
const preimage = bytesToHex(randomBytes(32))
|
|
630
641
|
const paymentHash = noteId(preimage)
|
|
631
|
-
const
|
|
642
|
+
const pr = fakeInvoice(amount, preimage)
|
|
643
|
+
const invoice = {amountMsat: net, preimage, settled: false, pr}
|
|
632
644
|
if (boundTo) {
|
|
633
645
|
invoice.boundTo = boundTo
|
|
634
646
|
boundOutputs.set(boundTo, paymentHash)
|
|
635
647
|
}
|
|
636
648
|
invoices.set(paymentHash, invoice)
|
|
637
|
-
const body = {pr
|
|
649
|
+
const body = {pr, disposable: false}
|
|
638
650
|
if (opts.verify) body.verify = `${origin}/verify/${paymentHash}`
|
|
639
651
|
// Appended last, and only when the quote really was bound, so a
|
|
640
652
|
// mock that was never told about any of this answers byte for byte
|
|
641
653
|
// what it always answered.
|
|
642
654
|
if (echoBound) body.mintToHash = true
|
|
655
|
+
if (
|
|
656
|
+
echoBound &&
|
|
657
|
+
boundTo &&
|
|
658
|
+
opts.mintReceipt &&
|
|
659
|
+
opts.verify &&
|
|
660
|
+
opts.signatures
|
|
661
|
+
) {
|
|
662
|
+
body.mint = {h: boundTo, amount: net}
|
|
663
|
+
}
|
|
643
664
|
return send(body)
|
|
644
665
|
}
|
|
645
666
|
|
|
@@ -652,7 +673,7 @@ export const createMockMint = async (options = {}) => {
|
|
|
652
673
|
}
|
|
653
674
|
const invoice = invoices.get(verifyMatch[1].toLowerCase())
|
|
654
675
|
if (!invoice) return fail('Unknown payment hash.')
|
|
655
|
-
|
|
676
|
+
const body = {
|
|
656
677
|
status: 'OK',
|
|
657
678
|
settled: invoice.settled,
|
|
658
679
|
// the preimage IS the bearer secret here - a real SERVICE should
|
|
@@ -661,8 +682,18 @@ export const createMockMint = async (options = {}) => {
|
|
|
661
682
|
// bound with its own h: that note is credited elsewhere, so the
|
|
662
683
|
// preimage is an ordinary payment proof and leaks nothing.
|
|
663
684
|
preimage: invoice.settled || opts.verifyLeaksEarly ? invoice.preimage : null,
|
|
664
|
-
pr: fakeInvoice(invoice.amountMsat, invoice.preimage)
|
|
665
|
-
}
|
|
685
|
+
pr: invoice.pr ?? fakeInvoice(invoice.amountMsat, invoice.preimage)
|
|
686
|
+
}
|
|
687
|
+
if (invoice.boundTo && opts.mintReceipt && opts.signatures) {
|
|
688
|
+
body.mint = {
|
|
689
|
+
h: invoice.boundTo,
|
|
690
|
+
amount: invoice.amountMsat,
|
|
691
|
+
...(invoice.settled
|
|
692
|
+
? {sig: sign(invoice.boundTo, invoice.amountMsat)}
|
|
693
|
+
: {})
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return send(body)
|
|
666
697
|
}
|
|
667
698
|
|
|
668
699
|
// ---- LUD-03 informational GET ----
|
|
@@ -776,7 +807,8 @@ export const createMockMint = async (options = {}) => {
|
|
|
776
807
|
invoices.set(paymentHash, {
|
|
777
808
|
amountMsat: note.amountMsat,
|
|
778
809
|
preimage: meltPreimage,
|
|
779
|
-
settled: false
|
|
810
|
+
settled: false,
|
|
811
|
+
pr
|
|
780
812
|
})
|
|
781
813
|
}
|
|
782
814
|
if (!opts.meltNeverSettles) {
|
package/package.json
CHANGED
package/runner/index.mjs
CHANGED
|
@@ -848,6 +848,22 @@ export const gradeNote = async (noteUrl, report, options = {}) => {
|
|
|
848
848
|
: `verified offline against a previous signing key (${signedBy.slice(0, 16)}...)`
|
|
849
849
|
})
|
|
850
850
|
|
|
851
|
+
await report.check('keeps signatures off the informational endpoint', async () => {
|
|
852
|
+
// LUD-25: "Signatures are only ever delivered in the
|
|
853
|
+
// withdrawSuccessResponse of a rotate, split or merge, the
|
|
854
|
+
// informational endpoint never returns one." A mint that hands one out
|
|
855
|
+
// here lets anyone holding only a note's PUBLIC url mint a certificate
|
|
856
|
+
// for it, and invites a wallet to treat the informational answer as an
|
|
857
|
+
// offline proof when it is an online one.
|
|
858
|
+
const here = new URL(url)
|
|
859
|
+
here.searchParams.set('k1', current)
|
|
860
|
+
const body = await get(here)
|
|
861
|
+
assert(body.status !== 'ERROR', `informational GET refused: ${body.reason}`)
|
|
862
|
+
assert(body.sig === undefined, 'the informational GET returned a sig')
|
|
863
|
+
assert(body.sig2 === undefined, 'the informational GET returned a sig2')
|
|
864
|
+
return 'no signature where the spec forbids one'
|
|
865
|
+
})
|
|
866
|
+
|
|
851
867
|
// The still-alive probe the three adversarial checks below share: a
|
|
852
868
|
// compliant refusal is ATOMIC, so the note it refused to touch must
|
|
853
869
|
// still be spendable afterwards.
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"noteId": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
199
199
|
"k1": "0505050505050505050505050505050505050505050505050505050505050505",
|
|
200
200
|
"preimageIsAValidK1": false,
|
|
201
|
-
"note": "
|
|
201
|
+
"note": "a software wallet can claim by asking the withdraw endpoint for its own secret directly. A sealed signer that will not export that secret instead uses the optional bound receipt below to confirm the note without revealing k1"
|
|
202
202
|
},
|
|
203
203
|
"unbound": {
|
|
204
204
|
"noteId": "e802086ad6a1e16b78352ad7296d2aabd835b1b16dbe951e1135b97c68e29d81",
|
|
@@ -207,6 +207,108 @@
|
|
|
207
207
|
"note": "the wallet must poll verify for the preimage, and rotate the instant it has it"
|
|
208
208
|
}
|
|
209
209
|
},
|
|
210
|
+
"receipt": {
|
|
211
|
+
"description": "An optional LUD-21 settlement receipt for a bound quote. This is needed by a sealed signer that generated k1 but will not export it merely so its companion app can probe the withdraw endpoint. The quote commits to the exact output id and net amount before payment. Once settled, verify repeats that commitment and adds the ordinary LUD-25 note signature. No field changes the legacy LUD-21 meaning of preimage: it remains payment proof and still does not open the bound note.",
|
|
212
|
+
"optional": true,
|
|
213
|
+
"field": "mint",
|
|
214
|
+
"keyEstablishment": {
|
|
215
|
+
"rule": "The WALLET must know the receipt verification key before it pays: recover the signing node identity from the BOLT-11 invoice, or read mintPubkey from the payRequest under the wallet's existing trust/pinning policy.",
|
|
216
|
+
"payRequest": {
|
|
217
|
+
"mintToHash": true,
|
|
218
|
+
"mintPubkey": "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"commitment": {
|
|
222
|
+
"h": "the normalised output id committed by this quote; 32 bytes as 64 lowercase hex",
|
|
223
|
+
"amount": "the exact net note value in millisatoshis after fees",
|
|
224
|
+
"sig": "absent before settlement; after settlement, the ordinary recoverable LUD-25 signature over LNURLcash:<amount>:<h>"
|
|
225
|
+
},
|
|
226
|
+
"quote": {
|
|
227
|
+
"pr": "lnbc210n1pjqrstuvwxyz",
|
|
228
|
+
"verify": "https://mint.example/verify/e802086ad6a1e16b78352ad7296d2aabd835b1b16dbe951e1135b97c68e29d81",
|
|
229
|
+
"mintToHash": true,
|
|
230
|
+
"mint": {
|
|
231
|
+
"h": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
232
|
+
"amount": 21000
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"unsettled": {
|
|
236
|
+
"status": "OK",
|
|
237
|
+
"settled": false,
|
|
238
|
+
"preimage": null,
|
|
239
|
+
"pr": "lnbc210n1pjqrstuvwxyz",
|
|
240
|
+
"mint": {
|
|
241
|
+
"h": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
242
|
+
"amount": 21000
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"settled": {
|
|
246
|
+
"status": "OK",
|
|
247
|
+
"settled": true,
|
|
248
|
+
"preimage": "0606060606060606060606060606060606060606060606060606060606060606",
|
|
249
|
+
"pr": "lnbc210n1pjqrstuvwxyz",
|
|
250
|
+
"mint": {
|
|
251
|
+
"h": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
252
|
+
"amount": 21000,
|
|
253
|
+
"sig": "5559e4ad39ea32a9f8dba641cc69e2874e962fd569cd661b5278ccd13f647d6e74bce7105178f3817f1938cee81765cc3ab9005a5821c5d2228c2003418542ea01"
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
"walletRules": [
|
|
257
|
+
"A WALLET that requires a receipt MUST refuse to show or pay an invoice unless quote.mintToHash is exactly true and quote.mint matches the h it requested and the exact amount it expects to receive.",
|
|
258
|
+
"Before accepting settlement, it MUST match verify.pr to quote.pr, match verify.mint.h and verify.mint.amount to the quote commitment, require settled to be exactly true, and verify mint.sig with the mint public key and its locally held k1.",
|
|
259
|
+
"A SERVICE MUST NOT return mint.sig before settlement. An unsettled response MAY repeat h and amount so a wallet can diagnose a mismatch, but that repetition is not a receipt.",
|
|
260
|
+
"Absence of quote.mint means the optional receipt is not offered. A software wallet can still use mintToHash and claim with its own k1; a sealed signer falls back before payment to the legacy preimage-and-rotate flow."
|
|
261
|
+
],
|
|
262
|
+
"invalid": [
|
|
263
|
+
{
|
|
264
|
+
"name": "quote commits a different h",
|
|
265
|
+
"quote": {
|
|
266
|
+
"mintToHash": true,
|
|
267
|
+
"mint": {
|
|
268
|
+
"h": "e0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e",
|
|
269
|
+
"amount": 21000
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"reason": "the invoice is not demonstrably buying the output the wallet named"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "verify changes the net amount",
|
|
276
|
+
"verify": {
|
|
277
|
+
"settled": true,
|
|
278
|
+
"mint": {
|
|
279
|
+
"h": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
280
|
+
"amount": 20999,
|
|
281
|
+
"sig": "5559e4ad39ea32a9f8dba641cc69e2874e962fd569cd661b5278ccd13f647d6e74bce7105178f3817f1938cee81765cc3ab9005a5821c5d2228c2003418542ea01"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"reason": "the settled receipt is not the commitment shown before payment"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "signature appears before settlement",
|
|
288
|
+
"verify": {
|
|
289
|
+
"settled": false,
|
|
290
|
+
"mint": {
|
|
291
|
+
"h": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
292
|
+
"amount": 21000,
|
|
293
|
+
"sig": "5559e4ad39ea32a9f8dba641cc69e2874e962fd569cd661b5278ccd13f647d6e74bce7105178f3817f1938cee81765cc3ab9005a5821c5d2228c2003418542ea01"
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
"reason": "a note signature is evidence of minted value and cannot be issued speculatively"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "settled response has the wrong signature",
|
|
300
|
+
"verify": {
|
|
301
|
+
"settled": true,
|
|
302
|
+
"mint": {
|
|
303
|
+
"h": "f849d67325facf04177bc663b2dc544051831c589ef581d412f2eba44834e77c",
|
|
304
|
+
"amount": 21000,
|
|
305
|
+
"sig": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
"reason": "the signer cannot confirm a note the mint has not authenticated"
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
},
|
|
210
312
|
"normalisation": {
|
|
211
313
|
"description": "One worked pair, so an implementation can check that the two spellings name one output rather than two.",
|
|
212
314
|
"sent": "F849D67325FACF04177BC663B2DC544051831C589EF581D412F2EBA44834E77C",
|
|
@@ -441,7 +543,7 @@
|
|
|
441
543
|
"A WALLET MUST send `h` as 64 lowercase hex. A SERVICE SHOULD normalise case before comparing and MUST NOT read the two spellings as two outputs, but a wallet that leans on that will meet a SERVICE that refuses upper case outright.",
|
|
442
544
|
"A WALLET MUST decide from the payRequest, which every SERVICE publishes, rather than from the experimental mint address document, which many do not. A SERVICE that does not advertise `mintToHash` ignores `h`, and the note it mints is the preimage's.",
|
|
443
545
|
"A WALLET MUST check the pay callback's own `mintToHash` before it pays. The other two advertisements can be cached or stale; that one is this quote, now. Absent means not bound, so claim the preimage way and rotate on sight.",
|
|
444
|
-
"Against a SERVICE that
|
|
546
|
+
"Against a SERVICE that advertises mintToHash, a software WALLET needs no verify poll to claim: it knows its own secret, so it asks the withdraw endpoint for it directly. A sealed signer that will not export k1 MAY require the optional bound receipt and use verify only as authenticated settlement evidence.",
|
|
445
547
|
"A note minted at a WALLET-chosen hash is the WALLET's from birth. A wallet deriving its secrets from a seed can restore that note from the seed alone, which a preimage-secret note can never be."
|
|
446
548
|
]
|
|
447
549
|
}
|