@forgesworn/moneyer 0.17.1 → 0.18.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 +13 -0
- package/README.md +7 -4
- package/dist/melt.d.ts +9 -1
- package/dist/melt.js +15 -12
- package/dist/server.d.ts +1 -0
- package/dist/server.js +24 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 - 2026-10-07
|
|
4
|
+
|
|
5
|
+
- **A melt that cannot leave is answered with its reason.** The callback
|
|
6
|
+
used to reply `OK` the moment the note was reserved, so a melt with no
|
|
7
|
+
route looked like success until the wallet checked again. It now waits up
|
|
8
|
+
to two seconds for the payment: if the funding source confirms nothing
|
|
9
|
+
went out, the note is restored first and the answer is `ERROR` with the
|
|
10
|
+
reason (for lnd, "Could not find a route to pay this invoice."). A
|
|
11
|
+
payment still in flight, or one that settles, is `OK` as before. The
|
|
12
|
+
hodl-invoice rule stands: a failure report is acted on only once the
|
|
13
|
+
funding source confirms it. The invoice itself is still refused if tried
|
|
14
|
+
again, as it was after a background restore.
|
|
15
|
+
|
|
3
16
|
## 0.17.1 - 2026-10-06
|
|
4
17
|
|
|
5
18
|
- **The expiry sweep no longer deletes paid mint invoices.** A mint invoice
|
package/README.md
CHANGED
|
@@ -37,10 +37,13 @@ lesson, that behaviour is kept deliberately and tested.
|
|
|
37
37
|
from the start.
|
|
38
38
|
- Signs every note it mints with its own mint key (secp256k1, the standard
|
|
39
39
|
`Lightning Signed Message` construction) for LUD-25 offline verification.
|
|
40
|
-
- The melt discipline:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
- The melt discipline: reserve the note, then give the payment up to two
|
|
41
|
+
seconds. A payment the funding source confirms never left (no route, say)
|
|
42
|
+
is answered `ERROR` with its reason and the note is outstanding again;
|
|
43
|
+
anything else is answered `OK` and finishes in the background. Burn only
|
|
44
|
+
on confirmed payment, restore only on confirmed non-payment, and park
|
|
45
|
+
everything else as pending for reconciliation - which also runs at
|
|
46
|
+
startup, so a crash mid-melt never guesses.
|
|
44
47
|
|
|
45
48
|
## Run
|
|
46
49
|
|
package/dist/melt.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export type MeltJob = {
|
|
|
7
7
|
amountMsat: number;
|
|
8
8
|
payAmountMsat?: number;
|
|
9
9
|
};
|
|
10
|
+
export type MeltOutcome = {
|
|
11
|
+
kind: 'paid';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'restored';
|
|
14
|
+
reason: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'unresolved';
|
|
17
|
+
};
|
|
10
18
|
export type MeltDeps = {
|
|
11
19
|
store: NoteStore;
|
|
12
20
|
backend: LightningBackend;
|
|
@@ -14,5 +22,5 @@ export type MeltDeps = {
|
|
|
14
22
|
confirmDelaysMs?: number[];
|
|
15
23
|
log?: (message: string) => void;
|
|
16
24
|
};
|
|
17
|
-
export declare const runMelt: (job: MeltJob, deps: MeltDeps) => Promise<
|
|
25
|
+
export declare const runMelt: (job: MeltJob, deps: MeltDeps) => Promise<MeltOutcome>;
|
|
18
26
|
export declare const reconcilePendingMelts: (store: NoteStore, backend: LightningBackend, inFlight: ReadonlySet<string>, log?: (message: string) => void) => Promise<void>;
|
package/dist/melt.js
CHANGED
|
@@ -2,7 +2,7 @@ import { verifyPreimage } from 'farrier-kit/preimage';
|
|
|
2
2
|
import { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError } from "./backends/types.js";
|
|
3
3
|
const DEFAULT_CONFIRM_DELAYS_MS = [0, 2_000, 4_000, 9_000, 16_000];
|
|
4
4
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
5
|
-
const confirmThenSettle = async (job, deps) => {
|
|
5
|
+
const confirmThenSettle = async (job, deps, reason) => {
|
|
6
6
|
const log = deps.log ?? (() => { });
|
|
7
7
|
for (const delay of deps.confirmDelaysMs ?? DEFAULT_CONFIRM_DELAYS_MS) {
|
|
8
8
|
if (delay > 0)
|
|
@@ -11,12 +11,11 @@ const confirmThenSettle = async (job, deps) => {
|
|
|
11
11
|
const complete = await deps.backend.isPaymentComplete(job.paymentHash);
|
|
12
12
|
if (complete) {
|
|
13
13
|
deps.store.finalizeMelt(job.paymentHash);
|
|
14
|
+
return { kind: 'paid' };
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
return;
|
|
16
|
+
log(`melt ${job.noteId}: confirmed not paid - restoring`);
|
|
17
|
+
deps.store.restoreMelt(job.paymentHash);
|
|
18
|
+
return { kind: 'restored', reason };
|
|
20
19
|
}
|
|
21
20
|
catch (err) {
|
|
22
21
|
if (!(err instanceof PaymentPendingError)) {
|
|
@@ -25,6 +24,7 @@ const confirmThenSettle = async (job, deps) => {
|
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
log(`melt ${job.noteId}: payment status unconfirmable - note left pending for reconciliation`);
|
|
27
|
+
return { kind: 'unresolved' };
|
|
28
28
|
};
|
|
29
29
|
export const runMelt = async (job, deps) => {
|
|
30
30
|
const log = deps.log ?? (() => { });
|
|
@@ -47,23 +47,26 @@ export const runMelt = async (job, deps) => {
|
|
|
47
47
|
// lands between that check and the send.
|
|
48
48
|
log(`melt ${job.noteId}: the funding source already knew this hash - restored (shared-node replay?)`);
|
|
49
49
|
deps.store.restoreMelt(job.paymentHash);
|
|
50
|
-
return;
|
|
50
|
+
return { kind: 'restored', reason: 'Invoice already used by an earlier melt - use a fresh one.' };
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// A clean failure's message is the backend's reason, written for a
|
|
53
|
+
// wallet ("Could not find a route..."); an ambiguous one is transport
|
|
54
|
+
// detail, so a wallet gets a plain sentence instead.
|
|
55
|
+
const clean = err instanceof PaymentFailedError;
|
|
56
|
+
if (!clean) {
|
|
53
57
|
log(`melt ${job.noteId}: payment attempt failed ambiguously (${err.message})`);
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
return;
|
|
59
|
+
return confirmThenSettle(job, deps, clean ? err.message : 'Payment failed.');
|
|
57
60
|
}
|
|
58
61
|
if (outcome.preimageHex !== null && !verifyPreimage(outcome.preimageHex, job.paymentHash)) {
|
|
59
62
|
// The backend claims success with a preimage that does not settle this
|
|
60
63
|
// invoice. That is a statement about the evidence, not the money - fall
|
|
61
64
|
// back to the tracker rather than trusting either way.
|
|
62
65
|
log(`melt ${job.noteId}: backend preimage does not settle this invoice - reconfirming`);
|
|
63
|
-
|
|
64
|
-
return;
|
|
66
|
+
return confirmThenSettle(job, deps, 'Payment failed.');
|
|
65
67
|
}
|
|
66
68
|
deps.store.finalizeMelt(job.paymentHash);
|
|
69
|
+
return { kind: 'paid' };
|
|
67
70
|
};
|
|
68
71
|
// Resolves melts an earlier process left pending - a crash mid-melt, or an
|
|
69
72
|
// outcome that could not be confirmed at the time. Melts whose attempt is
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -45,6 +45,9 @@ const renamedRef = (q, name, legacy) => {
|
|
|
45
45
|
// more than a name and a note, and an unbounded read on a public endpoint
|
|
46
46
|
// is a way to run a mint out of memory.
|
|
47
47
|
const MAX_BODY_BYTES = 8 * 1024;
|
|
48
|
+
// How long a melt callback waits for the payment's outcome before answering
|
|
49
|
+
// OK. Long enough for a no-route failure, short against a wallet's timeout.
|
|
50
|
+
const MELT_ANSWER_WINDOW_MS = 2_000;
|
|
48
51
|
const readBody = async (req, limit = MAX_BODY_BYTES) => {
|
|
49
52
|
const chunks = [];
|
|
50
53
|
let size = 0;
|
|
@@ -1161,17 +1164,34 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1161
1164
|
return fail('Invoice already used by an earlier melt - use a fresh one.');
|
|
1162
1165
|
}
|
|
1163
1166
|
inFlight.add(paymentHash);
|
|
1164
|
-
|
|
1167
|
+
const melt = runMelt({
|
|
1165
1168
|
paymentHash,
|
|
1166
1169
|
noteId: inputIds[0],
|
|
1167
1170
|
pr: pr.trim(),
|
|
1168
1171
|
amountMsat: totalMsat,
|
|
1169
1172
|
...(payAmountMsat !== null ? { payAmountMsat } : {})
|
|
1170
1173
|
}, { store, backend, feeLimitMsat: meltFeeLimitMsat, log, ...(deps.confirmDelaysMs ? { confirmDelaysMs: deps.confirmDelaysMs } : {}) })
|
|
1171
|
-
.catch(err =>
|
|
1174
|
+
.catch((err) => {
|
|
1175
|
+
log(`melt ${inputIds[0]}: ${err.message}`);
|
|
1176
|
+
return { kind: 'unresolved' };
|
|
1177
|
+
})
|
|
1172
1178
|
.finally(() => inFlight.delete(paymentHash));
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1179
|
+
// A payment that cannot leave usually says so within a second (lnd
|
|
1180
|
+
// answers no-route before sending any HTLC), so wait briefly: a
|
|
1181
|
+
// melt the funding source has confirmed never went out is answered
|
|
1182
|
+
// with its reason, and the note is already outstanding again. Any
|
|
1183
|
+
// other outcome, or none yet, is OK as LUD-03 has it: the melt is
|
|
1184
|
+
// requested and the note reserved, never that it settled.
|
|
1185
|
+
let windowTimer;
|
|
1186
|
+
const outcome = await Promise.race([
|
|
1187
|
+
melt,
|
|
1188
|
+
new Promise(resolve => {
|
|
1189
|
+
windowTimer = setTimeout(() => resolve(null), deps.meltAnswerWindowMs ?? MELT_ANSWER_WINDOW_MS);
|
|
1190
|
+
})
|
|
1191
|
+
]);
|
|
1192
|
+
clearTimeout(windowTimer);
|
|
1193
|
+
if (outcome?.kind === 'restored')
|
|
1194
|
+
return fail(outcome.reason);
|
|
1175
1195
|
return send({
|
|
1176
1196
|
status: 'OK',
|
|
1177
1197
|
...(config.verify ? { pr: pr.trim(), verify: `${origin}/verify/${paymentHash}` } : {})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|