@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -0
  4. package/SECURITY.md +29 -0
  5. package/THREAT-MODEL.md +104 -0
  6. package/dist/backends/cln.d.ts +5 -0
  7. package/dist/backends/cln.js +130 -0
  8. package/dist/backends/fake-bolt11.d.ts +6 -0
  9. package/dist/backends/fake-bolt11.js +60 -0
  10. package/dist/backends/fake.d.ts +17 -0
  11. package/dist/backends/fake.js +104 -0
  12. package/dist/backends/lnd.d.ts +5 -0
  13. package/dist/backends/lnd.js +191 -0
  14. package/dist/backends/types.d.ts +38 -0
  15. package/dist/backends/types.js +25 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +61 -0
  18. package/dist/config.d.ts +44 -0
  19. package/dist/config.js +100 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.js +23 -0
  22. package/dist/landing.d.ts +8 -0
  23. package/dist/landing.js +63 -0
  24. package/dist/melt.d.ts +17 -0
  25. package/dist/melt.js +91 -0
  26. package/dist/server.d.ts +24 -0
  27. package/dist/server.js +502 -0
  28. package/dist/signing.d.ts +6 -0
  29. package/dist/signing.js +37 -0
  30. package/dist/store.d.ts +53 -0
  31. package/dist/store.js +245 -0
  32. package/dist/web-assets.d.ts +12 -0
  33. package/dist/web-assets.js +51 -0
  34. package/llms.txt +64 -0
  35. package/package.json +76 -0
  36. package/web/dist/art/plate-l.webp +0 -0
  37. package/web/dist/art/plate-p.webp +0 -0
  38. package/web/dist/assets/cinzel-latin-400-normal-C8jUSQqm.woff +0 -0
  39. package/web/dist/assets/cinzel-latin-400-normal-DnUIPmzd.woff2 +0 -0
  40. package/web/dist/assets/cinzel-latin-700-normal-C-gK7hA8.woff +0 -0
  41. package/web/dist/assets/cinzel-latin-700-normal-Dkw14w9r.woff2 +0 -0
  42. package/web/dist/assets/cinzel-latin-ext-400-normal-DJ0Lq8y-.woff +0 -0
  43. package/web/dist/assets/cinzel-latin-ext-400-normal-XQK_CSAr.woff2 +0 -0
  44. package/web/dist/assets/cinzel-latin-ext-700-normal-C24KFjuG.woff2 +0 -0
  45. package/web/dist/assets/cinzel-latin-ext-700-normal-CORa-yIv.woff +0 -0
  46. package/web/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  47. package/web/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  48. package/web/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  49. package/web/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  50. package/web/dist/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
  51. package/web/dist/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
  52. package/web/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  53. package/web/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  54. package/web/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  55. package/web/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  56. package/web/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
  57. package/web/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
  58. package/web/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  59. package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  60. package/web/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  61. package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  62. package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  63. package/web/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  64. package/web/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  65. package/web/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  66. package/web/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  67. package/web/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  68. package/web/dist/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
  69. package/web/dist/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
  70. package/web/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  71. package/web/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  72. package/web/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  73. package/web/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  74. package/web/dist/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
  75. package/web/dist/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
  76. package/web/dist/assets/index-BGYBQuiQ.css +1 -0
  77. package/web/dist/assets/index-CIuRQd9k.js +130 -0
  78. package/web/dist/assets/spectral-cyrillic-400-italic-C6o5RXh-.woff2 +0 -0
  79. package/web/dist/assets/spectral-cyrillic-400-italic-C95RxGpc.woff +0 -0
  80. package/web/dist/assets/spectral-cyrillic-400-normal-Bx9Tn3WZ.woff2 +0 -0
  81. package/web/dist/assets/spectral-cyrillic-400-normal-X3p2PSzL.woff +0 -0
  82. package/web/dist/assets/spectral-cyrillic-500-italic-BLpmaxIM.woff +0 -0
  83. package/web/dist/assets/spectral-cyrillic-500-italic-D14WfPba.woff2 +0 -0
  84. package/web/dist/assets/spectral-cyrillic-600-normal-Dl75bDKp.woff2 +0 -0
  85. package/web/dist/assets/spectral-cyrillic-600-normal-veT-ELBO.woff +0 -0
  86. package/web/dist/assets/spectral-cyrillic-ext-400-italic-C6Gmo4JY.woff +0 -0
  87. package/web/dist/assets/spectral-cyrillic-ext-400-italic-CZOVIkJA.woff2 +0 -0
  88. package/web/dist/assets/spectral-cyrillic-ext-400-normal--n_iQ4wr.woff +0 -0
  89. package/web/dist/assets/spectral-cyrillic-ext-400-normal-B4-YHs9i.woff2 +0 -0
  90. package/web/dist/assets/spectral-cyrillic-ext-500-italic-CxkamsWL.woff +0 -0
  91. package/web/dist/assets/spectral-cyrillic-ext-500-italic-DGZ5KH5h.woff2 +0 -0
  92. package/web/dist/assets/spectral-cyrillic-ext-600-normal-BVATWin6.woff +0 -0
  93. package/web/dist/assets/spectral-cyrillic-ext-600-normal-CvRHnPuy.woff2 +0 -0
  94. package/web/dist/assets/spectral-latin-400-italic-CDO25mTW.woff2 +0 -0
  95. package/web/dist/assets/spectral-latin-400-italic-Ds-NcNVC.woff +0 -0
  96. package/web/dist/assets/spectral-latin-400-normal-CTVgUekv.woff2 +0 -0
  97. package/web/dist/assets/spectral-latin-400-normal-D34IWA1h.woff +0 -0
  98. package/web/dist/assets/spectral-latin-500-italic-CqeZqSS8.woff2 +0 -0
  99. package/web/dist/assets/spectral-latin-500-italic-DxwFSvPk.woff +0 -0
  100. package/web/dist/assets/spectral-latin-600-normal-DE3OxvnQ.woff2 +0 -0
  101. package/web/dist/assets/spectral-latin-600-normal-DXnQJnYb.woff +0 -0
  102. package/web/dist/assets/spectral-latin-ext-400-italic-BcKU4A_a.woff +0 -0
  103. package/web/dist/assets/spectral-latin-ext-400-italic-BgTM1Mst.woff2 +0 -0
  104. package/web/dist/assets/spectral-latin-ext-400-normal-BHbqXr-7.woff +0 -0
  105. package/web/dist/assets/spectral-latin-ext-400-normal-XDLFwhqg.woff2 +0 -0
  106. package/web/dist/assets/spectral-latin-ext-500-italic-DMigwuN9.woff2 +0 -0
  107. package/web/dist/assets/spectral-latin-ext-500-italic-UnF3HUpx.woff +0 -0
  108. package/web/dist/assets/spectral-latin-ext-600-normal-DKlDBbgb.woff2 +0 -0
  109. package/web/dist/assets/spectral-latin-ext-600-normal-DnKmvInL.woff +0 -0
  110. package/web/dist/assets/spectral-vietnamese-400-italic-D8dhja4j.woff +0 -0
  111. package/web/dist/assets/spectral-vietnamese-400-italic-DO3bsfcj.woff2 +0 -0
  112. package/web/dist/assets/spectral-vietnamese-400-normal-C7CRHYqk.woff2 +0 -0
  113. package/web/dist/assets/spectral-vietnamese-400-normal-CWkMWCzb.woff +0 -0
  114. package/web/dist/assets/spectral-vietnamese-500-italic-e7VYE3f9.woff2 +0 -0
  115. package/web/dist/assets/spectral-vietnamese-500-italic-q7ilnnst.woff +0 -0
  116. package/web/dist/assets/spectral-vietnamese-600-normal-ZNiwCzjU.woff2 +0 -0
  117. package/web/dist/assets/spectral-vietnamese-600-normal-esmjhrFa.woff +0 -0
  118. package/web/dist/index.html +19 -0
@@ -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