@forgesworn/moneyer 0.18.0 → 0.18.1

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.1 - 2026-10-07
4
+
5
+ - **Bound mint receipts verify in current wallets again.** Since
6
+ `@lnurlcash/kit` moved to taproot notes, a wallet checks a settled
7
+ `/verify` receipt's `mint.sig` as a `cs1` over the note's Q. moneyer still
8
+ sent a bare hex signature over the bearer `h`, which the kit drops as
9
+ unreadable, so a sealed signer could not confirm a note minted here. The
10
+ receipt now carries the note's ordinary certificate, and a quote or
11
+ receipt for a note named by its `cp1` repeats that `cp1` rather than the
12
+ hex of its Q, which a wallet would read as a bearer `h`.
13
+ - `@lnurlcash/kit` 0.20.2. `moneyer admin verify-note` checks a certificate
14
+ over the note's Q, or over `sha256(k1)` for a bearer note certified before
15
+ that, and still reads a pre-`cs1` signature from a note URL's `sig`. The
16
+ mint page and the live bound-mint check name a quote by its bare `h`.
17
+
3
18
  ## 0.18.0 - 2026-10-07
4
19
 
5
20
  - **A melt that cannot leave is answered with its reason.** The callback
package/README.md CHANGED
@@ -420,10 +420,11 @@ The rules:
420
420
  true`, so a wallet knows this mint takes the parameter before it asks
421
421
  rather than after it pays.
422
422
  - When signing and LUD-21 verification are enabled, the quote also carries
423
- `mint: {h, amount}`. This is the exact output and net millisatoshi value
424
- the invoice will mint; it never carries `sig` before settlement.
423
+ `mint: {h, amount}`. This is the exact output, in the spelling the wallet
424
+ named it (a bearer note's hex `h`, or its `cp1`), and the net millisatoshi
425
+ value the invoice will mint; it never carries `sig` before settlement.
425
426
  - Once settled, `/verify` repeats the same `h` and `amount` and adds `sig`,
426
- the ordinary LUD-25 signature over `LNURLcash:<amount>:<h>`. A sealed
427
+ the note's ordinary LUD-25 certificate: a `cs1` over its Q. A sealed
427
428
  signer can verify that receipt against the pre-payment commitment and
428
429
  pinned `mintPubkey`, then confirm its staged note without exporting `k1`.
429
430
  - Claiming needs nothing else. `GET /w?k1=<the secret>` brings the note
package/dist/admin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
3
3
  import { secp256k1 } from '@noble/curves/secp256k1.js';
4
- import { noteDeclaredAmount, noteK1, noteSignature, verifyNoteSignature, verifyNoteSignatureHash } from '@lnurlcash/kit';
4
+ import { encodeCs1WithAmount, hashK1, noteDeclaredAmount, noteK1, noteSignature, verifyNoteSignatureForKey } from '@lnurlcash/kit';
5
5
  import { decodeNote, decodeSpend } from "./spend.js";
6
6
  import { configFromEnv, pubkeyHex } from "./config.js";
7
7
  import { NoteStore } from "./store.js";
@@ -374,7 +374,20 @@ export const runAdmin = async (argv, deps = {}) => {
374
374
  }
375
375
  const id = bytesToHex(spend.outputKey);
376
376
  const declared = noteDeclaredAmount(url);
377
- const signature = noteSignature(url);
377
+ // `c` since the taproot unification; `sig` on a note URL from before.
378
+ let signature = noteSignature(url);
379
+ if (!signature) {
380
+ try {
381
+ signature = new URL(url).searchParams.get('sig');
382
+ }
383
+ catch { }
384
+ }
385
+ // The kit reads only a cs1 now. A certificate from before that was
386
+ // the bare 65-byte signature in hex, its amount on the URL beside
387
+ // it: put the amount back into a cs1 so it can still be checked.
388
+ if (signature && declared !== null && /^[0-9a-f]{130}$/i.test(signature)) {
389
+ signature = encodeCs1WithAmount(declared, hexToBytes(signature));
390
+ }
378
391
  const current = signingPubkey(config);
379
392
  const keys = [...(current ? [current] : []), ...(config.previousSigningPubkeys ?? [])];
380
393
  out(`note id ${id}`);
@@ -387,9 +400,12 @@ export const runAdmin = async (argv, deps = {}) => {
387
400
  out('signature present, but the URL declares no amount to check it against');
388
401
  }
389
402
  else {
390
- // Over Q, as LUD-25 certifies every note now, or over the note's
391
- // old id for a certificate issued before that.
392
- const signedBy = keys.find(pubkey => verifyNoteSignatureHash(id, declared, signature, pubkey) || verifyNoteSignature(k1, declared, signature, pubkey));
403
+ // Over Q, as LUD-25 certifies every note now, or, for a bearer
404
+ // note certified before that, over its old id sha256(k1). A key
405
+ // note's old id was its key, which is Q already.
406
+ const legacyId = spend.kind === 'script' && /^[0-9a-f]{64}$/i.test(k1.trim()) ? hashK1(k1.trim()) : null;
407
+ const signedBy = keys.find(pubkey => verifyNoteSignatureForKey(id, declared, signature, pubkey) ||
408
+ (legacyId !== null && verifyNoteSignatureForKey(legacyId, declared, signature, pubkey)));
393
409
  out(signedBy === undefined
394
410
  ? 'signature DOES NOT verify against this mint'
395
411
  : `signature verifies against ${signedBy === current ? 'the current key' : `a previous key (${signedBy})`}`);
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { lstat, open, readFile, rename, unlink } from 'node:fs/promises';
3
3
  import { decodeBolt11 } from 'farrier-kit/bolt11';
4
- import { NoteSpentError, PendingNoteError, buildNoteUrl, decodeBolt11AmountMsat, fetchInvoiceVerification, fetchPayRequest, hashK1, isBolt11Invoice, isPreimage, meltNote, probeBurnedNote, requestInvoice, requireBoundMintQuote, validateBoundMintReceipt, withinMintFeeBand } from '@lnurlcash/kit';
4
+ import { NoteSpentError, PendingNoteError, buildNoteUrl, decodeBolt11AmountMsat, fetchInvoiceVerification, fetchPayRequest, hashK1, isBolt11Invoice, isPreimage, meltNote, probeBurnedNote, requestInvoiceShort, requireBoundMintQuote, validateBoundMintReceipt, withinMintFeeBand } from '@lnurlcash/kit';
5
5
  import { claimMintedNote } from "./claim.js";
6
6
  const stages = new Set(['prepared', 'quoted', 'settled', 'claimed', 'retiring', 'retired']);
7
7
  const errno = (error) => error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
@@ -216,7 +216,9 @@ export const runLiveBoundMintCheck = async (options) => {
216
216
  if (state.grossMsat < pay.minSendable || state.grossMsat > pay.maxSendable) {
217
217
  throw new Error(`Test amount is outside the mint range ${pay.minSendable}-${pay.maxSendable} msat.`);
218
218
  }
219
- const quote = await requestInvoice(pay.callback, state.grossMsat, state.h);
219
+ // Named by the bare h, which the kit reads as a bearer note's h and
220
+ // normalises to its Q on both sides of the comparison below.
221
+ const quote = await requestInvoiceShort(pay.callback, state.grossMsat, state.h);
220
222
  if (!quote.verify || !quote.mint)
221
223
  throw new Error('Mint did not bind this quote to h and a verification URL.');
222
224
  const netMsat = quote.mint.amountMsat;
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { applyMintFee, encodeCs1WithAmount, grossUpForMintFee, hashK1 } from '@l
5
5
  import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
6
6
  import { NotePendingError, NoteStore, NoteUnavailableError, OutputCollisionError, swapFingerprint } from "./store.js";
7
7
  import { createNoteSigner } from "./signing.js";
8
- import { bearerNoteId, decodeNote, decodeSpend, spendDomainOf, verifySpend } from "./spend.js";
8
+ import { bearerNoteId, decodeNote, decodeSpend, encodeCp1, spendDomainOf, verifySpend } from "./spend.js";
9
9
  import { createKernelVerifier } from "./kernel.js";
10
10
  import { describeFee } from "./fee-words.js";
11
11
  import { createFakeBackend } from "./backends/fake.js";
@@ -25,6 +25,10 @@ const namedRef = (value) => {
25
25
  const q = decodeNote(value);
26
26
  return q ? { q, named: HEX32.test(value) ? value : q } : null;
27
27
  };
28
+ // The name a quote or receipt repeats back, in the spelling the wallet used.
29
+ // A wallet reads bare hex there as a bearer note's h, so a note named by its
30
+ // cp1 has to come back as that cp1, never as the hex of its Q.
31
+ const echoedName = (q, named) => (named === q ? encodeCp1(hexToBytes(q)) : named);
28
32
  const INVALID_K1 = 'Invalid or already spent k1.';
29
33
  // LUD-25's own reason for an output that names a note already credited.
30
34
  const IN_USE = 'already in use';
@@ -839,7 +843,7 @@ export const createMoneyer = async (config, deps = {}) => {
839
843
  // value this invoice will mint. It is only offered when /verify can
840
844
  // later authenticate settlement with this mint's signing key.
841
845
  ...(config.verify && signer
842
- ? { mint: { h: commentRef.named, amount: net } }
846
+ ? { mint: { h: echoedName(commentRef.q, commentRef.named), amount: net } }
843
847
  : {}),
844
848
  // Every current-draft mint quote is comment-bound, so the LUD-21
845
849
  // preimage is ordinary settlement proof and safe to disclose after
@@ -879,19 +883,21 @@ export const createMoneyer = async (config, deps = {}) => {
879
883
  preimage: preimageHex,
880
884
  pr: invoice.pr,
881
885
  // Repeating h and amount binds this response to the quote. The
882
- // ordinary note signature appears only once value exists at h.
883
- // The receipt signs exactly the `h` it repeats - what the wallet
884
- // named, a bearer note's h or a cp1's Q - since that is what a
885
- // receipt validator checks it over. The note's own certificate,
886
- // the cs1 over its Q, comes from the withdraw endpoint.
886
+ // signature appears only once value exists there, and it is the
887
+ // note's ordinary certificate: a cs1 over its Q, which is what a
888
+ // receipt validator derives from the repeated name and checks.
889
+ // A quote from before notes were keyed by Q stored the bearer h
890
+ // as its output and no separate name.
887
891
  ...(currentInvoice.outputId !== null && signer
888
892
  ? (() => {
893
+ const legacy = currentInvoice.outputNamed === null;
894
+ const q = legacy ? bearerNoteId(currentInvoice.outputId) : currentInvoice.outputId;
889
895
  const named = currentInvoice.outputNamed ?? currentInvoice.outputId;
890
896
  return {
891
897
  mint: {
892
- h: named,
898
+ h: echoedName(q, named),
893
899
  amount: currentInvoice.netMsat,
894
- ...(settled ? { sig: signer.sign(named, currentInvoice.netMsat) } : {})
900
+ ...(settled ? { sig: certify(q, currentInvoice.netMsat) } : {})
895
901
  }
896
902
  };
897
903
  })()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  "live:bound-mint": "npm run build && node scripts/live-bound-mint-check.mjs"
75
75
  },
76
76
  "dependencies": {
77
- "@lnurlcash/kit": "0.18.1",
77
+ "@lnurlcash/kit": "0.20.2",
78
78
  "@noble/curves": "^2.3.0",
79
79
  "@noble/hashes": "^2.3.0",
80
80
  "@scure/base": "^1.2.4",