@forgesworn/moneyer 0.11.1 → 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,27 @@
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
+
3
25
  ## 0.11.1 - 2026-09-07
4
26
 
5
27
  - Hash lookups report `Note already spent.` for retained burned notes,
@@ -9,6 +31,9 @@
9
31
  burned rows are retained already; no database migration is required.
10
32
  - Test tooling uses an immutable conformance commit matching this response
11
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.
12
37
 
13
38
  ## 0.11.0 - 2026-09-06
14
39
 
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,18 +861,20 @@ 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
880
  // Hash lookups protect the secret, not the note's spent state.
@@ -876,7 +916,10 @@ export const createMoneyer = async (config, deps = {}) => {
876
916
  // but a note learns from the note itself that the other door
877
917
  // exists.
878
918
  ...(mirrors.length ? { mirrors } : {}),
879
- ...(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) } : {})
880
923
  });
881
924
  }
882
925
  // ---- the mutating callback: melt / rotate / split / merge ----
@@ -884,8 +927,12 @@ export const createMoneyer = async (config, deps = {}) => {
884
927
  const k1s = q.getAll('k1').map(value => value.toLowerCase());
885
928
  const pr = q.get('pr');
886
929
  const amountRaw = q.get('amount');
887
- const h = q.get('h')?.toLowerCase();
888
- 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;
889
936
  if (k1s.length === 0)
890
937
  return fail('Missing k1.');
891
938
  if (k1s.length > config.maxK1s)
@@ -896,19 +943,29 @@ export const createMoneyer = async (config, deps = {}) => {
896
943
  // Hash parameters are checked before any note resolves, so an
897
944
  // invalid or missing hash can never burn anything.
898
945
  if (!pr) {
899
- if (!h || !HEX32.test(h))
900
- return fail('missing h');
901
- if (amountRaw !== null && (!h2 || !HEX32.test(h2)))
902
- return fail('missing h2');
903
- if (h2 !== undefined && h2 === h)
904
- 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.');
905
956
  }
906
957
  if (config.sunset && amountRaw !== null) {
907
958
  return fail('This mint is sunsetting - splitting is disabled.');
908
959
  }
909
- // A repeated k1 would count one note's value twice on the way into a
910
- // merge or split. Atomic refusal, same as an invalid one.
911
- 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)
912
969
  return fail('Invalid or already spent k1.');
913
970
  // ---- the retried mutation ----
914
971
  //
@@ -925,39 +982,36 @@ export const createMoneyer = async (config, deps = {}) => {
925
982
  // balance: it is a read. The signature is deterministic over the
926
983
  // output id and its amount, so it is recomputed rather than stored.
927
984
  // Anything else naming a burned input is a double-spend attempt and
928
- // still gets today's reason string, unchanged.
929
- // Every k1 has to be hex before it can be hashed into a
930
- // fingerprint; a malformed one falls through to the loop below and
931
- // gets the same refusal it always did.
932
- 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
933
989
  ? null
934
990
  : swapFingerprint({
935
- inputIds: k1s.map(hashK1),
936
- h: h,
937
- h2,
991
+ inputIds: spentIds,
992
+ h: o1.id,
993
+ h2: o2?.id,
938
994
  ...(amountRaw !== null ? { amountMsat: Number(amountRaw) } : {})
939
995
  });
940
996
  const alreadyMinted = fingerprint === null ? null : store.swapByFingerprint(fingerprint);
941
997
  if (alreadyMinted) {
942
- const first = alreadyMinted.find(output => output.id === h);
943
- 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;
944
1000
  if (first) {
945
1001
  return send({
946
1002
  status: 'OK',
947
1003
  ...(signer
948
1004
  ? {
949
- sig: signer.sign(first.id, first.amountMsat),
950
- ...(second ? { sig2: signer.sign(second.id, second.amountMsat) } : {})
1005
+ sig: certify(o1, first.amountMsat),
1006
+ ...(second ? { sig2: certify(o2, second.amountMsat) } : {})
951
1007
  }
952
1008
  : {})
953
1009
  });
954
1010
  }
955
1011
  }
956
1012
  const found = [];
957
- for (const k1 of k1s) {
958
- if (!HEX32.test(k1))
959
- return fail('Invalid or already spent k1.');
960
- const note = await resolveNote(k1);
1013
+ for (const id of spentIds) {
1014
+ const note = await resolveNoteId(id);
961
1015
  if (!note || note.state === 'burned')
962
1016
  return fail('Invalid or already spent k1.');
963
1017
  if (note.state === 'pending')
@@ -1072,8 +1126,8 @@ export const createMoneyer = async (config, deps = {}) => {
1072
1126
  return fail('insufficient value');
1073
1127
  try {
1074
1128
  store.swap(inputIds, [
1075
- { id: h, amountMsat: amount },
1076
- { id: h2, amountMsat: changeMsat }
1129
+ { id: o1.id, amountMsat: amount },
1130
+ { id: o2.id, amountMsat: changeMsat }
1077
1131
  ], fingerprint ?? undefined);
1078
1132
  }
1079
1133
  catch (err) {
@@ -1085,7 +1139,7 @@ export const createMoneyer = async (config, deps = {}) => {
1085
1139
  }
1086
1140
  return send({
1087
1141
  status: 'OK',
1088
- ...(signer ? { sig: signer.sign(h, amount), sig2: signer.sign(h2, changeMsat) } : {})
1142
+ ...(signer ? { sig: certify(o1, amount), sig2: certify(o2, changeMsat) } : {})
1089
1143
  });
1090
1144
  }
1091
1145
  // rotate (one k1) or merge (several) - the same swap either way.
@@ -1094,7 +1148,7 @@ export const createMoneyer = async (config, deps = {}) => {
1094
1148
  // of one - the refund is exactly 0.
1095
1149
  const mergedMsat = totalMsat + (inputIds.length - 1) * baseFeeMsat;
1096
1150
  try {
1097
- store.swap(inputIds, [{ id: h, amountMsat: mergedMsat }], fingerprint ?? undefined);
1151
+ store.swap(inputIds, [{ id: o1.id, amountMsat: mergedMsat }], fingerprint ?? undefined);
1098
1152
  }
1099
1153
  catch (err) {
1100
1154
  if (err instanceof NotePendingError)
@@ -1102,7 +1156,7 @@ export const createMoneyer = async (config, deps = {}) => {
1102
1156
  // same oracle-free reason for a collision as for a dead k1
1103
1157
  return fail('Invalid or already spent k1.');
1104
1158
  }
1105
- return send({ status: 'OK', ...(signer ? { sig: signer.sign(h, mergedMsat) } : {}) });
1159
+ return send({ status: 'OK', ...(signer ? { sig: certify(o1, mergedMsat) } : {}) });
1106
1160
  }
1107
1161
  return fail('Not found.', 404);
1108
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.1",
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": "https://codeload.github.com/lnurlcash/lnurlcash-conformance/tar.gz/6c6aee66cf1b6cba72a03af976ecec113f470c9e",
73
+ "lnurlcash-conformance": "^0.8.0",
74
74
  "playwright": "^1.62.1",
75
75
  "typescript": "^5.7.0",
76
76
  "uqr": "^0.1.2",