@forgesworn/moneyer 0.11.0 → 0.12.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 CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 - 2026-09-11
4
+
5
+ - LUD-25's renamed parameters are accepted: `p` for the informational
6
+ GET's hash lookup, `p1`/`p2` for the callback's outputs. The old names
7
+ `h` and `h`/`h2` keep working, and a wallet may mix them in one request.
8
+ Sending both spellings of one value with different contents is refused.
9
+ A retry is matched on the outputs, not the spelling, so a split first
10
+ sent as `h`/`h2` and retried as `p1`/`p2` is answered as a replay.
11
+ - The callback's missing-output reasons are now `missing p1` and
12
+ `missing p2` (were `missing h`/`missing h2`), and `p1 and p2 must
13
+ differ.` The pay callback's `missing h` is unchanged.
14
+ - **LUD-25 Part 2 notes.** A note can be keyed by a public key and spent
15
+ with a recoverable signature, so the mint never holds anything that could
16
+ spend it. `comment=cp1…` mints one; `k1=ck1…` looks it up or spends it;
17
+ `p`, `p1` and `p2` take `cp1` keys; and a Part 2 note's certificate comes
18
+ back as `cs1`, on the lookup and on every rotate, split and merge. The two
19
+ kinds mix freely in one merge.
20
+ - A note named twice in one request is now caught by the note each `k1`
21
+ spends rather than by the `k1` string, since one Part 2 note has many
22
+ valid `ck1` strings. The store refuses a repeated input as well.
23
+ - Takes lnurlcash-kit 0.10.0.
24
+
25
+ ## 0.11.1 - 2026-09-07
26
+
27
+ - Hash lookups report `Note already spent.` for retained burned notes,
28
+ `Unknown note.` for unregistered hashes, and `pending` for unresolved
29
+ melts. This follows the proposed LUD-25 status revision and lets wallets
30
+ recognise a redeemed note without sending its bearer secret. Existing
31
+ burned rows are retained already; no database migration is required.
32
+ - Test tooling uses an immutable conformance commit matching this response
33
+ contract. It is a development dependency, not a runtime dependency.
34
+ Since 2026-09-09 that contract is released as `lnurlcash-conformance`
35
+ 0.8.0 and the pin is the version rather than the commit; the graded bytes
36
+ are the same ones 0.11.1 shipped against.
37
+
3
38
  ## 0.11.0 - 2026-09-06
4
39
 
5
40
  **Three fields the reference mint publishes and moneyer did not.** Each
package/README.md CHANGED
@@ -344,6 +344,21 @@ preimage then buys nothing: it is an ordinary payment proof, which is why
344
344
  there is no race left to run, and no window in which holding the invoice
345
345
  is nearly holding the money.
346
346
 
347
+ ### Or name it by a key (LUD-25 Part 2)
348
+
349
+ The comment can instead be `cp1<pk>`, a public key. The note is keyed by
350
+ that key and spent with `ck1`, a recoverable signature by its private key,
351
+ so the mint never sees anything that could spend it. Everywhere a note is
352
+ named or spent, the two kinds mix freely:
353
+
354
+ - `GET /w?k1=<ck1>` or `GET /w?p=<cp1>` looks a Part 2 note up, and the
355
+ answer carries `sig`, the mint's certificate for it as `cs1`.
356
+ - `p1`/`p2` on the callback may be `cp1` keys, and the matching `sig`/`sig2`
357
+ come back as `cs1`. A hash output still gets a plain hex signature.
358
+ - A merge may combine Part 1 secrets and Part 2 `ck1`s in one request.
359
+
360
+ A recipient checks a Part 2 note offline from its `ck1` and `cs1` alone.
361
+
347
362
  Moneyer's earlier `mintToHash` extension remains additive. A compatible
348
363
  wallet repeats the same commitment as `h`; it does not replace `comment`:
349
364
 
@@ -416,9 +431,10 @@ signatures, nothing burned, nothing minted, no balance moved. A retry is
416
431
  a read.
417
432
 
418
433
  A request is the same request when it names the same input notes and asks
