@forgesworn/moneyer 0.18.1 → 0.19.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 +18 -0
- package/README.md +5 -2
- package/dist/backends/fake.js +4 -2
- package/dist/backends/lnd.js +4 -1
- package/dist/server.js +13 -4
- package/dist/store.d.ts +2 -0
- package/dist/store.js +16 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0 - 2026-10-07
|
|
4
|
+
|
|
5
|
+
- **A failed melt's invoice can be tried again.** Once the funding source
|
|
6
|
+
confirms a melt never paid, the note is restored as before, and now the
|
|
7
|
+
invoice is free again too: the payee is still waiting on it, so a wallet
|
|
8
|
+
can melt into it a second time, from the same note or a different one,
|
|
9
|
+
instead of asking for a fresh invoice. lnd sends a hash whose payment
|
|
10
|
+
failed again; a backend that refuses it restores the note, as for any
|
|
11
|
+
payment the node already holds. An invoice that paid, or whose melt is
|
|
12
|
+
still unresolved, is refused as before. The retry reuses the melt's row, so
|
|
13
|
+
`moneyer admin melts` and the restored count in the lifetime totals show
|
|
14
|
+
the latest attempt only.
|
|
15
|
+
- lnd's "invoice is already paid" refusal, sent when a shared node paid the
|
|
16
|
+
hash between the mint's pre-check and its send, is now read as a payment
|
|
17
|
+
the node already holds, like its other two refusals. It used to fall
|
|
18
|
+
through as an ambiguous failure, which the payment tracker would then
|
|
19
|
+
have confirmed as paid against somebody else's payment, burning the note.
|
|
20
|
+
|
|
3
21
|
## 0.18.1 - 2026-10-07
|
|
4
22
|
|
|
5
23
|
- **Bound mint receipts verify in current wallets again.** Since
|
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@ lesson, that behaviour is kept deliberately and tested.
|
|
|
43
43
|
anything else is answered `OK` and finishes in the background. Burn only
|
|
44
44
|
on confirmed payment, restore only on confirmed non-payment, and park
|
|
45
45
|
everything else as pending for reconciliation - which also runs at
|
|
46
|
-
startup, so a crash mid-melt never guesses.
|
|
46
|
+
startup, so a crash mid-melt never guesses. An invoice whose melt was
|
|
47
|
+
restored may be melted into again, from that note or another; one that
|
|
48
|
+
paid or is still unresolved is refused.
|
|
47
49
|
|
|
48
50
|
## Run
|
|
49
51
|
|
|
@@ -471,7 +473,8 @@ at `h`" alone would let anyone holding a burned `k1` and any outstanding
|
|
|
471
473
|
note id draw a success out of the mint.
|
|
472
474
|
|
|
473
475
|
The melt path is untouched: melts are deduplicated by payment hash, which
|
|
474
|
-
is a different question with a different answer
|
|
476
|
+
is a different question with a different answer (a restored melt's
|
|
477
|
+
invoice excepted - see the melt discipline above).
|
|
475
478
|
|
|
476
479
|
This is moneyer's own behaviour. The LUD-25 draft says nothing about
|
|
477
480
|
retries yet; the suggested wording is on lnurl/luds#301 as a SHOULD.
|
package/dist/backends/fake.js
CHANGED
|
@@ -65,8 +65,10 @@ export const createFakeBackend = (options = {}) => {
|
|
|
65
65
|
if (!paymentHashHex)
|
|
66
66
|
throw new PaymentFailedError('That is not a decodable invoice.');
|
|
67
67
|
// Real nodes dedupe sends by payment hash: a hash this node already
|
|
68
|
-
//
|
|
69
|
-
|
|
68
|
+
// paid, or is still paying, is refused exactly as lnd and cln refuse
|
|
69
|
+
// it. One whose every attempt failed may be sent again, as on both.
|
|
70
|
+
const earlier = payments.get(paymentHashHex);
|
|
71
|
+
if (earlier && earlier.status !== 'failed') {
|
|
70
72
|
throw new PaymentAlreadyKnownError('this node already has a payment for that hash');
|
|
71
73
|
}
|
|
72
74
|
const preimageHex = knownPreimages.get(paymentHashHex) ?? null;
|
package/dist/backends/lnd.js
CHANGED
|
@@ -122,7 +122,10 @@ export const createLndBackend = (config) => {
|
|
|
122
122
|
// lnd refuses a send whose payment hash it already holds - on a
|
|
123
123
|
// shared node that is somebody else's payment, and nothing went
|
|
124
124
|
// out for THIS call. Distinct from ambiguity: safely restorable.
|
|
125
|
-
|
|
125
|
+
// Its three refusals (lnd 0.20, payments/db/errors.go): one being
|
|
126
|
+
// created, one in flight, one that succeeded. A hash whose payment
|
|
127
|
+
// failed is not refused - lnd sends it again.
|
|
128
|
+
if (error && /already exists|AlreadyExists|payment is in transition|invoice is already paid/i.test(error)) {
|
|
126
129
|
throw new PaymentAlreadyKnownError('lnd already has a payment for this hash');
|
|
127
130
|
}
|
|
128
131
|
// The stream ended without a terminal status - genuinely ambiguous.
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
|
3
3
|
import { finalizeEvent } from 'nostr-tools/pure';
|
|
4
4
|
import { applyMintFee, encodeCs1WithAmount, grossUpForMintFee, hashK1 } from '@lnurlcash/kit';
|
|
5
5
|
import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
|
|
6
|
-
import { NotePendingError, NoteStore, NoteUnavailableError, OutputCollisionError, swapFingerprint } from "./store.js";
|
|
6
|
+
import { InvoiceUsedError, NotePendingError, NoteStore, NoteUnavailableError, OutputCollisionError, swapFingerprint } from "./store.js";
|
|
7
7
|
import { createNoteSigner } from "./signing.js";
|
|
8
8
|
import { bearerNoteId, decodeNote, decodeSpend, encodeCp1, spendDomainOf, verifySpend } from "./spend.js";
|
|
9
9
|
import { createKernelVerifier } from "./kernel.js";
|
|
@@ -1137,8 +1137,14 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1137
1137
|
}
|
|
1138
1138
|
// The funding source dedupes payments by hash, so a second melt
|
|
1139
1139
|
// into the same invoice would be "confirmed" against the first
|
|
1140
|
-
// payment and burn its note without moving funds.
|
|
1141
|
-
|
|
1140
|
+
// payment and burn its note without moving funds. A melt that was
|
|
1141
|
+
// restored is the exception: the funding source confirmed it never
|
|
1142
|
+
// paid, lnd sends a hash whose payment failed again (it refuses
|
|
1143
|
+
// only one being created, in flight or paid), and the payee is
|
|
1144
|
+
// still waiting on that invoice. A backend that refuses anyway
|
|
1145
|
+
// throws PaymentAlreadyKnownError and the note restores.
|
|
1146
|
+
const earlier = store.meltByHash(paymentHash);
|
|
1147
|
+
if (earlier && earlier.outcome !== 'restored') {
|
|
1142
1148
|
return fail('Invoice already used by an earlier melt - use a fresh one.');
|
|
1143
1149
|
}
|
|
1144
1150
|
// A shared funding source is a supported deployment (DEPLOY.md),
|
|
@@ -1167,7 +1173,10 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
1167
1173
|
return fail('pending');
|
|
1168
1174
|
if (err instanceof NoteUnavailableError)
|
|
1169
1175
|
return fail(INVALID_K1);
|
|
1170
|
-
|
|
1176
|
+
if (err instanceof InvoiceUsedError)
|
|
1177
|
+
return fail('Invoice already used by an earlier melt - use a fresh one.');
|
|
1178
|
+
log(`melt reservation failed: ${err.message}`);
|
|
1179
|
+
return fail('Temporarily unable to melt - try again shortly.');
|
|
1171
1180
|
}
|
|
1172
1181
|
inFlight.add(paymentHash);
|
|
1173
1182
|
const melt = runMelt({
|
package/dist/store.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export declare class NoteUnavailableError extends Error {
|
|
|
82
82
|
}
|
|
83
83
|
export declare class OutputCollisionError extends Error {
|
|
84
84
|
}
|
|
85
|
+
export declare class InvoiceUsedError extends Error {
|
|
86
|
+
}
|
|
85
87
|
export declare class NoteStore {
|
|
86
88
|
private db;
|
|
87
89
|
readonly readOnly: boolean;
|
package/dist/store.js
CHANGED
|
@@ -30,6 +30,10 @@ export class NoteUnavailableError extends Error {
|
|
|
30
30
|
// can learn that id's preimage take the output.
|
|
31
31
|
export class OutputCollisionError extends Error {
|
|
32
32
|
}
|
|
33
|
+
// An earlier melt into this invoice paid it, or has not yet resolved. Only
|
|
34
|
+
// one the funding source confirmed never paid may be melted into again.
|
|
35
|
+
export class InvoiceUsedError extends Error {
|
|
36
|
+
}
|
|
33
37
|
export class NoteStore {
|
|
34
38
|
db;
|
|
35
39
|
readOnly;
|
|
@@ -329,14 +333,22 @@ export class NoteStore {
|
|
|
329
333
|
return parsed.map(([id, amountMsat]) => ({ id, amountMsat }));
|
|
330
334
|
}
|
|
331
335
|
// Reserves a note for a melt and records the melt, atomically. The melts
|
|
332
|
-
// row is keyed by the invoice's payment hash
|
|
333
|
-
//
|
|
336
|
+
// row is keyed by the invoice's payment hash. An earlier melt into the
|
|
337
|
+
// same invoice may be retried only once it was restored - the funding
|
|
338
|
+
// source confirmed nothing went out - and the retry takes over its row.
|
|
339
|
+
// One that paid, or is still unresolved, refuses the reservation.
|
|
334
340
|
markPending(noteId, paymentHash, pr, amountMsat) {
|
|
335
341
|
this.tx(() => {
|
|
336
342
|
this.assertOutstanding(noteId);
|
|
337
|
-
this.db
|
|
338
|
-
.prepare(
|
|
343
|
+
const written = this.db
|
|
344
|
+
.prepare(`INSERT INTO melts (payment_hash, note_id, pr, amount_msat, outcome, created_at) VALUES (?, ?, ?, ?, NULL, ?)
|
|
345
|
+
ON CONFLICT (payment_hash) DO UPDATE SET
|
|
346
|
+
note_id = excluded.note_id, pr = excluded.pr, amount_msat = excluded.amount_msat,
|
|
347
|
+
outcome = NULL, created_at = excluded.created_at, resolved_at = NULL
|
|
348
|
+
WHERE melts.outcome = 'restored'`)
|
|
339
349
|
.run(paymentHash, noteId, pr, amountMsat, Date.now());
|
|
350
|
+
if (written.changes !== 1)
|
|
351
|
+
throw new InvoiceUsedError('an earlier melt already used this invoice');
|
|
340
352
|
this.setNoteState(noteId, 'pending');
|
|
341
353
|
});
|
|
342
354
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|