@forgesworn/moneyer 0.17.0 → 0.17.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 +15 -0
- package/README.md +4 -3
- package/THREAT-MODEL.md +3 -3
- package/dist/admin.js +4 -2
- package/dist/backends/lnd.js +8 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +43 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.1 - 2026-10-06
|
|
4
|
+
|
|
5
|
+
- **The expiry sweep no longer deletes paid mint invoices.** A mint invoice
|
|
6
|
+
was recorded as paid only when a wallet came back to `/verify` or to
|
|
7
|
+
claim its note, and the sweep deleted every unrecorded one an hour past
|
|
8
|
+
its expiry. A payer whose wallet never came back lost the payment: the
|
|
9
|
+
funding source kept the money and the note's id went with the row. The
|
|
10
|
+
sweep, and `moneyer admin sweep`, now ask the funding source first. Paid,
|
|
11
|
+
the invoice is settled and its note minted; unpaid, the row goes; no
|
|
12
|
+
answer, the row stays.
|
|
13
|
+
- **lnd: a refused invoice lookup is no longer read as unpaid.** Only a 404
|
|
14
|
+
(an invoice lnd does not hold) now means unpaid; any other non-2xx throws.
|
|
15
|
+
A transient lnd error on `/verify` now returns an error instead of "not
|
|
16
|
+
paid yet".
|
|
17
|
+
|
|
3
18
|
## 0.17.0 - 2026-09-26
|
|
4
19
|
|
|
5
20
|
LUD-25's unified taproot model (lnurl/luds 6e865b1). Every note is now a
|
package/README.md
CHANGED
|
@@ -703,7 +703,7 @@ path should not silently create an empty database to answer from.
|
|
|
703
703
|
| `note <id\|k1>` | one note; 64 hex that names no note is hashed and looked up as the secret |
|
|
704
704
|
| `melts [--pending]` | list melts |
|
|
705
705
|
| `reconcile` | one pending-melt reconcile pass, printing what changed |
|
|
706
|
-
| `sweep` | delete mint invoices
|
|
706
|
+
| `sweep` | delete expired mint invoices the funding source confirms unpaid, and mint the notes of any it finds paid |
|
|
707
707
|
| `snapshot <path>` | a consistent copy of the database, taken live, refusing to overwrite |
|
|
708
708
|
| `names list\|add <name> <npub>\|rm <name>` | the lightning addresses this mint pays out as notes |
|
|
709
709
|
| `keys rotate` | generate a signing key and print the two environment lines; writes nothing |
|
|
@@ -735,8 +735,9 @@ is not.
|
|
|
735
735
|
Whether that is a channel imbalance or something worse, it is the one
|
|
736
736
|
number a custodial mint must never be relaxed about.
|
|
737
737
|
- **unsettled invoices growing.** Invoices are issued and never paid all
|
|
738
|
-
day, and the sweep clears the expired ones
|
|
739
|
-
a sweep means invoices are being
|
|
738
|
+
day, and the sweep clears the expired ones the funding source confirms
|
|
739
|
+
unpaid. A count that climbs through a sweep means invoices are being
|
|
740
|
+
issued that nobody can pay, or that the funding source is not answering.
|
|
740
741
|
|
|
741
742
|
## Testing
|
|
742
743
|
|
package/THREAT-MODEL.md
CHANGED
|
@@ -147,9 +147,9 @@ is what notecase and lnurl-wallet do.
|
|
|
147
147
|
- No rate limiting is built in; put it at the proxy. This matters most for
|
|
148
148
|
`/p/cb`: each unauthenticated call creates a real invoice at the funding
|
|
149
149
|
source. moneyer sweeps its own unsettled invoices once their bolt11
|
|
150
|
-
expiry has passed
|
|
151
|
-
|
|
152
|
-
what keeps the RPC pile-up and the node database from growing at all.
|
|
150
|
+
expiry has passed and the funding source confirms them unpaid, but the
|
|
151
|
+
node's side of that growth is the operator's to bound (cln's autoclean,
|
|
152
|
+
or an equivalent cron) - and a proxy limit is what keeps the RPC pile-up and the node database from growing at all.
|
|
153
153
|
- A fee-free configuration pays the melt routing-fee floor (0.5% of the
|
|
154
154
|
amount or 5000 msat) out of its own channel balance, and mint-and-melt
|
|
155
155
|
cycling costs a griefer nothing. Set `MONEYER_BASE_FEE_MSAT` to cover the
|
package/dist/admin.js
CHANGED
|
@@ -18,7 +18,7 @@ const COMMANDS = [
|
|
|
18
18
|
['note <id|k1>', 'one note, by id or by the secret itself'],
|
|
19
19
|
['melts [--pending]', 'list melts, newest first'],
|
|
20
20
|
['reconcile', 'resolve melts left in flight, and say what changed'],
|
|
21
|
-
['sweep', 'delete mint invoices
|
|
21
|
+
['sweep', 'delete expired mint invoices the funding source confirms unpaid'],
|
|
22
22
|
['snapshot <path>', 'a consistent copy of the database, taken live'],
|
|
23
23
|
['names list|add <name> <npub>|rm <name>', 'the lightning addresses this mint pays out as notes'],
|
|
24
24
|
['keys rotate', 'generate a signing key and print the two env lines'],
|
|
@@ -271,8 +271,10 @@ export const runAdmin = async (argv, deps = {}) => {
|
|
|
271
271
|
return 0;
|
|
272
272
|
}
|
|
273
273
|
case 'sweep': {
|
|
274
|
-
const swept = sweepExpiredMintInvoices(store, now());
|
|
274
|
+
const { swept, settled } = await sweepExpiredMintInvoices(store, fundingSource(), now());
|
|
275
275
|
out(`swept ${swept} expired mint invoice${swept === 1 ? '' : 's'}`);
|
|
276
|
+
if (settled > 0)
|
|
277
|
+
out(`found ${settled} paid after all - note${settled === 1 ? '' : 's'} minted`);
|
|
276
278
|
return 0;
|
|
277
279
|
}
|
|
278
280
|
case 'snapshot': {
|
package/dist/backends/lnd.js
CHANGED
|
@@ -158,9 +158,16 @@ export const createLndBackend = (config) => {
|
|
|
158
158
|
throw new Error('lnd did not report a payment status.');
|
|
159
159
|
return result;
|
|
160
160
|
},
|
|
161
|
+
// 404 is lnd's answer for an invoice it does not hold, which can never
|
|
162
|
+
// settle. Any other refusal is no answer at all, and reading it as
|
|
163
|
+
// "unpaid" would let the expiry sweep delete a paid invoice's row.
|
|
161
164
|
async isInvoiceSettled(paymentHashHex) {
|
|
162
165
|
const res = await json(`/v1/invoice/${paymentHashHex}`);
|
|
163
|
-
|
|
166
|
+
if (res.status === 404)
|
|
167
|
+
return false;
|
|
168
|
+
if (!res.ok)
|
|
169
|
+
throw new Error(`lnd invoice lookup failed: HTTP ${res.status}`);
|
|
170
|
+
return Boolean(res.json?.settled);
|
|
164
171
|
},
|
|
165
172
|
async invoicePreimage(paymentHashHex) {
|
|
166
173
|
const res = await json(`/v1/invoice/${paymentHashHex}`);
|
package/dist/server.d.ts
CHANGED
|
@@ -33,4 +33,7 @@ export type MoneyerDeps = {
|
|
|
33
33
|
};
|
|
34
34
|
export { describeFee } from './fee-words.ts';
|
|
35
35
|
export declare const createMoneyer: (config: MoneyerConfig, deps?: MoneyerDeps) => Promise<Moneyer>;
|
|
36
|
-
export declare const sweepExpiredMintInvoices: (store: NoteStore, nowMs?: number) =>
|
|
36
|
+
export declare const sweepExpiredMintInvoices: (store: NoteStore, backend: Pick<LightningBackend, "isInvoiceSettled">, nowMs?: number) => Promise<{
|
|
37
|
+
swept: number;
|
|
38
|
+
settled: number;
|
|
39
|
+
}>;
|
package/dist/server.js
CHANGED
|
@@ -1262,9 +1262,12 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1262
1262
|
// otherwise grow by one dead row per /p/cb call forever. The timer is
|
|
1263
1263
|
// unref'd so it never keeps the process alive.
|
|
1264
1264
|
const housekeeping = async () => {
|
|
1265
|
-
const swept = sweepExpiredMintInvoices(store
|
|
1265
|
+
const { swept: mintSwept, settled } = await sweepExpiredMintInvoices(store, backend);
|
|
1266
|
+
const swept = mintSwept + (zap?.sweep() ?? 0);
|
|
1266
1267
|
if (swept > 0)
|
|
1267
1268
|
log(`swept ${swept} expired mint invoice${swept === 1 ? '' : 's'}`);
|
|
1269
|
+
if (settled > 0)
|
|
1270
|
+
log(`found ${settled} expired mint invoice${settled === 1 ? '' : 's'} paid after all - note${settled === 1 ? '' : 's'} minted`);
|
|
1268
1271
|
await reconcilePendingMelts(store, backend, inFlight, log);
|
|
1269
1272
|
reconciledAt = Date.now();
|
|
1270
1273
|
};
|
|
@@ -1337,15 +1340,25 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1337
1340
|
}
|
|
1338
1341
|
};
|
|
1339
1342
|
};
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
//
|
|
1348
|
-
|
|
1343
|
+
// Clears mint invoices this mint has not seen paid once their bolt11 expiry
|
|
1344
|
+
// is comfortably past, returning how many it deleted and how many it found
|
|
1345
|
+
// paid after all.
|
|
1346
|
+
//
|
|
1347
|
+
// "Unsettled" here is only this mint's record. A payment is recorded when a
|
|
1348
|
+
// wallet comes back to /verify or to claim its note, so a payer whose wallet
|
|
1349
|
+
// never comes back - a closed tab, an invoice paid from another app - leaves
|
|
1350
|
+
// a row that reads unsettled while the funding source holds the money.
|
|
1351
|
+
// Deleting that row would strand the payment for good: the note's id lives
|
|
1352
|
+
// only here. So the funding source is asked first. Paid, the invoice is
|
|
1353
|
+
// settled and its note minted, exactly as a /verify would have. Unpaid, the
|
|
1354
|
+
// expiry is the proof it never can be, and the row goes. No answer, no
|
|
1355
|
+
// delete.
|
|
1356
|
+
//
|
|
1357
|
+
// The hour's margin keeps the delete unambiguously behind every
|
|
1358
|
+
// implementation's reading of that expiry, an undecodable invoice is kept,
|
|
1359
|
+
// and the delete is conditional on the row still being unsettled, so a
|
|
1360
|
+
// settle racing the sweep always wins.
|
|
1361
|
+
export const sweepExpiredMintInvoices = async (store, backend, nowMs = Date.now()) => {
|
|
1349
1362
|
const stale = [];
|
|
1350
1363
|
for (const invoice of store.unsettledMintInvoices()) {
|
|
1351
1364
|
const decoded = tryDecodeBolt11(invoice.pr);
|
|
@@ -1355,7 +1368,24 @@ export const sweepExpiredMintInvoices = (store, nowMs = Date.now()) => {
|
|
|
1355
1368
|
if (nowMs > expiresAtMs + 3_600_000)
|
|
1356
1369
|
stale.push(invoice.paymentHash);
|
|
1357
1370
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1371
|
+
let swept = 0;
|
|
1372
|
+
let settled = 0;
|
|
1373
|
+
for (const paymentHash of stale) {
|
|
1374
|
+
let paid;
|
|
1375
|
+
try {
|
|
1376
|
+
paid = await backend.isInvoiceSettled(paymentHash);
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (paid) {
|
|
1382
|
+
store.settleMintInvoice(paymentHash);
|
|
1383
|
+
settled++;
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
store.deleteUnsettledMintInvoice(paymentHash);
|
|
1387
|
+
swept++;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return { swept, settled };
|
|
1361
1391
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.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",
|