419
- for the same outputs: the same `h`, the same `h2`, the same `amount`.
434
+ for the same outputs: the same `p1`, the same `p2`, the same `amount`.
420
435
  Input order does not matter, because a reordered retry is the same
421
- operation. Anything else naming a burned note is a double-spend attempt
436
+ operation, and nor does spelling: `h`/`h2`, the older names for
437
+ `p1`/`p2`, name the same outputs. Anything else naming a burned note is a double-spend attempt
422
438
  and still gets `Invalid or already spent k1.`, unchanged. Provenance is
423
439
  recorded rather than inferred for that reason: matching on "a note exists
424
440
  at `h`" alone would let anyone holding a burned `k1` and any outstanding
@@ -586,11 +602,11 @@ clearnet.
586
602
  | `/.well-known/lnurlp/<user>` | LUD-16 payRequest; paying mints a note |
587
603
  | `/.well-known/lnurlp/<zap name>` | NIP-57 payRequest; paying mints a note *to the name's pubkey* |
588
604
  | `/.well-known/lnurlw/<user>` | LUD-25 mint address discovery (experimental) |
589
- | `/p/cb` | LUD-06 pay callback; issues the mint invoice, and takes an optional `h` naming the note |
605
+ | `/p/cb` | LUD-06 pay callback; issues the mint invoice for the note the `comment` names, a hash or a `cp1` key (`h` is an older spelling for a hash) |
590
606
  | `/z/cb/<zap name>` | the zap callback; validates the kind 9734 and issues the invoice |
591
607
  | `/verify/<hash>` | LUD-21 verify, for mint invoices and melt payments |
592
- | `/w` | LUD-03 informational GET; accepts `k1` or the non-disclosing LUD-25 `h=sha256(k1)` check; a live note also carries `payLink` |
593
- | `/w/cb` | the mutating callback: melt, rotate, split, merge |
608
+ | `/w` | LUD-03 informational GET; accepts `k1` (a secret or a `ck1`) or the non-disclosing LUD-25 `p` check (`sha256(k1)` or a `cp1` key; `h` is its older name, still accepted); a live note also carries `payLink`, and a Part 2 note its `cs1` as `sig` |
609
+ | `/w/cb` | the mutating callback: melt, rotate, split, merge; inputs as secrets or `ck1`s, outputs as `p1`/`p2` hashes or `cp1` keys, or their older names `h`/`h2` |
594
610
  | `POST /names` | claim a lightning address, authenticated by NIP-98 |
595
611
  | `/.well-known/nostr.json` | NIP-05 for the names this mint serves |
596
612
  | `/stats` | what the mint owes, what the node holds, and the coverage between them |
package/THREAT-MODEL.md CHANGED
@@ -23,7 +23,12 @@ request carrying a callback's query string cannot mutate anything.
23
23
 
24
24
  **A holder inflating a merge or split with a repeated k1.** Duplicated k1
25
25
  parameters in one request are refused outright; they would otherwise count
