@forgesworn/moneyer 0.1.2 → 0.2.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 +170 -0
- package/README.md +447 -11
- package/THREAT-MODEL.md +15 -6
- package/dist/admin.d.ts +12 -0
- package/dist/admin.js +410 -0
- package/dist/announce.d.ts +12 -0
- package/dist/announce.js +82 -0
- package/dist/backends/cln.js +30 -8
- package/dist/backends/fake-bolt11.d.ts +1 -1
- package/dist/backends/fake-bolt11.js +3 -1
- package/dist/backends/fake.d.ts +7 -1
- package/dist/backends/fake.js +27 -11
- package/dist/backends/lnd.js +30 -6
- package/dist/backends/types.d.ts +3 -0
- package/dist/cli.js +25 -3
- package/dist/config.d.ts +29 -0
- package/dist/config.js +194 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +42 -2
- package/dist/melt.d.ts +1 -0
- package/dist/melt.js +2 -1
- package/dist/names.d.ts +39 -0
- package/dist/names.js +172 -0
- package/dist/privacy.d.ts +2 -0
- package/dist/privacy.js +14 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +561 -41
- package/dist/signing.d.ts +2 -0
- package/dist/signing.js +34 -0
- package/dist/stats.d.ts +31 -0
- package/dist/stats.js +115 -0
- package/dist/store.d.ts +105 -3
- package/dist/store.js +355 -19
- package/dist/version.d.ts +1 -0
- package/dist/version.js +17 -0
- package/dist/zap.d.ts +52 -0
- package/dist/zap.js +294 -0
- package/llms.txt +25 -8
- package/package.json +4 -3
package/dist/zap.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
2
|
+
import { hashK1 } from 'lnurlcash-kit';
|
|
3
|
+
import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
|
|
4
|
+
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
|
5
|
+
import { SimplePool } from 'nostr-tools/pool';
|
|
6
|
+
import { makeZapReceipt, validateZapRequest } from 'nostr-tools/nip57';
|
|
7
|
+
import { wrapEvent } from 'nostr-tools/nip59';
|
|
8
|
+
// Zap-to-note: a lightning address on this host that pays out as an
|
|
9
|
+
// LNURLcash note delivered over Nostr.
|
|
10
|
+
//
|
|
11
|
+
// A NIP-57 zap is an ordinary LNURL-pay. Paying the mint's own address
|
|
12
|
+
// would mint a note, but to the PAYER: LUD-25 makes the invoice preimage
|
|
13
|
+
// the secret, and on Lightning the payer always learns the preimage. So a
|
|
14
|
+
// zap name works the other way round. The invoice gets a throwaway
|
|
15
|
+
// preimage; on settlement the mint creates a note with a fresh secret of
|
|
16
|
+
// its own, gift-wraps it (NIP-59, kind 2525 rumor) to the name's pubkey,
|
|
17
|
+
// leaves it on their NIP-17 inbox relays, and publishes the kind 9735
|
|
18
|
+
// receipt that makes the zap show up in clients.
|
|
19
|
+
//
|
|
20
|
+
// Until the recipient rotates the note, the mint knows its secret. That is
|
|
21
|
+
// exactly the position a freshly minted note is in anyway, and it is why
|
|
22
|
+
// wallets rotate on receipt. What is new is that the mint learns who was
|
|
23
|
+
// paid, which a lightning address always did.
|
|
24
|
+
export const NOTE_KIND = 2525;
|
|
25
|
+
export const INBOX_RELAYS_KIND = 10050;
|
|
26
|
+
export const ZAP_REQUEST_KIND = 9734;
|
|
27
|
+
// Where a recipient's kind 10050 is looked for, beyond the mint's own relays.
|
|
28
|
+
export const INDEXER_RELAYS = ['wss://purplepag.es', 'wss://relay.damus.io', 'wss://nos.lol'];
|
|
29
|
+
// How long a settled zap keeps retrying the recipient's inbox relays
|
|
30
|
+
// before it settles for whichever took the wrap. The receiver reads ONE
|
|
31
|
+
// of its inbox relays, so a wrap that reached three others is still
|
|
32
|
+
// undelivered. Seen live: a long-lived pool's dead socket to the device's
|
|
33
|
+
// relay failed silently for hours while the other relays said OK.
|
|
34
|
+
export const INBOX_RETRY_MS = 10 * 60_000;
|
|
35
|
+
export const poolTransport = () => {
|
|
36
|
+
const pool = new SimplePool();
|
|
37
|
+
const used = new Set();
|
|
38
|
+
return {
|
|
39
|
+
async publish(relays, event) {
|
|
40
|
+
for (const r of relays)
|
|
41
|
+
used.add(r);
|
|
42
|
+
// A relay whose socket died reports failure forever on a pool that
|
|
43
|
+
// keeps the stale Relay object; drop it so the next publish dials
|
|
44
|
+
// afresh.
|
|
45
|
+
const results = await Promise.allSettled(pool.publish(relays, event));
|
|
46
|
+
results.forEach((r, i) => {
|
|
47
|
+
if (r.status === 'rejected') {
|
|
48
|
+
try {
|
|
49
|
+
pool.close([relays[i]]);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// nothing to close
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const ok = [];
|
|
57
|
+
const failed = [];
|
|
58
|
+
results.forEach((r, i) => (r.status === 'fulfilled' ? ok : failed).push(relays[i]));
|
|
59
|
+
return { ok, failed };
|
|
60
|
+
},
|
|
61
|
+
async query(relays, filter) {
|
|
62
|
+
for (const r of relays)
|
|
63
|
+
used.add(r);
|
|
64
|
+
try {
|
|
65
|
+
return await pool.querySync(relays, filter, { maxWait: 6_000 });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
close() {
|
|
72
|
+
pool.close([...used]);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export const inboxRelays = async (transport, pubkey, lookOn) => {
|
|
77
|
+
const events = await transport.query(lookOn, { kinds: [INBOX_RELAYS_KIND], authors: [pubkey], limit: 3 });
|
|
78
|
+
const latest = events.sort((a, b) => b.created_at - a.created_at)[0];
|
|
79
|
+
if (!latest)
|
|
80
|
+
return [];
|
|
81
|
+
return [...new Set(latest.tags.filter(t => t[0] === 'relay' && t[1]).map(t => t[1]))];
|
|
82
|
+
};
|
|
83
|
+
const metadataFor = (name, host, mintFeeLine, feeInWords = null) => {
|
|
84
|
+
const metadata = [
|
|
85
|
+
['text/plain', `Zap ${name}@${host}: arrives as a Lightning bearer note${feeInWords ? ` (${feeInWords})` : ''}`],
|
|
86
|
+
['text/identifier', `${name}@${host}`]
|
|
87
|
+
];
|
|
88
|
+
if (mintFeeLine)
|
|
89
|
+
metadata.push(['text/plain', mintFeeLine]);
|
|
90
|
+
return JSON.stringify(metadata);
|
|
91
|
+
};
|
|
92
|
+
const tagValues = (event, name) => event.tags.filter(t => t[0] === name && t[1]).map(t => t[1]);
|
|
93
|
+
export const createZapBridge = (deps) => {
|
|
94
|
+
const { config, store, backend, transport } = deps;
|
|
95
|
+
const log = deps.log ?? (() => { });
|
|
96
|
+
const now = deps.now ?? (() => Date.now());
|
|
97
|
+
const secret = hexToBytes(config.nostrKey);
|
|
98
|
+
const pubkey = getPublicKey(secret);
|
|
99
|
+
const origin = deps.origin;
|
|
100
|
+
const host = new URL(origin).host;
|
|
101
|
+
// Names live in the store, not in the configuration: the operator's own
|
|
102
|
+
// are loaded there at startup and self-service registrations land in
|
|
103
|
+
// the same table, so one lookup serves both.
|
|
104
|
+
const isZapName = (name) => store.zapName(name.toLowerCase()) !== null;
|
|
105
|
+
const payRequest = (name) => {
|
|
106
|
+
const lowered = name.toLowerCase();
|
|
107
|
+
if (!isZapName(lowered))
|
|
108
|
+
return null;
|
|
109
|
+
return {
|
|
110
|
+
tag: 'payRequest',
|
|
111
|
+
callback: `${origin}/z/cb/${lowered}`,
|
|
112
|
+
minSendable: deps.minSendableMsat,
|
|
113
|
+
maxSendable: deps.maxSendableMsat,
|
|
114
|
+
metadata: metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords),
|
|
115
|
+
allowsNostr: true,
|
|
116
|
+
nostrPubkey: pubkey
|
|
117
|
+
// Deliberately no withdrawLink: the preimage of this invoice is NOT
|
|
118
|
+
// a note, and an LNURLcash wallet that paid this must not think it
|
|
119
|
+
// is one.
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
const callback = async (name, amountMsat, nostrParam) => {
|
|
123
|
+
const lowered = name.toLowerCase();
|
|
124
|
+
const recipient = store.zapName(lowered)?.pubkey;
|
|
125
|
+
if (!recipient)
|
|
126
|
+
return { reason: 'Unknown user.' };
|
|
127
|
+
if (!Number.isSafeInteger(amountMsat) || amountMsat <= 0)
|
|
128
|
+
return { reason: 'Invalid amount.' };
|
|
129
|
+
if (amountMsat < deps.minSendableMsat || amountMsat > deps.maxSendableMsat)
|
|
130
|
+
return { reason: 'Amount out of range.' };
|
|
131
|
+
const net = deps.netAfterMintFee(amountMsat);
|
|
132
|
+
if (net < deps.minMintMsat)
|
|
133
|
+
return { reason: 'Amount too small to mint a note.' };
|
|
134
|
+
let zapRequest = null;
|
|
135
|
+
if (nostrParam !== null && nostrParam !== '') {
|
|
136
|
+
const problem = validateZapRequest(nostrParam);
|
|
137
|
+
if (problem)
|
|
138
|
+
return { reason: problem };
|
|
139
|
+
const parsed = JSON.parse(nostrParam);
|
|
140
|
+
if (parsed.kind !== ZAP_REQUEST_KIND)
|
|
141
|
+
return { reason: 'Zap request is not kind 9734.' };
|
|
142
|
+
const p = tagValues(parsed, 'p');
|
|
143
|
+
if (p.length !== 1 || p[0] !== recipient)
|
|
144
|
+
return { reason: `Zap request is not for ${lowered}@${host}.` };
|
|
145
|
+
const amountTag = tagValues(parsed, 'amount')[0];
|
|
146
|
+
if (amountTag !== undefined && Number(amountTag) !== amountMsat) {
|
|
147
|
+
return { reason: 'Zap request amount does not match.' };
|
|
148
|
+
}
|
|
149
|
+
zapRequest = nostrParam;
|
|
150
|
+
}
|
|
151
|
+
// A throwaway preimage: the payer will learn it, and it must be worth
|
|
152
|
+
// nothing to them. The note's secret is minted at settlement.
|
|
153
|
+
const preimage = bytesToHex(randomBytes(32));
|
|
154
|
+
const paymentHash = hashK1(preimage);
|
|
155
|
+
let pr;
|
|
156
|
+
try {
|
|
157
|
+
pr = (await backend.createInvoice({
|
|
158
|
+
amountMsat,
|
|
159
|
+
preimageHex: preimage,
|
|
160
|
+
memo: `Zap ${lowered}@${host}`,
|
|
161
|
+
descriptionForHash: zapRequest ?? metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords)
|
|
162
|
+
})).pr;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
log(`zap: create invoice failed: ${err.message}`);
|
|
166
|
+
return { reason: 'Temporarily unable to issue an invoice.' };
|
|
167
|
+
}
|
|
168
|
+
const decoded = tryDecodeBolt11(pr);
|
|
169
|
+
if (!decoded || decoded.paymentHashHex !== paymentHash || decoded.amountMsats !== BigInt(amountMsat)) {
|
|
170
|
+
log('zap: funding source returned an invoice that does not match the requested preimage/amount');
|
|
171
|
+
return { reason: 'Temporarily unable to issue an invoice.' };
|
|
172
|
+
}
|
|
173
|
+
store.recordZapInvoice({ paymentHash, name: lowered, recipient, pr, grossMsat: amountMsat, netMsat: net, zapRequest });
|
|
174
|
+
return { pr, ...(deps.verify ? { verify: `${origin}/verify/${paymentHash}` } : {}) };
|
|
175
|
+
};
|
|
176
|
+
// The note as the recipient's wallet will paste it, wrapped to them.
|
|
177
|
+
const buildWrap = (row, k1) => {
|
|
178
|
+
const tags = [
|
|
179
|
+
['p', row.recipient],
|
|
180
|
+
['amount', String(row.netMsat)],
|
|
181
|
+
['u', `${host}/w`]
|
|
182
|
+
];
|
|
183
|
+
if (row.zapRequest) {
|
|
184
|
+
const zr = JSON.parse(row.zapRequest);
|
|
185
|
+
// Who zapped, and what they zapped, so a receiver can say so.
|
|
186
|
+
tags.push(['P', zr.pubkey]);
|
|
187
|
+
for (const e of tagValues(zr, 'e'))
|
|
188
|
+
tags.push(['e', e]);
|
|
189
|
+
for (const a of tagValues(zr, 'a'))
|
|
190
|
+
tags.push(['a', a]);
|
|
191
|
+
// The zap request itself, the same content the kind 9735 receipt
|
|
192
|
+
// carries, so a wallet can show who zapped and what they wrote
|
|
193
|
+
// without going to a relay for it. Readers take tags by name, so an
|
|
194
|
+
// older one simply does not see this.
|
|
195
|
+
tags.push(['description', row.zapRequest]);
|
|
196
|
+
}
|
|
197
|
+
const rumor = {
|
|
198
|
+
kind: NOTE_KIND,
|
|
199
|
+
pubkey,
|
|
200
|
+
created_at: Math.floor(now() / 1000),
|
|
201
|
+
tags,
|
|
202
|
+
content: `${origin}/w?k1=${k1}&amount=${row.netMsat}`
|
|
203
|
+
};
|
|
204
|
+
return wrapEvent(rumor, secret, row.recipient);
|
|
205
|
+
};
|
|
206
|
+
// NIP-57 receipt, without the preimage tag: it is optional there, and
|
|
207
|
+
// here it would only invite someone to mistake it for the note.
|
|
208
|
+
const buildReceipt = (row) => {
|
|
209
|
+
if (!row.zapRequest)
|
|
210
|
+
return null;
|
|
211
|
+
const unsigned = makeZapReceipt({ zapRequest: row.zapRequest, bolt11: row.pr, paidAt: new Date(now()) });
|
|
212
|
+
return finalizeEvent(unsigned, secret);
|
|
213
|
+
};
|
|
214
|
+
const settle = async () => {
|
|
215
|
+
let minted = 0;
|
|
216
|
+
for (const row of store.unsettledZapInvoices()) {
|
|
217
|
+
let paid;
|
|
218
|
+
try {
|
|
219
|
+
paid = await backend.isInvoiceSettled(row.paymentHash);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log(`zap: settlement check failed for ${row.paymentHash.slice(0, 8)}: ${err.message}`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!paid)
|
|
226
|
+
continue;
|
|
227
|
+
const k1 = bytesToHex(randomBytes(32));
|
|
228
|
+
const noteId = hashK1(k1);
|
|
229
|
+
const wrap = buildWrap(row, k1);
|
|
230
|
+
const receipt = buildReceipt(row);
|
|
231
|
+
if (store.settleZapInvoice(row.paymentHash, noteId, JSON.stringify(wrap), receipt ? JSON.stringify(receipt) : null)) {
|
|
232
|
+
minted += 1;
|
|
233
|
+
log(`zap: ${row.name} received ${row.netMsat} msat; note ${noteId.slice(0, 8)} minted, wrap parked`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return minted;
|
|
237
|
+
};
|
|
238
|
+
const publish = async () => {
|
|
239
|
+
let published = 0;
|
|
240
|
+
for (const row of store.unpublishedZaps()) {
|
|
241
|
+
if (!row.wrapJson) {
|
|
242
|
+
store.markZapPublished(row.paymentHash);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const wrap = JSON.parse(row.wrapJson);
|
|
246
|
+
const lookOn = [...new Set([...config.relays, ...INDEXER_RELAYS])];
|
|
247
|
+
const inbox = await inboxRelays(transport, row.recipient, lookOn);
|
|
248
|
+
const wrapRelays = [...new Set([...inbox, ...config.relays])];
|
|
249
|
+
const result = await transport.publish(wrapRelays, wrap);
|
|
250
|
+
if (!result.ok.length) {
|
|
251
|
+
log(`zap: wrap for ${row.name} reached no relay (${result.failed.join(', ')}); will retry`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const missedInbox = result.failed.filter(r => inbox.includes(r));
|
|
255
|
+
if (missedInbox.length) {
|
|
256
|
+
const age = now() - (row.settledAt ?? now());
|
|
257
|
+
if (age < INBOX_RETRY_MS) {
|
|
258
|
+
log(`zap: wrap for ${row.name} missed inbox relay(s) ${missedInbox.join(', ')}; parked for another try`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
log(`zap: wrap for ${row.name} never reached ${missedInbox.join(', ')} in ${Math.round(age / 60_000)} min; left on ${result.ok.join(', ')}`);
|
|
262
|
+
}
|
|
263
|
+
if (!inbox.length)
|
|
264
|
+
log(`zap: ${row.name} publishes no inbox list; wrap left on the mint's relays`);
|
|
265
|
+
if (row.receiptJson) {
|
|
266
|
+
const receipt = JSON.parse(row.receiptJson);
|
|
267
|
+
// NIP-57 puts every relay in ONE tag: ["relays", url, url, ...].
|
|
268
|
+
const zr = JSON.parse(row.zapRequest ?? '{"tags":[]}');
|
|
269
|
+
const fromRequest = zr.tags.find(t => t[0] === 'relays')?.slice(1) ?? [];
|
|
270
|
+
const receiptRelays = [...new Set([...fromRequest, ...config.relays])].filter(r => /^wss?:\/\//.test(r));
|
|
271
|
+
const sent = await transport.publish(receiptRelays, receipt);
|
|
272
|
+
if (!sent.ok.length)
|
|
273
|
+
log(`zap: receipt for ${row.name} reached no relay; the note still went out`);
|
|
274
|
+
}
|
|
275
|
+
store.markZapPublished(row.paymentHash);
|
|
276
|
+
published += 1;
|
|
277
|
+
}
|
|
278
|
+
return published;
|
|
279
|
+
};
|
|
280
|
+
const sweep = (nowMs = now()) => {
|
|
281
|
+
const stale = [];
|
|
282
|
+
for (const row of store.unsettledZapInvoices()) {
|
|
283
|
+
const decoded = tryDecodeBolt11(row.pr);
|
|
284
|
+
if (!decoded)
|
|
285
|
+
continue;
|
|
286
|
+
if (nowMs > (decoded.timestamp + decoded.expirySeconds) * 1000 + 3_600_000)
|
|
287
|
+
stale.push(row.paymentHash);
|
|
288
|
+
}
|
|
289
|
+
for (const hash of stale)
|
|
290
|
+
store.deleteUnsettledZapInvoice(hash);
|
|
291
|
+
return stale.length;
|
|
292
|
+
};
|
|
293
|
+
return { pubkey, isZapName, payRequest, callback, settle, publish, sweep };
|
|
294
|
+
};
|
package/llms.txt
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# moneyer
|
|
2
2
|
|
|
3
3
|
LNURLcash (LUD-25) mint for Node 24+. Strikes Lightning bearer notes:
|
|
4
|
-
paying an invoice it issues mints a note whose spend secret
|
|
5
|
-
invoice's payment preimage
|
|
4
|
+
paying an invoice it issues mints a note whose spend secret is the
|
|
5
|
+
invoice's payment preimage, or a secret the buyer named with `h` on the
|
|
6
|
+
pay callback. ESM-only. MIT.
|
|
6
7
|
|
|
7
8
|
Install: not yet on npm - build from source (see README)
|
|
8
9
|
Run: node dist/cli.js --dev (fake funding source, prints a funded note)
|
|
@@ -16,17 +17,29 @@ Client library to build against a mint: lnurlcash-kit
|
|
|
16
17
|
|
|
17
18
|
1. A mint invoice's preimage is the note secret - the funding source must
|
|
18
19
|
accept a caller-supplied preimage (cln and lnd do; phoenixd and NIP-47
|
|
19
|
-
make_invoice do not, and cannot back a mint).
|
|
20
|
+
make_invoice do not, and cannot back a mint). UNLESS the payer sent
|
|
21
|
+
`h` on the pay callback: then the note is credited at `h`, the payer's
|
|
22
|
+
own secret, and the preimage is only a payment proof. Prefer that: a
|
|
23
|
+
preimage is known to the funding source, to every node on the route,
|
|
24
|
+
and to anyone who polls verify with the payment hash in the invoice.
|
|
20
25
|
2. Mutations are atomic: all named k1 burn and all outputs mint, or
|
|
21
26
|
nothing happens. Duplicate k1 in one request is refused.
|
|
22
27
|
3. h/h2 are WALLET-supplied hashes; the mint never generates a
|
|
23
|
-
replacement secret, and refuses output ids that already exist as
|
|
24
|
-
|
|
28
|
+
replacement secret, and refuses output ids that already exist as
|
|
29
|
+
notes, as mint-invoice payment hashes, or as a note another payer has
|
|
30
|
+
already bought with `h`. A malformed `h` on the pay callback is
|
|
31
|
+
refused BEFORE an invoice is issued; a colliding one gets the same
|
|
32
|
+
oracle-free refusal a colliding mutation output gets. The reply
|
|
33
|
+
carries mintToHash: true when the invoice was bound, and the
|
|
34
|
+
payRequest and mint address advertise mintToHash: true.
|
|
25
35
|
4. Melt OK means IN FLIGHT. The note burns only on confirmed payment,
|
|
26
36
|
restores only on confirmed non-payment, and stays pending otherwise.
|
|
27
37
|
A clean failure REPORT is not confirmation (hodl invoices).
|
|
28
|
-
5. A melt invoice must equal the note total exactly,
|
|
29
|
-
|
|
38
|
+
5. A melt invoice must equal the note total exactly, or state no amount
|
|
39
|
+
at all, in which case the mint sends the whole-sat floor of the note's
|
|
40
|
+
value and refuses with 'insufficient value' when that is zero. It must
|
|
41
|
+
not be one this mint issued, and must not repeat an earlier melt's
|
|
42
|
+
payment hash.
|
|
30
43
|
On a SHARED funding source the node's own payment history is the only
|
|
31
44
|
dedupe that spans every consumer: pre-check it before reserving the
|
|
32
45
|
note, and treat the node's "payment already exists" refusal as a
|
|
@@ -47,6 +60,10 @@ Client library to build against a mint: lnurlcash-kit
|
|
|
47
60
|
## Library API
|
|
48
61
|
|
|
49
62
|
createMoneyer(config, deps?) -> {url, port, store, backend, signer, reconcile, close}
|
|
63
|
+
verifyAnnouncement(content, mintPubkey) -> {valid, document}
|
|
64
|
+
kind 30078 / d=lnurlcash-mint: a mint announcing itself, content = its
|
|
65
|
+
discovery document + sig by the note signing key. MONEYER_ANNOUNCE=true,
|
|
66
|
+
off by default.
|
|
50
67
|
configFromEnv(env?) -> MoneyerConfig MONEYER_* environment variables
|
|
51
68
|
createFakeBackend() -> LightningBackend with .control test hooks
|
|
52
69
|
createClnBackend({url, rune}) / createLndBackend({url, macaroon})
|
|
@@ -58,7 +75,7 @@ NoteStore - SQLite store; notes by sha256(k1), never secrets
|
|
|
58
75
|
|
|
59
76
|
/.well-known/lnurlp/{user} LUD-16 payRequest (mints)
|
|
60
77
|
/.well-known/lnurlw/{user} LUD-25 mint address (experimental)
|
|
61
|
-
/p/cb LUD-06 pay callback
|
|
78
|
+
/p/cb LUD-06 pay callback (optional h names the note)
|
|
62
79
|
/verify/{payment_hash} LUD-21 verify (mint invoices AND melt payments)
|
|
63
80
|
/w LUD-03 informational GET
|
|
64
81
|
/w/cb melt / rotate / split / merge
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"@noble/hashes": "^2.3.0",
|
|
58
58
|
"@scure/base": "^1.2.4",
|
|
59
59
|
"farrier-kit": "^1.1.3",
|
|
60
|
-
"lnurlcash-kit": "^0.
|
|
60
|
+
"lnurlcash-kit": "^0.2.0",
|
|
61
|
+
"nostr-tools": "2.24.1"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@fontsource/cinzel": "^5.3.0",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"@types/node": "^24.0.0",
|
|
67
68
|
"animejs": "^4.0.0",
|
|
68
69
|
"happy-dom": "^20.0.0",
|
|
69
|
-
"lnurlcash-conformance": "^0.1
|
|
70
|
+
"lnurlcash-conformance": "^0.2.1",
|
|
70
71
|
"playwright": "^1.62.1",
|
|
71
72
|
"typescript": "^5.7.0",
|
|
72
73
|
"uqr": "^0.1.2",
|