@forgesworn/moneyer 0.20.0 → 0.21.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 +29 -0
- package/README.md +6 -7
- package/dist/names.js +2 -2
- package/dist/server.js +9 -9
- package/dist/zap.d.ts +1 -1
- package/dist/zap.js +26 -7
- package/llms.txt +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 - 2026-10-16
|
|
4
|
+
|
|
5
|
+
- **Certificates go out as `c` and `c2` only.** The `sig`/`sig2` that had
|
|
6
|
+
carried the same value alongside them since 0.17.0 are dropped, as dni
|
|
7
|
+
asked. The draft never named them, the reference mint emits only `c`/`c2`,
|
|
8
|
+
and every wallet that verifies a moneyer certificate reads `c` first:
|
|
9
|
+
notecase since 0.25.0, heartwood since 0.18.0-beta.24, lnurl-wallet,
|
|
10
|
+
lnurlcash-core and its Python and Go ports. The bound-mint receipt on
|
|
11
|
+
`/verify` is a different field and keeps its name.
|
|
12
|
+
|
|
13
|
+
## 0.20.1 - 2026-10-13
|
|
14
|
+
|
|
15
|
+
- **A name with a cx1 issues invoices its payer's wallet will accept.** Its
|
|
16
|
+
payRequest metadata carries `text/cpub` with the next free index, but the
|
|
17
|
+
invoice's description hash was taken over the metadata without it, so it
|
|
18
|
+
never matched `sha256(metadata)` as LUD-06 requires. A wallet that checks
|
|
19
|
+
refused to pay the name by Lightning. The callback is now
|
|
20
|
+
`/z/cb/<name>/<index>`, the index the payRequest advertised, and the
|
|
21
|
+
invoice commits to exactly the metadata the payer read, even when a
|
|
22
|
+
settled payment has moved the next free index on in between. The index
|
|
23
|
+
feeds only the metadata: the note still lands on the next free key at
|
|
24
|
+
settlement. An index that is not a non-hardened one is refused. Zaps were
|
|
25
|
+
unaffected (their hash is over the zap request), and so is a name without
|
|
26
|
+
a cx1, whose callback stays `/z/cb/<name>`; that form still answers for
|
|
27
|
+
every name.
|
|
28
|
+
- The derivation, address proof and auto-mint text moved from LUD-25 to
|
|
29
|
+
LUD-26 in the draft (lnurl/luds 2f17578, editorial); the README and
|
|
30
|
+
comments now point there.
|
|
31
|
+
|
|
3
32
|
## 0.20.0 - 2026-10-07
|
|
4
33
|
|
|
5
34
|
- **A zap paid while the mint was down is no longer lost.** The expiry
|
package/README.md
CHANGED
|
@@ -393,9 +393,8 @@ spend is refused with a reason and the note stays outstanding; a verifier
|
|
|
393
393
|
that crashes, errors or stalls refuses too, and never accepts.
|
|
394
394
|
|
|
395
395
|
Every note gets a certificate, `cs1` over hex(Q) and its amount: `c` on
|
|
396
|
-
the informational GET, `c`/`c2` on every rotate, split and merge
|
|
397
|
-
|
|
398
|
-
those have moved on. The
|
|
396
|
+
the informational GET, `c`/`c2` on every rotate, split and merge, and
|
|
397
|
+
under no other name. The
|
|
399
398
|
informational GET verifies a `k1` in full before answering, so a wallet
|
|
400
399
|
checking a note it was handed learns whether its spend really opens it.
|
|
401
400
|
|
|
@@ -584,7 +583,7 @@ A registered name resolves on both rails at once:
|
|
|
584
583
|
The discovery endpoint advertises `namePriceMsat` while registration is
|
|
585
584
|
open, so a wallet can offer the flow without asking.
|
|
586
585
|
|
|
587
|
-
### A name that pays to your own keys (LUD-
|
|
586
|
+
### A name that pays to your own keys (LUD-26 auto-mint)
|
|
588
587
|
|
|
589
588
|
Add `"cx1": "cx1..."` to the registration body, or send `{"name", "cx1"}`
|
|
590
589
|
again later from the key that owns the name, and payments to it stop being
|
|
@@ -592,7 +591,7 @@ custodial. A `cx1` is a watch-only branch: from it the mint can work out each
|
|
|
592
591
|
of the holder's note keys in turn, but never spend one.
|
|
593
592
|
|
|
594
593
|
A `cx1` is public, so pointing a name at one has to be proven by the branch
|
|
595
|
-
itself, as LUD-
|
|
594
|
+
itself, as LUD-26 requires: the body also carries `"sig"`, a BIP-340
|
|
596
595
|
signature by the branch's purpose-0 index-0 key over
|
|
597
596
|
`sha256("LNURLcash:register:<domain>:<name>")`, `<domain>` being this mint's
|
|
598
597
|
hostname. Changing the branch later is proven by the branch on file, not the
|
|
@@ -690,9 +689,9 @@ clearnet.
|
|
|
690
689
|
| `/.well-known/lnurlp/<zap name>` | NIP-57 payRequest; paying mints a note *to the name's pubkey* |
|
|
691
690
|
| `/.well-known/lnurlw/<user>` | LUD-25 mint address discovery (experimental) |
|
|
692
691
|
| `/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) |
|
|
693
|
-
| `/z/cb/<zap name>` | the zap callback; validates the kind 9734 and issues the invoice |
|
|
692
|
+
| `/z/cb/<zap name>` | the zap callback; validates the kind 9734 and issues the invoice. A name with a `cx1` calls back at `/z/cb/<zap name>/<index>`, the index its payRequest advertised, so the invoice commits to exactly the metadata the payer was shown |
|
|
694
693
|
| `/verify/<hash>` | LUD-21 verify, for mint invoices and melt payments |
|
|
695
|
-
| `/w` | LUD-03 informational GET; accepts `k1` (a preimage, `ck1` or `cw1`, verified in full) or the non-disclosing LUD-25 `p` check (a bearer note's h or any `cp1`; `h` is its older name, still accepted); a live note carries `payLink` and its `cs1` as `
|
|
694
|
+
| `/w` | LUD-03 informational GET; accepts `k1` (a preimage, `ck1` or `cw1`, verified in full) or the non-disclosing LUD-25 `p` check (a bearer note's h or any `cp1`; `h` is its older name, still accepted); a live note carries `payLink` and its `cs1` as `c` |
|
|
696
695
|
| `/w/cb` | the mutating callback: melt, rotate, split, merge; inputs as preimages, `ck1`s or `cw1`s, outputs as `p1`/`p2` (a bearer note's h or a `cp1`), or their older names `h`/`h2` |
|
|
697
696
|
| `POST /names` | claim a lightning address, authenticated by NIP-98 |
|
|
698
697
|
| `/.well-known/nostr.json` | NIP-05 for the names this mint serves |
|
package/dist/names.js
CHANGED
|
@@ -87,7 +87,7 @@ export const validateNip98 = (authorization, request) => {
|
|
|
87
87
|
}
|
|
88
88
|
return { pubkey: event.pubkey };
|
|
89
89
|
};
|
|
90
|
-
// LUD-
|
|
90
|
+
// LUD-26's address proof. A cx1 is public, so pointing a name at one, or
|
|
91
91
|
// away from one, has to be proven by the branch itself: a BIP-340
|
|
92
92
|
// signature by its index-0 key over sha256("LNURLcash:<action>:<domain>:
|
|
93
93
|
// <name>"). The domain is this mint's own, so a proof one mint has seen
|
|
@@ -132,7 +132,7 @@ export const registerName = async (args) => {
|
|
|
132
132
|
if ([...ALWAYS_RESERVED, ...args.reserved.map(name => name.toLowerCase())].includes(raw)) {
|
|
133
133
|
return refuse('That name is reserved.', 403);
|
|
134
134
|
}
|
|
135
|
-
// LUD-
|
|
135
|
+
// LUD-26 auto-mint: a watch-only branch, so payments to the name are minted
|
|
136
136
|
// to the holder's own keys. Absent leaves it as it is; null clears it.
|
|
137
137
|
let cx1;
|
|
138
138
|
if (args.body.cx1 === null) {
|
package/dist/server.js
CHANGED
|
@@ -97,13 +97,12 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
97
97
|
// LUD-25 offline verification: a cs1 over hex(Q) and the amount, for
|
|
98
98
|
// every note, bearer notes included.
|
|
99
99
|
const certify = (q, amountMsat) => encodeCs1WithAmount(amountMsat, hexToBytes(signer.sign(q, amountMsat)));
|
|
100
|
-
// LUD-25 names the certificate `c` (and `c2` for split change). The
|
|
101
|
-
// `sig`/`sig2`
|
|
102
|
-
//
|
|
103
|
-
const certified = (field, q, amountMsat) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
100
|
+
// LUD-25 names the certificate `c` (and `c2` for split change). The
|
|
101
|
+
// `sig`/`sig2` that went out alongside them from 0.17.0 to 0.20.1 are
|
|
102
|
+
// gone: the draft never named them, and every wallet reads `c`.
|
|
103
|
+
const certified = (field, q, amountMsat) => ({
|
|
104
|
+
[field]: certify(q, amountMsat)
|
|
105
|
+
});
|
|
107
106
|
// Every host this mint answers on. A spend's signature is bound to one
|
|
108
107
|
// domain, and a note travels as a URL naming one host, so a signature
|
|
109
108
|
// bound to any of this mint's own hosts is this mint's. Without a
|
|
@@ -615,13 +614,14 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
615
614
|
if (lnurlpMatch && zap && zap.isZapName(lnurlpMatch[1])) {
|
|
616
615
|
return send(zap.payRequest(lnurlpMatch[1]));
|
|
617
616
|
}
|
|
618
|
-
|
|
617
|
+
// `/z/cb/<name>`, or `/z/cb/<name>/<index>` for a name advertising a cx1.
|
|
618
|
+
const zapCbMatch = requestUrl.pathname.match(/^\/z\/cb\/([^/]+)(?:\/([^/]+))?$/);
|
|
619
619
|
if (zapCbMatch) {
|
|
620
620
|
if (!zap || !zap.isZapName(zapCbMatch[1]))
|
|
621
621
|
return fail('Unknown user.', 404);
|
|
622
622
|
if (config.sunset)
|
|
623
623
|
return fail('This mint is sunsetting - minting is disabled.');
|
|
624
|
-
const result = await zap.callback(zapCbMatch[1], Number(q.get('amount')), q.get('nostr'));
|
|
624
|
+
const result = await zap.callback(zapCbMatch[1], Number(q.get('amount')), q.get('nostr'), zapCbMatch[2] ?? null);
|
|
625
625
|
if ('reason' in result)
|
|
626
626
|
return fail(result.reason);
|
|
627
627
|
return send({ ...result, disposable: false });
|
package/dist/zap.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type ZapBridge = {
|
|
|
45
45
|
pubkey: string;
|
|
46
46
|
isZapName(name: string): boolean;
|
|
47
47
|
payRequest(name: string): Record<string, unknown> | null;
|
|
48
|
-
callback(name: string, amountMsat: number, nostrParam: string | null): Promise<ZapCallbackResult>;
|
|
48
|
+
callback(name: string, amountMsat: number, nostrParam: string | null, advertisedIndex?: string | null): Promise<ZapCallbackResult>;
|
|
49
49
|
settle(): Promise<number>;
|
|
50
50
|
publish(): Promise<number>;
|
|
51
51
|
sweep(nowMs?: number): Promise<number>;
|
package/dist/zap.js
CHANGED
|
@@ -111,7 +111,7 @@ export const createZapBridge = (deps) => {
|
|
|
111
111
|
const registered = store.zapName(lowered);
|
|
112
112
|
if (!registered)
|
|
113
113
|
return null;
|
|
114
|
-
let
|
|
114
|
+
let advertisedIndex;
|
|
115
115
|
const branch = decodeCx1(registered.cx1 ?? '');
|
|
116
116
|
if (branch) {
|
|
117
117
|
let index = registered.nextIndex;
|
|
@@ -119,7 +119,7 @@ export const createZapBridge = (deps) => {
|
|
|
119
119
|
try {
|
|
120
120
|
const noteId = bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, NOTE_PURPOSE_LIGHTNING_ADDRESS, index));
|
|
121
121
|
if (!store.outputIdInUse(noteId)) {
|
|
122
|
-
|
|
122
|
+
advertisedIndex = index;
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -128,9 +128,15 @@ export const createZapBridge = (deps) => {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
const internalTransfer = advertisedIndex === undefined ? undefined : `${registered.cx1}:${advertisedIndex}`;
|
|
131
132
|
return {
|
|
132
133
|
tag: 'payRequest',
|
|
133
|
-
callback
|
|
134
|
+
// The index rides in the callback path so the invoice can commit to
|
|
135
|
+
// this exact metadata (LUD-06): by the time the payer calls back, a
|
|
136
|
+
// settled payment may have moved the next unused index on. A path
|
|
137
|
+
// segment rather than a query parameter, because some zap clients
|
|
138
|
+
// append `?amount=` without looking for an existing query.
|
|
139
|
+
callback: `${origin}/z/cb/${lowered}${advertisedIndex === undefined ? '' : `/${advertisedIndex}`}`,
|
|
134
140
|
minSendable: deps.minSendableMsat,
|
|
135
141
|
maxSendable: deps.maxSendableMsat,
|
|
136
142
|
metadata: metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords, internalTransfer),
|
|
@@ -141,11 +147,16 @@ export const createZapBridge = (deps) => {
|
|
|
141
147
|
// is one.
|
|
142
148
|
};
|
|
143
149
|
};
|
|
144
|
-
const callback = async (name, amountMsat, nostrParam) => {
|
|
150
|
+
const callback = async (name, amountMsat, nostrParam, advertisedIndex = null) => {
|
|
145
151
|
const lowered = name.toLowerCase();
|
|
146
|
-
const
|
|
147
|
-
|
|
152
|
+
const registered = store.zapName(lowered);
|
|
153
|
+
const recipient = registered?.pubkey;
|
|
154
|
+
if (!registered || !recipient)
|
|
148
155
|
return { reason: 'Unknown user.' };
|
|
156
|
+
// Below 2^31: a non-hardened index, as the derivation's ser32 needs.
|
|
157
|
+
if (advertisedIndex !== null && (!/^\d{1,10}$/.test(advertisedIndex) || Number(advertisedIndex) > 0x7fffffff)) {
|
|
158
|
+
return { reason: 'Invalid callback.' };
|
|
159
|
+
}
|
|
149
160
|
if (!Number.isSafeInteger(amountMsat) || amountMsat <= 0)
|
|
150
161
|
return { reason: 'Invalid amount.' };
|
|
151
162
|
if (amountMsat < deps.minSendableMsat || amountMsat > deps.maxSendableMsat)
|
|
@@ -174,13 +185,21 @@ export const createZapBridge = (deps) => {
|
|
|
174
185
|
// nothing to them. The note's secret is minted at settlement.
|
|
175
186
|
const preimage = bytesToHex(randomBytes(32));
|
|
176
187
|
const paymentHash = hashK1(preimage);
|
|
188
|
+
// A zap commits to its zap request (NIP-57); any other payment to the
|
|
189
|
+
// metadata the payer was shown (LUD-06), rebuilt from the index the
|
|
190
|
+
// payRequest advertised. That index is only a hint and feeds nothing
|
|
191
|
+
// but this string: settlement derives the note's key itself, so a
|
|
192
|
+
// forged one changes no more than the forger's own invoice. A cx1
|
|
193
|
+
// changed or cleared between the two requests still mismatches, and
|
|
194
|
+
// the payer's wallet fetches the payRequest again.
|
|
195
|
+
const internalTransfer = advertisedIndex !== null && registered.cx1 ? `${registered.cx1}:${Number(advertisedIndex)}` : undefined;
|
|
177
196
|
let pr;
|
|
178
197
|
try {
|
|
179
198
|
pr = (await backend.createInvoice({
|
|
180
199
|
amountMsat,
|
|
181
200
|
preimageHex: preimage,
|
|
182
201
|
memo: `Zap ${lowered}@${host}`,
|
|
183
|
-
descriptionForHash: zapRequest ?? metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords)
|
|
202
|
+
descriptionForHash: zapRequest ?? metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords, internalTransfer)
|
|
184
203
|
})).pr;
|
|
185
204
|
}
|
|
186
205
|
catch (err) {
|
package/llms.txt
CHANGED
|
@@ -131,4 +131,7 @@ POST /names claim a name (NIP-98; the signer owns it); body {nam
|
|
|
131
131
|
/w?p=<cp1>&c=<amount-bearing-cs1>&i=<index> plus an `i` tag. The index is
|
|
132
132
|
taken at settlement, never at the invoice, and occupied keys are skipped
|
|
133
133
|
Its payRequest metadata publishes text/cpub = cx1:<next-free-purpose-2-index> for
|
|
134
|
-
same-mint rotate/split/merge transfers; a raced key returns "already in use"
|
|
134
|
+
same-mint rotate/split/merge transfers; a raced key returns "already in use".
|
|
135
|
+
Such a name's callback is /z/cb/{name}/{index}: the index it advertised, so a
|
|
136
|
+
non-zap invoice's description hash is sha256 of that exact metadata (a zap's
|
|
137
|
+
is of its kind 9734). The index feeds only the metadata, never the note's key
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|