26
- one note's value twice into the output.
26
+ one note's value twice into the output. The check is on the note each k1
27
+ spends, not on the k1 string: a Part 2 note's `ck1` is a signature, and one
28
+ note has many valid ones (anyone holding a `ck1` can flip it to `(r, n-s)`,
29
+ and the key's owner can sign again). The store refuses a note named twice
30
+ among a swap's inputs as well, so the rule does not rest on the handler
31
+ alone.
27
32
 
28
33
  **A holder claiming an output id that already exists.** `h`/`h2` may not
29
34
  collide with any existing note, any mint invoice's payment hash, settled
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
3
3
  import { finalizeEvent } from 'nostr-tools/pure';
4
- import { applyMintFee, grossUpForMintFee, hashK1 } from 'lnurlcash-kit';
4
+ import { applyMintFee, decodeCk1, decodeCp1, encodeCs1, grossUpForMintFee, hashK1, recoverNoteOwnershipPubkey } from 'lnurlcash-kit';
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";
@@ -19,6 +19,36 @@ import { ANNOUNCE_D_TAG, ANNOUNCE_KIND, announcementContent } from "./announce.j
19
19
  import { isRefusal, registerName, validateNip98 } from "./names.js";
20
20
  import { CONFIG_TOKEN, loadWebAssets } from "./web-assets.js";
21
21
  const HEX32 = /^[0-9a-f]{64}$/;
22
+ // An output or a lookup: a 64-hex hash, or a cp1 key.
23
+ const namedRef = (value) => {
24
+ if (HEX32.test(value))
25
+ return { id: value, cp1: false };
26
+ const pubkey = decodeCp1(value);
27
+ return pubkey ? { id: bytesToHex(pubkey), cp1: true } : null;
28
+ };
29
+ // What a k1 spends: sha256(k1) for a Part 1 secret, and for a Part 2 ck1 the
30
+ // key its ownership signature recovers to. Different ck1 strings can recover
31
+ // to the same note, so inputs are compared by ref, never by k1 string.
32
+ const spentRef = (k1) => {
33
+ if (HEX32.test(k1))
34
+ return { id: hashK1(k1), cp1: false };
35
+ const signature = decodeCk1(k1);
36
+ const pubkey = signature ? recoverNoteOwnershipPubkey(signature) : null;
37
+ return pubkey ? { id: bytesToHex(pubkey), cp1: true } : null;
38
+ };
39
+ // LUD-25 renamed `h` to `p` on the lookup and `h`/`h2` to `p1`/`p2` on the
40
+ // callback. The old names stay accepted; sending both spellings must agree.
41
+ const renamedRef = (q, name, legacy) => {
42
+ const current = q.get(name)?.toLowerCase();
43
+ const old = q.get(legacy)?.toLowerCase();
44
+ const fromCurrent = current === undefined ? undefined : namedRef(current);
45
+ const fromOld = old === undefined ? undefined : namedRef(old);
46
+ return {
47
+ present: current !== undefined || old !== undefined,
48
+ ref: (fromCurrent !== undefined ? fromCurrent : fromOld) ?? null,
49
+ conflict: fromCurrent !== undefined && fromOld !== undefined && fromCurrent?.id !== fromOld?.id
50
+ };
51
+ };
22
52
  // The one request body this mint reads. Capped hard: nothing here needs
23
53
  // more than a name and a note, and an unbounded read on a public endpoint
24
54
  // is a way to run a mint out of memory.
@@ -61,6 +91,11 @@ export const createMoneyer = async (config, deps = {}) => {
61
91
  const store = deps.store ?? new NoteStore(config.dbPath);
62
92
  const backend = deps.backend ?? backendFor(config);
63
93
  const signer = createNoteSigner(config.signingKey);
94
+ // A cp1 note's certificate is the same signature, carried as cs1.
95
+ const certify = (ref, amountMsat) => {
96
+ const sig = signer.sign(ref.id, amountMsat);
97
+ return ref.cp1 ? encodeCs1(hexToBytes(sig)) : sig;
98
+ };
64
99
  const webAssets = deps.webAssets === undefined ? loadWebAssets() : deps.webAssets;
65
100
  // Node identity for the discovery endpoint, fetched once, best-effort: a
66
101
  // funding source that cannot answer does not stop the mint serving.
@@ -163,7 +198,6 @@ export const createMoneyer = async (config, deps = {}) => {
163
198
  }
164
199
  return null;
165
200
  };
166
- const resolveNote = async (k1) => resolveNoteId(hashK1(k1));
167
201
  // ---- transparency: what the mint owes, and what the node holds ----
168
202
  //
169
203
  // Cached for 30 seconds so a public endpoint cannot be turned into a
@@ -630,11 +664,15 @@ export const createMoneyer = async (config, deps = {}) => {
630
664
  // commitment. Missing or malformed input is refused before asking the
631
665
  // funding source for an invoice. `h` remains an additive compatibility
632
666
  // field, but it never substitutes for comment.
667
+ //
668
+ // LUD-25 Part 2: the comment may instead be `cp1<pk>`, and the note is
669
+ // keyed by that key, spent later with a ck1 rather than a secret.
633
670
  const askedComment = q.get('comment')?.trim().toLowerCase() ?? null;
634
- if (askedComment === null || !HEX32.test(askedComment)) {
635
- return fail('a mint quote must name its output with a LUD-12 comment carrying hex(sha256(secret))');
671
+ const commentRef = askedComment === null ? null : namedRef(askedComment);
672
+ if (commentRef === null) {
673
+ return fail('a mint quote must name its output with a LUD-12 comment carrying hex(sha256(secret)) or a cp1 key');
636
674
  }
637
- const commentOutputId = askedComment;
675
+ const commentOutputId = commentRef.id;
638
676
  const askedOutputId = q.get('h')?.trim().toLowerCase() ?? null;
639
677
  if (askedOutputId !== null && !HEX32.test(askedOutputId))
640
678
  return fail('missing h');
@@ -823,26 +861,26 @@ export const createMoneyer = async (config, deps = {}) => {
823
861
  // ---- LUD-03 informational GET ----
824
862
  if (requestUrl.pathname === '/w') {
825
863
  const hasK1 = q.has('k1');
826
- const hasH = q.has('h');
827
864
  const k1 = q.get('k1')?.toLowerCase();
828
- const h = q.get('h')?.toLowerCase();
829
- if (hasK1 === hasH ||
830
- (hasK1 && (!k1 || !HEX32.test(k1))) ||
831
- (hasH && (!h || !HEX32.test(h)))) {
865
+ const lookup = renamedRef(q, 'p', 'h');
866
+ if (lookup.conflict)
867
+ return fail('p and h name different notes');
868
+ if (hasK1 === lookup.present)
832
869
  return fail('Unknown note.');
833
- }
834
870
  // LUD-25's hash-only check lets a wallet prove that the note it just
835
871
  // bought exists without sending the bearer secret to the service a
836
- // second time. `h` is accepted in place of `k1`, never alongside it.
837
- const note = h ? await resolveNoteId(h) : await resolveNote(k1);
872
+ // second time. `p` (or `h`) is accepted in place of `k1`, never
873
+ // alongside it, and names a Part 2 note by its cp1 key.
874
+ const ref = hasK1 ? (k1 ? spentRef(k1) : null) : lookup.ref;
875
+ if (!ref)
876
+ return fail('Unknown note.');
877
+ const note = await resolveNoteId(ref.id);
838
878
  if (!note)
839
879
  return fail('Unknown note.');
840
- // A hash-only lookup must not disclose whether a note id once existed:
841
- // LUD-25 gives a never-registered and an already-spent h the same
842
- // response as an unknown k1. A caller presenting the bearer k1 still
843
- // receives the useful already-spent distinction.
880
+ // Hash lookups protect the secret, not the note's spent state.
881
+ // Retained burned rows let a wallet reconcile without revealing k1.
844
882
  if (note.state === 'burned')
845
- return fail(h ? 'Unknown note.' : 'Note already spent.');
883
+ return fail('Note already spent.');
846
884
  // A note reserved by an in-flight melt is not withdrawable, and must
847
885
  // not be advertised as though it were. LUD-25 makes this GET the way
848
886
  // anyone checks what a note is worth, so answering "live, worth all
@@ -878,7 +916,10 @@ export const createMoneyer = async (config, deps = {}) => {
878
916
  // but a note learns from the note itself that the other door
879
917
  // exists.
880
918
  ...(mirrors.length ? { mirrors } : {}),
881
- ...(signer ? { mintPubkey: signer.pubkey } : {})
919
+ ...(signer ? { mintPubkey: signer.pubkey } : {}),
920
+ // LUD-25 Part 2: a note named by cp1 or ck1 gets its certificate
921
+ // here, so a wallet need not rotate just to obtain one.
922
+ ...(ref.cp1 ? { sig: certify(ref, note.amountMsat) } : {})
882
923
  });
883
924
  }
884
925
  // ---- the mutating callback: melt / rotate / split / merge ----
@@ -886,8 +927,12 @@ export const createMoneyer = async (config, deps = {}) => {
886
927
  const k1s = q.getAll('k1').map(value => value.toLowerCase());
887
928
  const pr = q.get('pr');
888
929
  const amountRaw = q.get('amount');
889
- const h = q.get('h')?.toLowerCase();
890
- const h2 = q.get('h2')?.toLowerCase();
930
+ // One ref per output from here on, so the replay match below treats
931
+ // `p1=X` and `h=X` as the same request.
932
+ const out1 = renamedRef(q, 'p1', 'h');
933
+ const out2 = renamedRef(q, 'p2', 'h2');
934
+ const o1 = out1.ref;
935
+ const o2 = out2.ref;
891
936
  if (k1s.length === 0)
892
937
  return fail('Missing k1.');
893
938
  if (k1s.length > config.maxK1s)
@@ -898,19 +943,29 @@ export const createMoneyer = async (config, deps = {}) => {
898
943
  // Hash parameters are checked before any note resolves, so an
899
944
  // invalid or missing hash can never burn anything.
900
945
  if (!pr) {
901
- if (!h || !HEX32.test(h))
902
- return fail('missing h');
903
- if (amountRaw !== null && (!h2 || !HEX32.test(h2)))
904
- return fail('missing h2');
905
- if (h2 !== undefined && h2 === h)
906
- return fail('h and h2 must differ.');
946
+ if (out1.conflict)
947
+ return fail('p1 and h name different outputs');
948
+ if (out2.conflict)
949
+ return fail('p2 and h2 name different outputs');
950
+ if (!o1)
951
+ return fail('missing p1');
952
+ if (amountRaw !== null && !o2)
953
+ return fail('missing p2');
954
+ if (o2 && o2.id === o1.id)
955
+ return fail('p1 and p2 must differ.');
907
956
  }
908
957
  if (config.sunset && amountRaw !== null) {
909
958
  return fail('This mint is sunsetting - splitting is disabled.');
910
959
  }
911
- // A repeated k1 would count one note's value twice on the way into a
912
- // merge or split. Atomic refusal, same as an invalid one.
913
- if (new Set(k1s).size !== k1s.length)
960
+ // A note named twice would count its value twice on the way into a
961
+ // merge or split. Compared by the note each k1 spends, since two
962
+ // different ck1 strings can spend the same one. Atomic refusal, same
963
+ // as an invalid k1.
964
+ const spent = k1s.map(spentRef);
965
+ if (spent.some(ref => ref === null))
966
+ return fail('Invalid or already spent k1.');
967
+ const spentIds = spent.map(ref => ref.id);
968
+ if (new Set(spentIds).size !== spentIds.length)
914
969
  return fail('Invalid or already spent k1.');
915
970
  // ---- the retried mutation ----
916
971
  //
@@ -927,39 +982,36 @@ export const createMoneyer = async (config, deps = {}) => {
927
982
  // balance: it is a read. The signature is deterministic over the
928
983
  // output id and its amount, so it is recomputed rather than stored.
929
984
  // Anything else naming a burned input is a double-spend attempt and
930
- // still gets today's reason string, unchanged.
931
- // Every k1 has to be hex before it can be hashed into a
932
- // fingerprint; a malformed one falls through to the loop below and
933
- // gets the same refusal it always did.
934
- const fingerprint = pr !== null || !k1s.every(k1 => HEX32.test(k1))
985
+ // still gets today's reason string, unchanged. The fingerprint is
986
+ // over note ids, so a hex request fingerprints exactly as it did
987
+ // before Part 2 and replays from before an upgrade still match.
988
+ const fingerprint = pr !== null
935
989
  ? null
936
990
  : swapFingerprint({
937
- inputIds: k1s.map(hashK1),
938
- h: h,
939
- h2,
991
+ inputIds: spentIds,
992
+ h: o1.id,
993
+ h2: o2?.id,
940
994
  ...(amountRaw !== null ? { amountMsat: Number(amountRaw) } : {})
941
995
  });
942
996
  const alreadyMinted = fingerprint === null ? null : store.swapByFingerprint(fingerprint);
943
997
  if (alreadyMinted) {
944
- const first = alreadyMinted.find(output => output.id === h);
945
- const second = h2 === undefined ? undefined : alreadyMinted.find(output => output.id === h2);
998
+ const first = alreadyMinted.find(output => output.id === o1.id);
999
+ const second = o2 ? alreadyMinted.find(output => output.id === o2.id) : undefined;
946
1000
  if (first) {
947
1001
  return send({
948
1002
  status: 'OK',
949
1003
  ...(signer
950
1004
  ? {
951
- sig: signer.sign(first.id, first.amountMsat),
952
- ...(second ? { sig2: signer.sign(second.id, second.amountMsat) } : {})
1005
+ sig: certify(o1, first.amountMsat),
1006
+ ...(second ? { sig2: certify(o2, second.amountMsat) } : {})
953
1007
  }
954
1008
  : {})
955
1009
  });
956
1010
  }
957
1011
  }
958
1012
  const found = [];
959
- for (const k1 of k1s) {
960
- if (!HEX32.test(k1))
961
- return fail('Invalid or already spent k1.');
962
- const note = await resolveNote(k1);
1013
+ for (const id of spentIds) {
1014
+ const note = await resolveNoteId(id);
963
1015
  if (!note || note.state === 'burned')
964
1016
  return fail('Invalid or already spent k1.');
965
1017
  if (note.state === 'pending')
@@ -1074,8 +1126,8 @@ export const createMoneyer = async (config, deps = {}) => {
1074
1126
  return fail('insufficient value');
1075
1127
  try {
1076
1128
  store.swap(inputIds, [
1077
- { id: h, amountMsat: amount },
1078
- { id: h2, amountMsat: changeMsat }
1129
+ { id: o1.id, amountMsat: amount },
1130
+ { id: o2.id, amountMsat: changeMsat }
1079
1131
  ], fingerprint ?? undefined);
1080
1132
  }
1081
1133
  catch (err) {
@@ -1087,7 +1139,7 @@ export const createMoneyer = async (config, deps = {}) => {
1087
1139
  }
1088
1140
  return send({
1089
1141
  status: 'OK',
1090
- ...(signer ? { sig: signer.sign(h, amount), sig2: signer.sign(h2, changeMsat) } : {})
1142
+ ...(signer ? { sig: certify(o1, amount), sig2: certify(o2, changeMsat) } : {})
1091
1143
  });
1092
1144
  }
1093
1145
  // rotate (one k1) or merge (several) - the same swap either way.
@@ -1096,7 +1148,7 @@ export const createMoneyer = async (config, deps = {}) => {
1096
1148
  // of one - the refund is exactly 0.
1097
1149
  const mergedMsat = totalMsat + (inputIds.length - 1) * baseFeeMsat;
1098
1150
  try {
1099
- store.swap(inputIds, [{ id: h, amountMsat: mergedMsat }], fingerprint ?? undefined);
1151
+ store.swap(inputIds, [{ id: o1.id, amountMsat: mergedMsat }], fingerprint ?? undefined);
1100
1152
  }
1101
1153
  catch (err) {
1102
1154
  if (err instanceof NotePendingError)
@@ -1104,7 +1156,7 @@ export const createMoneyer = async (config, deps = {}) => {
1104
1156
  // same oracle-free reason for a collision as for a dead k1
1105
1157
  return fail('Invalid or already spent k1.');
1106
1158
  }
1107
- return send({ status: 'OK', ...(signer ? { sig: signer.sign(h, mergedMsat) } : {}) });
1159
+ return send({ status: 'OK', ...(signer ? { sig: certify(o1, mergedMsat) } : {}) });
1108
1160
  }
1109
1161
  return fail('Not found.', 404);
1110
1162
  };
package/dist/store.js CHANGED
@@ -214,6 +214,11 @@ export class NoteStore {
214
214
  // a secret the mint really did mint a note against.
215
215
  swap(inputIds, outputs, fingerprint) {
216
216
  this.tx(() => {
217
+ // Every input is checked before any is burned, so a note named twice
218
+ // would pass both checks and be counted twice by the caller.
219
+ if (new Set(inputIds).size !== inputIds.length) {
220
+ throw new NoteUnavailableError('a note is named twice among the inputs');
221
+ }
217
222
  for (const id of inputIds)
218
223
  this.assertOutstanding(id);
219
224
  for (const output of outputs)
package/llms.txt CHANGED
@@ -25,10 +25,13 @@ Client library to build against a mint: lnurlcash-kit
25
25
  inside the invoice. `h` is an additive alias for the same commitment and
26
26
  never replaces the comment. Because the note is not keyed by the
27
27
  preimage, a funding source whose node chooses its own preimages
28
- (phoenixd, NIP-47 make_invoice) can back a mint.
28
+ (phoenixd, NIP-47 make_invoice) can back a mint. LUD-25 Part 2: the
29
+ comment may instead be `cp1<pk>`, and the note is keyed by that key.
29
30
  2. Mutations are atomic: all named k1 burn and all outputs mint, or
30
- nothing happens. Duplicate k1 in one request is refused.
31
- 3. h/h2 are WALLET-supplied hashes; the mint never generates a
31
+ nothing happens. A note named twice in one request is refused, compared
32
+ by the note each k1 spends: one Part 2 note has many valid ck1 strings.
33
+ 3. p1/p2 (older names h/h2, still accepted; both spellings of one output
34
+ must agree) are WALLET-supplied hashes or cp1 keys; the mint never generates a
32
35
  replacement secret, and refuses output ids that already exist as
33
36
  notes, as mint-invoice payment hashes, or as a note another payer has
34
37
  already bought with `h`. A malformed `h` on the pay callback is
@@ -56,8 +59,8 @@ Client library to build against a mint: lnurlcash-kit
56
59
  part applies at mint time only.
57
60
  7. Reason strings are protocol surface: 'pending',
58
61
  'Invalid or already spent k1.', 'Unknown note.', 'Note already spent.',
59
- 'missing h', 'missing h2', 'insufficient value' - wallets classify on
60
- them.
62
+ 'missing p1', 'missing p2' (withdraw callback), 'missing h' (pay
63
+ callback), 'insufficient value' - wallets classify on them.
61
64
  8. minSendable must survive the mint's own fee (grossUpForMintFee of the
62
65
  dust floor), or the mint advertises an amount it will refuse.
63
66
 
@@ -84,7 +87,7 @@ NoteStore - SQLite store; notes by sha256(k1), never secrets
84
87
 
85
88
  /.well-known/lnurlp/{user} LUD-16 payRequest (mints)
86
89
  /.well-known/lnurlw/{user} LUD-25 mint address (experimental)
87
- /p/cb LUD-06 pay callback (optional h names the note)
90
+ /p/cb LUD-06 pay callback (comment names the note: hash or cp1)
88
91
  /verify/{payment_hash} LUD-21 verify (mint invoices AND melt payments)
89
- /w LUD-03 informational GET by k1 or hash-only h
90
- /w/cb melt / rotate / split / merge
92
+ /w LUD-03 informational GET by k1 (secret or ck1) or p (hash or cp1, older h); a cp1 note carries its cs1 as sig
93
+ /w/cb melt / rotate / split / merge; inputs secrets or ck1, outputs p1/p2 hashes or cp1 (or older h/h2); cp1 outputs certified as cs1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
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",
@@ -60,7 +60,7 @@
60
60
  "@noble/hashes": "^2.3.0",
61
61
  "@scure/base": "^1.2.4",
62
62
  "farrier-kit": "^1.1.3",
63
- "lnurlcash-kit": "^0.7.0",
63
+ "lnurlcash-kit": "^0.10.0",
64
64
  "nostr-tools": "2.24.1"
65
65
  },
66
66
  "devDependencies": {
@@ -70,7 +70,7 @@
70
70
  "@types/node": "^24.0.0",
71
71
  "animejs": "^4.0.0",
72
72
  "happy-dom": "^20.0.0",
73
- "lnurlcash-conformance": "^0.7.0",
73
+ "lnurlcash-conformance": "^0.8.0",
74
74
  "playwright": "^1.62.1",
75
75
  "typescript": "^5.7.0",
76
76
  "uqr": "^0.1.2",