lnurlcash-conformance 0.3.0 → 0.4.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 +44 -0
- package/mock-mint/index.mjs +67 -2
- package/package.json +1 -1
- package/runner/index.mjs +160 -70
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ 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.4.0 - 2026-08-26
|
|
8
|
+
|
|
9
|
+
**The mint-output naming check now reads the spelling LUD-25 actually
|
|
10
|
+
specifies.** The draft names the output with a LUD-12 `comment = hex(h)`,
|
|
11
|
+
advertised as a `commentAllowed` of at least 64; `mintToHash` is the
|
|
12
|
+
parameter form one mint shipped before the comment form was written. The
|
|
13
|
+
suite knew only the latter, so a mint naming notes exactly as the draft
|
|
14
|
+
describes graded as "not offered - a minted note's k1 is the invoice
|
|
15
|
+
preimage". That is the worst kind of wrong answer a conformance suite can
|
|
16
|
+
give: it tells a wallet author the safe mint is the unsafe one.
|
|
17
|
+
|
|
18
|
+
Both spellings are now read from the payRequest and the mint address
|
|
19
|
+
document, probed with a fresh hash each (a mint that binds output ids
|
|
20
|
+
uniquely would rightly refuse the second spelling for naming an output the
|
|
21
|
+
first just took), and reported as `named by comment` / `named by h`.
|
|
22
|
+
|
|
23
|
+
The two are deliberately not probed identically, because the draft's rules
|
|
24
|
+
for them are opposites:
|
|
25
|
+
|
|
26
|
+
- `h` is a parameter invented for naming, so a malformed one MUST be
|
|
27
|
+
refused before an invoice exists. Unchanged.
|
|
28
|
+
- `comment` is plain LUD-12 free text any wallet may send for unrelated
|
|
29
|
+
reasons, so a comment that is not a bare hash MUST fall back to crediting
|
|
30
|
+
`k1=P` rather than be refused, and the mint MUST NOT serve LUD-21 `verify`
|
|
31
|
+
on that fallback - there the preimage is not proof of payment, it is the
|
|
32
|
+
note. Both are now checked, the second as a failure.
|
|
33
|
+
|
|
34
|
+
Only `mintToHash` is expected in both documents. `commentAllowed` is a
|
|
35
|
+
payRequest field; the mint address document is a withdrawRequest, where a
|
|
36
|
+
LUD-12 comment has nowhere to go, so its absence there is no longer
|
|
37
|
+
reported as a disagreement.
|
|
38
|
+
|
|
39
|
+
**A rate-limited grade no longer accuses the mint.** Every mint quote
|
|
40
|
+
issues a real invoice on a real node, so a grader firing a dozen looks like
|
|
41
|
+
the abuse a limiter exists to stop - and probing two spellings nearly
|
|
42
|
+
doubled the count. HTTP 429 was indistinguishable from a spec refusal, so
|
|
43
|
+
the mint was reported as violating whatever check happened to be running
|
|
44
|
+
when the bucket ran dry. 429 is now waited out (honouring `Retry-After`)
|
|
45
|
+
and retried, and reported as an incomplete grade rather than a verdict if
|
|
46
|
+
it persists.
|
|
47
|
+
|
|
48
|
+
The mock mint gains `commentAllowed`, `commentRefusesMalformed` and
|
|
49
|
+
`verifyOnUnnamedMint` to cover all of this.
|
|
50
|
+
|
|
7
51
|
## 0.3.0 - 2026-08-24
|
|
8
52
|
|
|
9
53
|
- Add an optional bound-mint settlement receipt for sealed signers. A
|
package/mock-mint/index.mjs
CHANGED
|
@@ -211,7 +211,30 @@ const DEFAULTS = {
|
|
|
211
211
|
// 'payRequest,mintAddress' is the one that binds without confirming at
|
|
212
212
|
// the moment money moves. Read only when mintToHash is on. Accepts an
|
|
213
213
|
// array or a comma-separated string, so the CLI can pass one.
|
|
214
|
-
mintToHashAdvertisedOn: undefined
|
|
214
|
+
mintToHashAdvertisedOn: undefined,
|
|
215
|
+
|
|
216
|
+
// The SAME capability in the spelling LUD-25 actually specifies:
|
|
217
|
+
// `comment = hex(sha256(secret))` (LUD-12), advertised as a
|
|
218
|
+
// `commentAllowed` of at least 64. A number advertises that many
|
|
219
|
+
// characters and reads `comment` as the output name; false says
|
|
220
|
+
// nothing and reads nothing. Independent of mintToHash - a mint may
|
|
221
|
+
// ship either, both, or neither.
|
|
222
|
+
//
|
|
223
|
+
// The two spellings are not symmetric, and that is the whole reason
|
|
224
|
+
// both exist here. `h` is a parameter invented for naming, so a
|
|
225
|
+
// malformed one MUST be refused before an invoice exists. `comment` is
|
|
226
|
+
// plain LUD-12 free text any wallet may send for unrelated reasons, so
|
|
227
|
+
// LUD-25 requires the opposite: fall back to crediting k1=P, never
|
|
228
|
+
// refuse - and MUST NOT serve verify on that fallback, where P is not
|
|
229
|
+
// proof of payment but the note itself.
|
|
230
|
+
commentAllowed: false,
|
|
231
|
+
// non-compliant, commentAllowed on: refuse a comment that is not a
|
|
232
|
+
// bare 32-byte hex hash instead of falling back, so an ordinary LUD-12
|
|
233
|
+
// wallet cannot pay this mint at all
|
|
234
|
+
commentRefusesMalformed: false,
|
|
235
|
+
// non-compliant, commentAllowed on: serve LUD-21 verify even on the
|
|
236
|
+
// no-comment fallback, where the preimage it hands out IS the note
|
|
237
|
+
verifyOnUnnamedMint: false
|
|
215
238
|
}
|
|
216
239
|
|
|
217
240
|
export const createMockMint = async (options = {}) => {
|
|
@@ -466,6 +489,11 @@ export const createMockMint = async (options = {}) => {
|
|
|
466
489
|
// the option is on, so a mock started with no options answers
|
|
467
490
|
// exactly what it always answered.
|
|
468
491
|
...(mintToHashPlaces.has('payRequest') ? {mintToHash: true} : {}),
|
|
492
|
+
// LUD-25: "A mint payLink intending to support this SHOULD
|
|
493
|
+
// advertise a commentAllowed of at least 64". A payRequest field
|
|
494
|
+
// only - the mint address document is a withdrawRequest, where a
|
|
495
|
+
// LUD-12 comment has nowhere to go.
|
|
496
|
+
...(opts.commentAllowed ? {commentAllowed: opts.commentAllowed} : {}),
|
|
469
497
|
// A receipt verifier needs the signing key before payment. The
|
|
470
498
|
// baseline mock remains byte-for-byte unchanged when receipts are
|
|
471
499
|
// off; a real node-key signer can alternatively be recovered from
|
|
@@ -637,6 +665,36 @@ export const createMockMint = async (options = {}) => {
|
|
|
637
665
|
}
|
|
638
666
|
}
|
|
639
667
|
|
|
668
|
+
// The LUD-25 spelling. Read after `h` so a mock offering both lets
|
|
669
|
+
// an explicit comment name the output, and deliberately NOT an
|
|
670
|
+
// else-branch: a mint may ship either spelling or both.
|
|
671
|
+
let namedByComment = false
|
|
672
|
+
if (opts.commentAllowed) {
|
|
673
|
+
const sent = q.get('comment')
|
|
674
|
+
if (sent !== null && sent !== '') {
|
|
675
|
+
const wellFormed = /^[0-9a-f]{64}$/i.test(sent)
|
|
676
|
+
if (wellFormed) {
|
|
677
|
+
const h = sent.toLowerCase()
|
|
678
|
+
// Same collision rule the `h` spelling gets: an id already
|
|
679
|
+
// spoken for must never be minted over, whichever parameter
|
|
680
|
+
// named it.
|
|
681
|
+
if (outputIdInUse(h) && !opts.mintToHashAcceptsUsedH) {
|
|
682
|
+
return fail('Invalid or already spent k1.')
|
|
683
|
+
}
|
|
684
|
+
if (!opts.mintToHashIgnoresH) {
|
|
685
|
+
boundTo = h
|
|
686
|
+
namedByComment = true
|
|
687
|
+
}
|
|
688
|
+
} else if (opts.commentRefusesMalformed) {
|
|
689
|
+
// The non-compliant branch. LUD-25 says a comment that is not
|
|
690
|
+
// a bare hash MUST fall back, because plain LUD-12 comments
|
|
691
|
+
// are free text: refusing one turns an ordinary "thanks!"
|
|
692
|
+
// into a mint that cannot be paid.
|
|
693
|
+
return fail('Invalid comment.')
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
640
698
|
const preimage = bytesToHex(randomBytes(32))
|
|
641
699
|
const paymentHash = noteId(preimage)
|
|
642
700
|
const pr = fakeInvoice(amount, preimage)
|
|
@@ -647,7 +705,14 @@ export const createMockMint = async (options = {}) => {
|
|
|
647
705
|
}
|
|
648
706
|
invoices.set(paymentHash, invoice)
|
|
649
707
|
const body = {pr, disposable: false}
|
|
650
|
-
|
|
708
|
+
// LUD-25 gates verify on whether the note was named: in the
|
|
709
|
+
// no-comment fallback the preimage verify would hand out IS the
|
|
710
|
+
// note's entire bearer secret, so a mint offering the comment
|
|
711
|
+
// spelling must withhold verify from quotes that used it. A mock
|
|
712
|
+
// that never heard of the comment spelling keeps answering exactly
|
|
713
|
+
// as it always did.
|
|
714
|
+
const verifySafe = !opts.commentAllowed || namedByComment || opts.verifyOnUnnamedMint
|
|
715
|
+
if (opts.verify && verifySafe) body.verify = `${origin}/verify/${paymentHash}`
|
|
651
716
|
// Appended last, and only when the quote really was bound, so a
|
|
652
717
|
// mock that was never told about any of this answers byte for byte
|
|
653
718
|
// what it always answered.
|
package/package.json
CHANGED
package/runner/index.mjs
CHANGED
|
@@ -89,7 +89,17 @@ const isNpub = value => {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
const
|
|
92
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
93
|
+
|
|
94
|
+
// A mint may rate limit, and the good ones do: every mint quote issues a
|
|
95
|
+
// real invoice on a real node, so a grader firing a dozen of them looks
|
|
96
|
+
// exactly like the abuse the limiter exists to stop. An HTTP 429 is the
|
|
97
|
+
// grade failing to complete, NOT a verdict on the mint, so it is waited
|
|
98
|
+
// out (honouring Retry-After) and retried. Without this the mint is
|
|
99
|
+
// accused of whatever the check happened to be probing when the bucket
|
|
100
|
+
// ran dry - a conforming mint graded as broken, which is worse than no
|
|
101
|
+
// grade at all.
|
|
102
|
+
const get = async (url, timeoutMs = 15_000, retriesLeft = 2) => {
|
|
93
103
|
const res = await fetch(url.toString(), {signal: AbortSignal.timeout(timeoutMs)})
|
|
94
104
|
const text = await res.text()
|
|
95
105
|
let body
|
|
@@ -98,6 +108,17 @@ const get = async (url, timeoutMs = 15_000) => {
|
|
|
98
108
|
} catch {
|
|
99
109
|
throw new Error(`response was not JSON: ${text.slice(0, 120)}`)
|
|
100
110
|
}
|
|
111
|
+
if (res.status === 429) {
|
|
112
|
+
if (retriesLeft > 0) {
|
|
113
|
+
const after = Number(res.headers.get('retry-after'))
|
|
114
|
+
const waitMs = Math.min(Number.isFinite(after) && after > 0 ? after * 1000 : 10_000, 30_000)
|
|
115
|
+
await sleep(waitMs + 250)
|
|
116
|
+
return get(url, timeoutMs, retriesLeft - 1)
|
|
117
|
+
}
|
|
118
|
+
throw soft(
|
|
119
|
+
`the mint rate limited this grade (HTTP 429${body?.reason ? `: ${body.reason}` : ''}) - space the run out and grade again; nothing here is a verdict on the mint`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
101
122
|
return body
|
|
102
123
|
}
|
|
103
124
|
|
|
@@ -449,119 +470,186 @@ export const gradeMint = async (payUrl, report) => {
|
|
|
449
470
|
return detail
|
|
450
471
|
})
|
|
451
472
|
|
|
452
|
-
// Naming the note you are buying.
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
473
|
+
// Naming the note you are buying. No longer outside LUD-25: the draft
|
|
474
|
+
// spells it `comment = hex(sha256(secret))` (Protecting a freshly minted
|
|
475
|
+
// note from a preimage race), advertised as a `commentAllowed` of at
|
|
476
|
+
// least 64 - enough for a hex-encoded 32-byte hash. `mintToHash` is the
|
|
477
|
+
// parameter form one mint shipped before the comment form was written.
|
|
478
|
+
// Both are read here: a suite that knows only one spelling reports a
|
|
479
|
+
// mint that names notes perfectly well in the other as not offering it
|
|
480
|
+
// at all, which is worse than silence - it tells a wallet author the
|
|
481
|
+
// safe mint is the unsafe one.
|
|
482
|
+
//
|
|
483
|
+
// Soft in the direction that matters: a mint that says nothing anywhere
|
|
484
|
+
// and names nothing mints exactly what the draft's fallback describes,
|
|
485
|
+
// which is not a defect. What is graded is a mint that CLAIMS the
|
|
456
486
|
// capability, because a wallet then stops rotating on sight and trusts
|
|
457
487
|
// the mint to bind the note.
|
|
458
488
|
//
|
|
459
|
-
// The claim lives in three places
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
489
|
+
// The claim lives in three places. The payRequest is the one to decide
|
|
490
|
+
// from, since every mint publishes one while the mint address document
|
|
491
|
+
// is experimental; the mint address repeats it as corroboration; and the
|
|
492
|
+
// pay callback's own response echoes `mintToHash` when THAT quote was
|
|
493
|
+
// bound. LUD-25 defines no echo for the comment spelling, so a missing
|
|
494
|
+
// echo is only held against a mint claiming `mintToHash`.
|
|
495
|
+
//
|
|
496
|
+
// The two spellings are NOT symmetric, and that is why this cannot be
|
|
497
|
+
// one probe with two parameter names:
|
|
498
|
+
//
|
|
499
|
+
// `h` is a parameter invented for exactly this purpose, so a malformed
|
|
500
|
+
// one is a wallet error and MUST be refused before an invoice exists.
|
|
501
|
+
//
|
|
502
|
+
// `comment` is plain LUD-12 free text that any wallet may send for
|
|
503
|
+
// unrelated reasons, so LUD-25 requires the opposite (line 80 of the
|
|
504
|
+
// draft): a comment that is not a bare hex-encoded 32-byte hash MUST
|
|
505
|
+
// fall back to crediting the note as k1=P, never be refused - and on
|
|
506
|
+
// that fallback the mint MUST NOT serve LUD-21 verify, because there
|
|
507
|
+
// P is not proof of payment, it is the entire note.
|
|
508
|
+
await report.check('accepts a named output on the mint quote (LUD-25 comment / mintToHash, optional)', async () => {
|
|
468
509
|
const amount = Math.max(pay.minSendable, 1000)
|
|
469
|
-
|
|
510
|
+
|
|
511
|
+
// LUD-25: "A mint payLink intending to support this SHOULD advertise a
|
|
512
|
+
// commentAllowed of at least 64". Anything shorter cannot carry the
|
|
513
|
+
// hash at all, so it is not this capability whatever else it is.
|
|
514
|
+
const spellingsOf = source => {
|
|
515
|
+
const found = []
|
|
516
|
+
if (source?.mintToHash === true) found.push('h')
|
|
517
|
+
if (Number.isFinite(source?.commentAllowed) && source.commentAllowed >= 64) found.push('comment')
|
|
518
|
+
return found
|
|
519
|
+
}
|
|
520
|
+
const quoteAt = async (spelling, value, msat = amount) => {
|
|
470
521
|
const url = new URL(pay.callback)
|
|
471
522
|
url.searchParams.set('amount', String(msat))
|
|
472
|
-
url.searchParams.set(
|
|
523
|
+
url.searchParams.set(spelling, value)
|
|
473
524
|
return get(url)
|
|
474
525
|
}
|
|
526
|
+
const spelt = spelling => (spelling === 'comment' ? 'a LUD-12 comment' : 'an h parameter')
|
|
527
|
+
|
|
528
|
+
const advertised = spellingsOf(pay)
|
|
529
|
+
const corroborated = spellingsOf(mintAddress)
|
|
530
|
+
const claimed = [...new Set([...advertised, ...corroborated])]
|
|
475
531
|
|
|
476
532
|
// Asked of every mint, advertisement or not: a mint that echoes the
|
|
477
533
|
// capability without publishing it is still claiming it, and a wallet
|
|
478
534
|
// reading the echo would believe it. Nothing pays the invoice that
|
|
479
535
|
// comes back, so this is read-only - an unpaid quote costs a mint an
|
|
480
536
|
// invoice and nothing else.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
537
|
+
// A fresh secret per spelling, never one shared between them: a mint
|
|
538
|
+
// that accepts both and binds an output id uniquely - as it should,
|
|
539
|
+
// and as the repeat probe below asserts - would rightly refuse the
|
|
540
|
+
// second spelling for naming an output the first just took.
|
|
541
|
+
const probed = claimed.length > 0 ? claimed : ['h']
|
|
542
|
+
const named = Object.fromEntries(
|
|
543
|
+
probed.map(spelling => {
|
|
544
|
+
const secret = bytesToHex(randomBytes(32))
|
|
545
|
+
return [spelling, {secret, h: noteId(secret)}]
|
|
546
|
+
})
|
|
547
|
+
)
|
|
548
|
+
const bound = {}
|
|
549
|
+
for (const spelling of probed) bound[spelling] = await quoteAt(spelling, named[spelling].h)
|
|
550
|
+
const echoedIn = probed.filter(spelling => bound[spelling].mintToHash === true)
|
|
551
|
+
|
|
552
|
+
if (claimed.length === 0 && echoedIn.length === 0) {
|
|
553
|
+
const refused = probed.map(spelling => bound[spelling]).find(body => body.status === 'ERROR')
|
|
489
554
|
throw soft(
|
|
490
|
-
|
|
491
|
-
? `not offered, and
|
|
555
|
+
refused
|
|
556
|
+
? `not offered, and a named output was refused outright: ${refused.reason}`
|
|
492
557
|
: 'not offered - a minted note\'s k1 is the invoice preimage, which every routing node on the payment path learns, so a wallet must claim and rotate the instant it settles'
|
|
493
558
|
)
|
|
494
559
|
}
|
|
495
560
|
|
|
496
561
|
const problems = []
|
|
497
562
|
const claimedBy = [
|
|
498
|
-
advertised &&
|
|
499
|
-
corroborated &&
|
|
500
|
-
|
|
563
|
+
advertised.length > 0 && `the payRequest (${advertised.join(', ')})`,
|
|
564
|
+
corroborated.length > 0 && `the mint address (${corroborated.join(', ')})`,
|
|
565
|
+
echoedIn.length > 0 && 'the quote itself'
|
|
501
566
|
].filter(Boolean)
|
|
502
567
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
568
|
+
for (const spelling of probed) {
|
|
569
|
+
const body = bound[spelling]
|
|
570
|
+
assert(
|
|
571
|
+
body.status !== 'ERROR',
|
|
572
|
+
`claims this capability (${claimedBy.join(', ')}) and refused a well-formed hash sent as ${spelt(spelling)}: ${body.reason}`
|
|
573
|
+
)
|
|
574
|
+
assert(typeof body.pr === 'string', `no pr in the response to ${spelt(spelling)}`)
|
|
575
|
+
const invoiced = invoiceAmountMsat(body.pr)
|
|
576
|
+
if (invoiced !== null) {
|
|
577
|
+
assert(invoiced === amount, `asked for ${amount} msat, invoiced ${invoiced} msat`)
|
|
578
|
+
}
|
|
511
579
|
}
|
|
512
580
|
|
|
513
581
|
// A quote is not a note. Crediting one before its invoice settles
|
|
514
582
|
// would hand out money for nothing.
|
|
515
583
|
const withdrawUrl = pay.withdrawLink ? fromLud17(pay.withdrawLink) : null
|
|
516
584
|
if (withdrawUrl) {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
585
|
+
for (const spelling of probed) {
|
|
586
|
+
const probe = new URL(withdrawUrl)
|
|
587
|
+
probe.searchParams.set('k1', named[spelling].secret)
|
|
588
|
+
const early = await get(probe)
|
|
589
|
+
assert(
|
|
590
|
+
early.status === 'ERROR',
|
|
591
|
+
'the note exists before anything paid for it - a quote is not a note'
|
|
592
|
+
)
|
|
593
|
+
}
|
|
524
594
|
}
|
|
525
595
|
|
|
526
|
-
// A malformed h must be refused BEFORE an invoice exists. A wallet
|
|
527
|
-
// that pays a quote the mint was always going to reject has bought
|
|
528
|
-
// nothing, and the mint keeps the sats.
|
|
529
|
-
//
|
|
530
596
|
// Malformed means not 32 bytes of hex, in any casing. Upper case is a
|
|
531
597
|
// spelling, not a defect, and it is deliberately not probed: a WALLET
|
|
532
598
|
// MUST send lowercase and every client here does, a SERVICE SHOULD
|
|
533
599
|
// normalise before comparing, and one that refuses upper case outright
|
|
534
600
|
// is strict rather than wrong - the wallet learns before it pays.
|
|
535
|
-
|
|
601
|
+
const malformed = [
|
|
536
602
|
['not hex', 'z'.repeat(64)],
|
|
537
603
|
['a character short', '0'.repeat(63)],
|
|
538
604
|
['a character long', '0'.repeat(65)],
|
|
539
605
|
['empty', '']
|
|
540
|
-
]
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
for (const spelling of probed) {
|
|
609
|
+
for (const [what, value] of malformed) {
|
|
610
|
+
const body = await quoteAt(spelling, value)
|
|
611
|
+
if (spelling === 'h') {
|
|
612
|
+
// A wallet that pays a quote the mint was always going to reject
|
|
613
|
+
// has bought nothing, and the mint keeps the sats.
|
|
614
|
+
assert(
|
|
615
|
+
body.status === 'ERROR' && !body.pr,
|
|
616
|
+
`issued an invoice for an h that is ${what} - a wallet would pay for a quote this mint cannot honour`
|
|
617
|
+
)
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
// The comment spelling, where the draft says the opposite.
|
|
621
|
+
assert(
|
|
622
|
+
body.status !== 'ERROR',
|
|
623
|
+
`refused a comment that is ${what} (${body.reason}) - LUD-25 requires the no-name fallback here, because plain LUD-12 comments are free text and any wallet may send one`
|
|
624
|
+
)
|
|
625
|
+
assert(
|
|
626
|
+
!body.verify,
|
|
627
|
+
`served a LUD-21 verify URL on a quote whose comment is ${what}, which the draft credits as k1=P - verify then hands the note itself to anyone holding the invoice, not merely proof that it was paid`
|
|
628
|
+
)
|
|
629
|
+
}
|
|
546
630
|
}
|
|
547
631
|
|
|
548
|
-
// The
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
if (
|
|
632
|
+
// The claims must agree. None of these disagreements loses anyone
|
|
633
|
+
// money on its own - a wallet reading a missing field as false falls
|
|
634
|
+
// back to the preimage flow, which is safe - so each is named rather
|
|
635
|
+
// than failed. Whether the mint really binds is the one thing this
|
|
636
|
+
// check cannot see, because that needs a settlement: it is graded
|
|
637
|
+
// separately, and failed rather than warned.
|
|
638
|
+
if (advertised.includes('h') && echoedIn.length === 0) {
|
|
555
639
|
problems.push(
|
|
556
640
|
'bound quotes carry no mintToHash in the response, so a wallet cannot confirm at the one moment it is worth confirming, and falls back to racing the preimage'
|
|
557
641
|
)
|
|
558
642
|
}
|
|
559
|
-
if (
|
|
643
|
+
if (echoedIn.length > 0 && advertised.length === 0) {
|
|
560
644
|
problems.push(
|
|
561
645
|
'echoes mintToHash on a quote but does not advertise it on the payRequest, which is the endpoint every mint publishes and the one a wallet decides from'
|
|
562
646
|
)
|
|
563
647
|
}
|
|
564
|
-
|
|
648
|
+
// Only the `mintToHash` spelling belongs in both documents. LUD-25 asks
|
|
649
|
+
// a mint *payLink* to advertise `commentAllowed`, and the mint address
|
|
650
|
+
// document is a withdrawRequest, where a LUD-12 comment has nowhere to
|
|
651
|
+
// go - its absence there is correct, not a disagreement.
|
|
652
|
+
if (advertised.includes('h') && mintAddress && !corroborated.includes('h')) {
|
|
565
653
|
problems.push('the payRequest advertises mintToHash and the mint address document does not')
|
|
566
654
|
}
|
|
567
655
|
|
|
@@ -573,15 +661,17 @@ export const gradeMint = async (payUrl, report) => {
|
|
|
573
661
|
// rule the withdraw callback already enforces.
|
|
574
662
|
const otherAmount = Math.min(pay.maxSendable, amount * 2)
|
|
575
663
|
if (otherAmount !== amount) {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
664
|
+
for (const spelling of probed) {
|
|
665
|
+
const twice = await quoteAt(spelling, named[spelling].h, otherAmount)
|
|
666
|
+
if (twice.status !== 'ERROR') {
|
|
667
|
+
problems.push(
|
|
668
|
+
`issued a second quote against an output already named by ${spelt(spelling)} - whichever payment settles first takes the id, and the other payer has bought nothing`
|
|
669
|
+
)
|
|
670
|
+
}
|
|
581
671
|
}
|
|
582
672
|
}
|
|
583
673
|
if (problems.length > 0) throw soft(problems.join('; '))
|
|
584
|
-
return `claimed by ${claimedBy.join(', ')}; bound a quote to a hash of the runner's own secret and
|
|
674
|
+
return `named by ${probed.join(' and ')}; claimed by ${claimedBy.join(', ')}; bound a quote to a hash of the runner's own secret and handled four malformed ones as the draft requires`
|
|
585
675
|
})
|
|
586
676
|
|
|
587
677
|
// The payRequest, with the mint address the checks above fetched hung
|