@forgesworn/moneyer 0.1.0-next.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 +40 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/SECURITY.md +29 -0
- package/THREAT-MODEL.md +104 -0
- package/dist/backends/cln.d.ts +5 -0
- package/dist/backends/cln.js +130 -0
- package/dist/backends/fake-bolt11.d.ts +6 -0
- package/dist/backends/fake-bolt11.js +60 -0
- package/dist/backends/fake.d.ts +17 -0
- package/dist/backends/fake.js +104 -0
- package/dist/backends/lnd.d.ts +5 -0
- package/dist/backends/lnd.js +191 -0
- package/dist/backends/types.d.ts +38 -0
- package/dist/backends/types.js +25 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +61 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +100 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +23 -0
- package/dist/landing.d.ts +8 -0
- package/dist/landing.js +63 -0
- package/dist/melt.d.ts +17 -0
- package/dist/melt.js +91 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +502 -0
- package/dist/signing.d.ts +6 -0
- package/dist/signing.js +37 -0
- package/dist/store.d.ts +53 -0
- package/dist/store.js +245 -0
- package/dist/web-assets.d.ts +12 -0
- package/dist/web-assets.js +51 -0
- package/llms.txt +64 -0
- package/package.json +76 -0
- package/web/dist/art/plate-l.webp +0 -0
- package/web/dist/art/plate-p.webp +0 -0
- package/web/dist/assets/cinzel-latin-400-normal-C8jUSQqm.woff +0 -0
- package/web/dist/assets/cinzel-latin-400-normal-DnUIPmzd.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-700-normal-C-gK7hA8.woff +0 -0
- package/web/dist/assets/cinzel-latin-700-normal-Dkw14w9r.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-ext-400-normal-DJ0Lq8y-.woff +0 -0
- package/web/dist/assets/cinzel-latin-ext-400-normal-XQK_CSAr.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-ext-700-normal-C24KFjuG.woff2 +0 -0
- package/web/dist/assets/cinzel-latin-ext-700-normal-CORa-yIv.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
- package/web/dist/assets/index-BGYBQuiQ.css +1 -0
- package/web/dist/assets/index-CIuRQd9k.js +130 -0
- package/web/dist/assets/spectral-cyrillic-400-italic-C6o5RXh-.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-400-italic-C95RxGpc.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-400-normal-Bx9Tn3WZ.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-400-normal-X3p2PSzL.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-500-italic-BLpmaxIM.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-500-italic-D14WfPba.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-600-normal-Dl75bDKp.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-600-normal-veT-ELBO.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-italic-C6Gmo4JY.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-italic-CZOVIkJA.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-normal--n_iQ4wr.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-400-normal-B4-YHs9i.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-500-italic-CxkamsWL.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-500-italic-DGZ5KH5h.woff2 +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-600-normal-BVATWin6.woff +0 -0
- package/web/dist/assets/spectral-cyrillic-ext-600-normal-CvRHnPuy.woff2 +0 -0
- package/web/dist/assets/spectral-latin-400-italic-CDO25mTW.woff2 +0 -0
- package/web/dist/assets/spectral-latin-400-italic-Ds-NcNVC.woff +0 -0
- package/web/dist/assets/spectral-latin-400-normal-CTVgUekv.woff2 +0 -0
- package/web/dist/assets/spectral-latin-400-normal-D34IWA1h.woff +0 -0
- package/web/dist/assets/spectral-latin-500-italic-CqeZqSS8.woff2 +0 -0
- package/web/dist/assets/spectral-latin-500-italic-DxwFSvPk.woff +0 -0
- package/web/dist/assets/spectral-latin-600-normal-DE3OxvnQ.woff2 +0 -0
- package/web/dist/assets/spectral-latin-600-normal-DXnQJnYb.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-400-italic-BcKU4A_a.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-400-italic-BgTM1Mst.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-400-normal-BHbqXr-7.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-400-normal-XDLFwhqg.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-500-italic-DMigwuN9.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-500-italic-UnF3HUpx.woff +0 -0
- package/web/dist/assets/spectral-latin-ext-600-normal-DKlDBbgb.woff2 +0 -0
- package/web/dist/assets/spectral-latin-ext-600-normal-DnKmvInL.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-400-italic-D8dhja4j.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-400-italic-DO3bsfcj.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-400-normal-C7CRHYqk.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-400-normal-CWkMWCzb.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-500-italic-e7VYE3f9.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-500-italic-q7ilnnst.woff +0 -0
- package/web/dist/assets/spectral-vietnamese-600-normal-ZNiwCzjU.woff2 +0 -0
- package/web/dist/assets/spectral-vietnamese-600-normal-esmjhrFa.woff +0 -0
- package/web/dist/index.html +19 -0
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type NoteState = 'outstanding' | 'pending' | 'burned';
|
|
2
|
+
export type NoteRow = {
|
|
3
|
+
id: string;
|
|
4
|
+
amountMsat: number;
|
|
5
|
+
state: NoteState;
|
|
6
|
+
};
|
|
7
|
+
export type MintInvoiceRow = {
|
|
8
|
+
paymentHash: string;
|
|
9
|
+
pr: string;
|
|
10
|
+
grossMsat: number;
|
|
11
|
+
netMsat: number;
|
|
12
|
+
settled: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type MeltRow = {
|
|
15
|
+
paymentHash: string;
|
|
16
|
+
noteId: string;
|
|
17
|
+
pr: string;
|
|
18
|
+
amountMsat: number;
|
|
19
|
+
outcome: 'paid' | 'restored' | null;
|
|
20
|
+
};
|
|
21
|
+
export declare class NotePendingError extends Error {
|
|
22
|
+
}
|
|
23
|
+
export declare class NoteUnavailableError extends Error {
|
|
24
|
+
}
|
|
25
|
+
export declare class OutputCollisionError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export declare class NoteStore {
|
|
28
|
+
private db;
|
|
29
|
+
constructor(path: string);
|
|
30
|
+
private tx;
|
|
31
|
+
noteById(id: string): NoteRow | null;
|
|
32
|
+
private insertNote;
|
|
33
|
+
private setNoteState;
|
|
34
|
+
private assertOutputIdFree;
|
|
35
|
+
private assertOutstanding;
|
|
36
|
+
swap(inputIds: string[], outputs: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
amountMsat: number;
|
|
39
|
+
}>): void;
|
|
40
|
+
markPending(noteId: string, paymentHash: string, pr: string, amountMsat: number): void;
|
|
41
|
+
finalizeMelt(paymentHash: string): void;
|
|
42
|
+
restoreMelt(paymentHash: string): void;
|
|
43
|
+
meltByHash(paymentHash: string): MeltRow | null;
|
|
44
|
+
pendingMelts(): MeltRow[];
|
|
45
|
+
recordMintInvoice(paymentHash: string, pr: string, grossMsat: number, netMsat: number): void;
|
|
46
|
+
mintInvoiceByHash(paymentHash: string): MintInvoiceRow | null;
|
|
47
|
+
unsettledMintInvoices(): MintInvoiceRow[];
|
|
48
|
+
deleteUnsettledMintInvoice(paymentHash: string): void;
|
|
49
|
+
settleMintInvoice(paymentHash: string): void;
|
|
50
|
+
creditNote(id: string, amountMsat: number): void;
|
|
51
|
+
outstandingLiabilityMsat(): number;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
// A note named by the request is reserved by an in-flight melt. The wire
|
|
3
|
+
// reply for this is the exact reason string "pending".
|
|
4
|
+
export class NotePendingError extends Error {
|
|
5
|
+
}
|
|
6
|
+
// A note named by the request is burned or was never minted. The callback
|
|
7
|
+
// cannot say which without leaking, so the wire reply is the atomic
|
|
8
|
+
// "Invalid or already spent k1."
|
|
9
|
+
export class NoteUnavailableError extends Error {
|
|
10
|
+
}
|
|
11
|
+
// A requested output id already names a note or a mint invoice. Refused
|
|
12
|
+
// before anything burns: minting "over" an existing id would let whoever
|
|
13
|
+
// can learn that id's preimage take the output.
|
|
14
|
+
export class OutputCollisionError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export class NoteStore {
|
|
17
|
+
db;
|
|
18
|
+
constructor(path) {
|
|
19
|
+
this.db = new DatabaseSync(path);
|
|
20
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
21
|
+
this.db.exec('PRAGMA foreign_keys = ON');
|
|
22
|
+
this.db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
amount_msat INTEGER NOT NULL,
|
|
26
|
+
state TEXT NOT NULL CHECK (state IN ('outstanding','pending','burned')),
|
|
27
|
+
created_at INTEGER NOT NULL,
|
|
28
|
+
updated_at INTEGER NOT NULL
|
|
29
|
+
);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS mint_invoices (
|
|
31
|
+
payment_hash TEXT PRIMARY KEY,
|
|
32
|
+
pr TEXT NOT NULL,
|
|
33
|
+
gross_msat INTEGER NOT NULL,
|
|
34
|
+
net_msat INTEGER NOT NULL,
|
|
35
|
+
settled INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
created_at INTEGER NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS melts (
|
|
39
|
+
payment_hash TEXT PRIMARY KEY,
|
|
40
|
+
note_id TEXT NOT NULL,
|
|
41
|
+
pr TEXT NOT NULL,
|
|
42
|
+
amount_msat INTEGER NOT NULL,
|
|
43
|
+
outcome TEXT CHECK (outcome IN ('paid','restored')),
|
|
44
|
+
created_at INTEGER NOT NULL,
|
|
45
|
+
resolved_at INTEGER
|
|
46
|
+
);
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
tx(fn) {
|
|
50
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
51
|
+
try {
|
|
52
|
+
const result = fn();
|
|
53
|
+
this.db.exec('COMMIT');
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
this.db.exec('ROLLBACK');
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
noteById(id) {
|
|
62
|
+
const row = this.db
|
|
63
|
+
.prepare('SELECT id, amount_msat, state FROM notes WHERE id = ?')
|
|
64
|
+
.get(id);
|
|
65
|
+
return row ? { id: row.id, amountMsat: row.amount_msat, state: row.state } : null;
|
|
66
|
+
}
|
|
67
|
+
insertNote(id, amountMsat) {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
this.db
|
|
70
|
+
.prepare('INSERT INTO notes (id, amount_msat, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
|
|
71
|
+
.run(id, amountMsat, 'outstanding', now, now);
|
|
72
|
+
}
|
|
73
|
+
setNoteState(id, state) {
|
|
74
|
+
this.db.prepare('UPDATE notes SET state = ?, updated_at = ? WHERE id = ?').run(state, Date.now(), id);
|
|
75
|
+
}
|
|
76
|
+
// An output id may not collide with any existing note OR any mint
|
|
77
|
+
// invoice's payment hash, settled or not. The invoice case is the subtle
|
|
78
|
+
// one: /verify hands out a settled mint invoice's preimage, and that
|
|
79
|
+
// preimage is the k1 of whatever note carries the invoice's payment hash
|
|
80
|
+
// as its id - so letting a mutation claim such an id would point a future
|
|
81
|
+
// payer's money at a stranger's note.
|
|
82
|
+
assertOutputIdFree(id) {
|
|
83
|
+
const asNote = this.db.prepare('SELECT 1 FROM notes WHERE id = ?').get(id);
|
|
84
|
+
const asInvoice = this.db.prepare('SELECT 1 FROM mint_invoices WHERE payment_hash = ?').get(id);
|
|
85
|
+
if (asNote || asInvoice)
|
|
86
|
+
throw new OutputCollisionError(`output id ${id} is already in use`);
|
|
87
|
+
}
|
|
88
|
+
assertOutstanding(id) {
|
|
89
|
+
const note = this.noteById(id);
|
|
90
|
+
if (!note)
|
|
91
|
+
throw new NoteUnavailableError(`no note ${id}`);
|
|
92
|
+
if (note.state === 'pending')
|
|
93
|
+
throw new NotePendingError(`note ${id} has a melt in flight`);
|
|
94
|
+
if (note.state !== 'outstanding')
|
|
95
|
+
throw new NoteUnavailableError(`note ${id} is ${note.state}`);
|
|
96
|
+
return note;
|
|
97
|
+
}
|
|
98
|
+
// The atomic mutation behind rotate, split and merge: burn every input,
|
|
99
|
+
// mint every output, or do nothing at all.
|
|
100
|
+
swap(inputIds, outputs) {
|
|
101
|
+
this.tx(() => {
|
|
102
|
+
for (const id of inputIds)
|
|
103
|
+
this.assertOutstanding(id);
|
|
104
|
+
for (const output of outputs)
|
|
105
|
+
this.assertOutputIdFree(output.id);
|
|
106
|
+
for (const id of inputIds)
|
|
107
|
+
this.setNoteState(id, 'burned');
|
|
108
|
+
for (const output of outputs)
|
|
109
|
+
this.insertNote(output.id, output.amountMsat);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Reserves a note for a melt and records the melt, atomically. The melts
|
|
113
|
+
// row is keyed by the invoice's payment hash; a duplicate hash means an
|
|
114
|
+
// earlier melt already used this invoice and the INSERT itself refuses.
|
|
115
|
+
markPending(noteId, paymentHash, pr, amountMsat) {
|
|
116
|
+
this.tx(() => {
|
|
117
|
+
this.assertOutstanding(noteId);
|
|
118
|
+
this.db
|
|
119
|
+
.prepare('INSERT INTO melts (payment_hash, note_id, pr, amount_msat, outcome, created_at) VALUES (?, ?, ?, ?, NULL, ?)')
|
|
120
|
+
.run(paymentHash, noteId, pr, amountMsat, Date.now());
|
|
121
|
+
this.setNoteState(noteId, 'pending');
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
finalizeMelt(paymentHash) {
|
|
125
|
+
this.tx(() => {
|
|
126
|
+
const melt = this.meltByHash(paymentHash);
|
|
127
|
+
if (!melt || melt.outcome !== null)
|
|
128
|
+
return;
|
|
129
|
+
this.db
|
|
130
|
+
.prepare('UPDATE melts SET outcome = ?, resolved_at = ? WHERE payment_hash = ?')
|
|
131
|
+
.run('paid', Date.now(), paymentHash);
|
|
132
|
+
this.setNoteState(melt.noteId, 'burned');
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
restoreMelt(paymentHash) {
|
|
136
|
+
this.tx(() => {
|
|
137
|
+
const melt = this.meltByHash(paymentHash);
|
|
138
|
+
if (!melt || melt.outcome !== null)
|
|
139
|
+
return;
|
|
140
|
+
this.db
|
|
141
|
+
.prepare('UPDATE melts SET outcome = ?, resolved_at = ? WHERE payment_hash = ?')
|
|
142
|
+
.run('restored', Date.now(), paymentHash);
|
|
143
|
+
const note = this.noteById(melt.noteId);
|
|
144
|
+
if (note?.state === 'pending')
|
|
145
|
+
this.setNoteState(melt.noteId, 'outstanding');
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
meltByHash(paymentHash) {
|
|
149
|
+
const row = this.db
|
|
150
|
+
.prepare('SELECT payment_hash, note_id, pr, amount_msat, outcome FROM melts WHERE payment_hash = ?')
|
|
151
|
+
.get(paymentHash);
|
|
152
|
+
if (!row)
|
|
153
|
+
return null;
|
|
154
|
+
return {
|
|
155
|
+
paymentHash: row.payment_hash,
|
|
156
|
+
noteId: row.note_id,
|
|
157
|
+
pr: row.pr,
|
|
158
|
+
amountMsat: row.amount_msat,
|
|
159
|
+
outcome: row.outcome
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
pendingMelts() {
|
|
163
|
+
const rows = this.db
|
|
164
|
+
.prepare('SELECT payment_hash, note_id, pr, amount_msat, outcome FROM melts WHERE outcome IS NULL')
|
|
165
|
+
.all();
|
|
166
|
+
return rows.map(row => ({
|
|
167
|
+
paymentHash: row.payment_hash,
|
|
168
|
+
noteId: row.note_id,
|
|
169
|
+
pr: row.pr,
|
|
170
|
+
amountMsat: row.amount_msat,
|
|
171
|
+
outcome: row.outcome
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
recordMintInvoice(paymentHash, pr, grossMsat, netMsat) {
|
|
175
|
+
this.tx(() => {
|
|
176
|
+
this.assertOutputIdFree(paymentHash);
|
|
177
|
+
this.db
|
|
178
|
+
.prepare('INSERT INTO mint_invoices (payment_hash, pr, gross_msat, net_msat, settled, created_at) VALUES (?, ?, ?, ?, 0, ?)')
|
|
179
|
+
.run(paymentHash, pr, grossMsat, netMsat, Date.now());
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
mintInvoiceByHash(paymentHash) {
|
|
183
|
+
const row = this.db
|
|
184
|
+
.prepare('SELECT payment_hash, pr, gross_msat, net_msat, settled FROM mint_invoices WHERE payment_hash = ?')
|
|
185
|
+
.get(paymentHash);
|
|
186
|
+
if (!row)
|
|
187
|
+
return null;
|
|
188
|
+
return {
|
|
189
|
+
paymentHash: row.payment_hash,
|
|
190
|
+
pr: row.pr,
|
|
191
|
+
grossMsat: row.gross_msat,
|
|
192
|
+
netMsat: row.net_msat,
|
|
193
|
+
settled: row.settled === 1
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Every unsettled mint invoice, for the expiry sweep. An unsettled row
|
|
197
|
+
// past its bolt11 expiry can never settle - the funding source refuses
|
|
198
|
+
// expired invoices - so it is dead weight, and every /p/cb call adds one.
|
|
199
|
+
unsettledMintInvoices() {
|
|
200
|
+
const rows = this.db
|
|
201
|
+
.prepare('SELECT payment_hash, pr, gross_msat, net_msat, settled FROM mint_invoices WHERE settled = 0')
|
|
202
|
+
.all();
|
|
203
|
+
return rows.map(row => ({
|
|
204
|
+
paymentHash: row.payment_hash,
|
|
205
|
+
pr: row.pr,
|
|
206
|
+
grossMsat: row.gross_msat,
|
|
207
|
+
netMsat: row.net_msat,
|
|
208
|
+
settled: false
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
// Conditional on STILL unsettled: a settle landing between the sweep's
|
|
212
|
+
// check and this delete must win, always.
|
|
213
|
+
deleteUnsettledMintInvoice(paymentHash) {
|
|
214
|
+
this.db.prepare('DELETE FROM mint_invoices WHERE payment_hash = ? AND settled = 0').run(paymentHash);
|
|
215
|
+
}
|
|
216
|
+
// Paying a mint invoice is what brings its note into existence. Safe to
|
|
217
|
+
// call twice: the second settle finds the note already minted.
|
|
218
|
+
settleMintInvoice(paymentHash) {
|
|
219
|
+
this.tx(() => {
|
|
220
|
+
const invoice = this.mintInvoiceByHash(paymentHash);
|
|
221
|
+
if (!invoice)
|
|
222
|
+
return;
|
|
223
|
+
this.db.prepare('UPDATE mint_invoices SET settled = 1 WHERE payment_hash = ?').run(paymentHash);
|
|
224
|
+
if (!this.noteById(paymentHash))
|
|
225
|
+
this.insertNote(paymentHash, invoice.netMsat);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Operator/dev funding path: mint a note directly, bypassing Lightning.
|
|
229
|
+
// The fake backend's world only - the CLI never exposes it on a real one.
|
|
230
|
+
creditNote(id, amountMsat) {
|
|
231
|
+
this.tx(() => {
|
|
232
|
+
this.assertOutputIdFree(id);
|
|
233
|
+
this.insertNote(id, amountMsat);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
outstandingLiabilityMsat() {
|
|
237
|
+
const row = this.db
|
|
238
|
+
.prepare("SELECT COALESCE(SUM(amount_msat), 0) AS total FROM notes WHERE state IN ('outstanding','pending')")
|
|
239
|
+
.get();
|
|
240
|
+
return row.total;
|
|
241
|
+
}
|
|
242
|
+
close() {
|
|
243
|
+
this.db.close();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type WebAsset = {
|
|
2
|
+
body: Buffer;
|
|
3
|
+
type: string;
|
|
4
|
+
immutable: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type WebAssets = {
|
|
7
|
+
indexHtml: string;
|
|
8
|
+
get(path: string): WebAsset | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare const CONFIG_TOKEN = "<!--#mint-config-->";
|
|
11
|
+
export declare const defaultWebDist: () => string;
|
|
12
|
+
export declare const loadWebAssets: (dir?: string) => WebAssets | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const CONFIG_TOKEN = '<!--#mint-config-->';
|
|
5
|
+
const TYPES = {
|
|
6
|
+
'.html': 'text/html; charset=utf-8',
|
|
7
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
8
|
+
'.css': 'text/css; charset=utf-8',
|
|
9
|
+
'.svg': 'image/svg+xml',
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.jpg': 'image/jpeg',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.webp': 'image/webp',
|
|
14
|
+
'.ico': 'image/x-icon',
|
|
15
|
+
'.json': 'application/json',
|
|
16
|
+
'.webmanifest': 'application/manifest+json',
|
|
17
|
+
'.woff2': 'font/woff2',
|
|
18
|
+
'.txt': 'text/plain; charset=utf-8'
|
|
19
|
+
};
|
|
20
|
+
export const defaultWebDist = () => fileURLToPath(new URL('../web/dist', import.meta.url));
|
|
21
|
+
export const loadWebAssets = (dir = defaultWebDist()) => {
|
|
22
|
+
const files = new Map();
|
|
23
|
+
let indexHtml = null;
|
|
24
|
+
const walk = (current, prefix) => {
|
|
25
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
26
|
+
const path = `${prefix}/${entry.name}`;
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
walk(join(current, entry.name), path);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!entry.isFile())
|
|
32
|
+
continue;
|
|
33
|
+
const body = readFileSync(join(current, entry.name));
|
|
34
|
+
const type = TYPES[extname(entry.name).toLowerCase()] ?? 'application/octet-stream';
|
|
35
|
+
// vite content-hashes everything under /assets/, so those never change
|
|
36
|
+
// in place; everything else revalidates.
|
|
37
|
+
files.set(path, { body, type, immutable: path.startsWith('/assets/') });
|
|
38
|
+
if (path === '/index.html')
|
|
39
|
+
indexHtml = body.toString('utf8');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
walk(dir, '');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (indexHtml === null)
|
|
49
|
+
return null;
|
|
50
|
+
return { indexHtml, get: path => files.get(path) };
|
|
51
|
+
};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# moneyer
|
|
2
|
+
|
|
3
|
+
LNURLcash (LUD-25) mint for Node 24+. Strikes Lightning bearer notes:
|
|
4
|
+
paying an invoice it issues mints a note whose spend secret IS the
|
|
5
|
+
invoice's payment preimage. ESM-only. MIT.
|
|
6
|
+
|
|
7
|
+
Install: not yet on npm - build from source (see README)
|
|
8
|
+
Run: node dist/cli.js --dev (fake funding source, prints a funded note)
|
|
9
|
+
|
|
10
|
+
Draft spec: https://github.com/lnurl/luds/pull/301
|
|
11
|
+
Conformance: https://github.com/TheCryptoDonkey/lnurlcash-conformance
|
|
12
|
+
Companion wallet: @forgesworn/notecase
|
|
13
|
+
Client library to build against a mint: lnurlcash-kit
|
|
14
|
+
|
|
15
|
+
## Rules this SERVICE follows (and any mint MUST)
|
|
16
|
+
|
|
17
|
+
1. A mint invoice's preimage is the note secret - the funding source must
|
|
18
|
+
accept a caller-supplied preimage (cln and lnd do; phoenixd and NIP-47
|
|
19
|
+
make_invoice do not, and cannot back a mint).
|
|
20
|
+
2. Mutations are atomic: all named k1 burn and all outputs mint, or
|
|
21
|
+
nothing happens. Duplicate k1 in one request is refused.
|
|
22
|
+
3. h/h2 are WALLET-supplied hashes; the mint never generates a
|
|
23
|
+
replacement secret, and refuses output ids that already exist as notes
|
|
24
|
+
or as mint-invoice payment hashes.
|
|
25
|
+
4. Melt OK means IN FLIGHT. The note burns only on confirmed payment,
|
|
26
|
+
restores only on confirmed non-payment, and stays pending otherwise.
|
|
27
|
+
A clean failure REPORT is not confirmation (hodl invoices).
|
|
28
|
+
5. A melt invoice must equal the note total exactly, must not be one this
|
|
29
|
+
mint issued, and must not repeat an earlier melt's payment hash.
|
|
30
|
+
On a SHARED funding source the node's own payment history is the only
|
|
31
|
+
dedupe that spans every consumer: pre-check it before reserving the
|
|
32
|
+
note, and treat the node's "payment already exists" refusal as a
|
|
33
|
+
distinct restorable outcome - never confirm a melt against a payment
|
|
34
|
+
this mint did not initiate.
|
|
35
|
+
6. Fee algebra (LUD-25, when a fee is advertised): base_fee_msat comes
|
|
36
|
+
out of every split's CHANGE, never the requested amount; a merge of n
|
|
37
|
+
notes refunds (n-1) base fees; a change that cannot cover the fee or
|
|
38
|
+
would land at zero refuses with 'insufficient value'. The proportional
|
|
39
|
+
part applies at mint time only.
|
|
40
|
+
7. Reason strings are protocol surface: 'pending',
|
|
41
|
+
'Invalid or already spent k1.', 'Unknown note.', 'Note already spent.',
|
|
42
|
+
'missing h', 'missing h2', 'insufficient value' - wallets classify on
|
|
43
|
+
them.
|
|
44
|
+
8. minSendable must survive the mint's own fee (grossUpForMintFee of the
|
|
45
|
+
dust floor), or the mint advertises an amount it will refuse.
|
|
46
|
+
|
|
47
|
+
## Library API
|
|
48
|
+
|
|
49
|
+
createMoneyer(config, deps?) -> {url, port, store, backend, signer, reconcile, close}
|
|
50
|
+
configFromEnv(env?) -> MoneyerConfig MONEYER_* environment variables
|
|
51
|
+
createFakeBackend() -> LightningBackend with .control test hooks
|
|
52
|
+
createClnBackend({url, rune}) / createLndBackend({url, macaroon})
|
|
53
|
+
createNoteSigner(privHex) -> {pubkey, sign(noteId, amountMsat)}
|
|
54
|
+
fakeBolt11({amountMsat, paymentHashHex, memo?}) -> decodable, unpayable invoice
|
|
55
|
+
NoteStore - SQLite store; notes by sha256(k1), never secrets
|
|
56
|
+
|
|
57
|
+
## Endpoints
|
|
58
|
+
|
|
59
|
+
/.well-known/lnurlp/{user} LUD-16 payRequest (mints)
|
|
60
|
+
/.well-known/lnurlw/{user} LUD-25 mint address (experimental)
|
|
61
|
+
/p/cb LUD-06 pay callback
|
|
62
|
+
/verify/{payment_hash} LUD-21 verify (mint invoices AND melt payments)
|
|
63
|
+
/w LUD-03 informational GET
|
|
64
|
+
/w/cb melt / rotate / split / merge
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgesworn/moneyer",
|
|
3
|
+
"version": "0.1.0-next.0",
|
|
4
|
+
"description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
|
|
5
|
+
"author": "TheCryptoDonkey",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/forgesworn/moneyer.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/forgesworn/moneyer#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/forgesworn/moneyer/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"provenance": true
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"moneyer": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"web/dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"SECURITY.md",
|
|
39
|
+
"THREAT-MODEL.md",
|
|
40
|
+
"llms.txt"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"dev": "node --experimental-strip-types src/cli.ts --dev",
|
|
48
|
+
"conform": "node test/run-grader.mjs",
|
|
49
|
+
"check": "npm run typecheck && npm run typecheck:web && npm test && npm run build && npm run web:build && npm pack --dry-run",
|
|
50
|
+
"web:dev": "vite web",
|
|
51
|
+
"web:build": "vite build web",
|
|
52
|
+
"web:preview": "vite preview web",
|
|
53
|
+
"typecheck:web": "tsc -p web"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@noble/curves": "^2.3.0",
|
|
57
|
+
"@noble/hashes": "^2.3.0",
|
|
58
|
+
"@scure/base": "^1.2.4",
|
|
59
|
+
"farrier-kit": "^1.1.3",
|
|
60
|
+
"lnurlcash-kit": "^0.1.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@fontsource/cinzel": "^5.3.0",
|
|
64
|
+
"@fontsource/ibm-plex-mono": "^5.3.0",
|
|
65
|
+
"@fontsource/spectral": "^5.3.0",
|
|
66
|
+
"@types/node": "^24.0.0",
|
|
67
|
+
"animejs": "^4.0.0",
|
|
68
|
+
"happy-dom": "^20.0.0",
|
|
69
|
+
"lnurlcash-conformance": "^0.1.0",
|
|
70
|
+
"playwright": "^1.62.1",
|
|
71
|
+
"typescript": "^5.7.0",
|
|
72
|
+
"uqr": "^0.1.2",
|
|
73
|
+
"vite": "^7.0.0",
|
|
74
|
+
"vitest": "^3.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|