@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { hexToBytes } from '@noble/hashes/utils.js';
|
|
2
|
+
import { verifyPreimage } from 'farrier-kit/preimage';
|
|
3
|
+
import { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError } from "./types.js";
|
|
4
|
+
// lnd over its REST proxy. AddInvoice never returns a preimage but accepts
|
|
5
|
+
// one (r_preimage), which is what lets it back a mint. Payment send/track
|
|
6
|
+
// are chunked NDJSON streams read line by line to a terminal status.
|
|
7
|
+
//
|
|
8
|
+
// Not yet exercised against a live node - the semantics are a direct port
|
|
9
|
+
// of the reference mint's lnd backend, which is.
|
|
10
|
+
//
|
|
11
|
+
// TLS: for lnd's self-signed cert, point NODE_EXTRA_CA_CERTS at it.
|
|
12
|
+
const FAILURE_REASONS = {
|
|
13
|
+
FAILURE_REASON_NO_ROUTE: 'Could not find a route to pay this invoice.',
|
|
14
|
+
FAILURE_REASON_INCORRECT_PAYMENT_DETAILS: "The invoice's payment details were rejected.",
|
|
15
|
+
FAILURE_REASON_INSUFFICIENT_BALANCE: 'Insufficient balance to pay this invoice.',
|
|
16
|
+
FAILURE_REASON_TIMEOUT: 'Timed out trying to find a route to pay this invoice.'
|
|
17
|
+
};
|
|
18
|
+
const hexToBase64 = (hex) => Buffer.from(hexToBytes(hex)).toString('base64');
|
|
19
|
+
const decodeHexOrBase64 = (value) => {
|
|
20
|
+
if (/^[0-9a-fA-F]{64}$/.test(value))
|
|
21
|
+
return value.toLowerCase();
|
|
22
|
+
return Buffer.from(value, 'base64').toString('hex');
|
|
23
|
+
};
|
|
24
|
+
export const createLndBackend = (config) => {
|
|
25
|
+
const headers = { 'Grpc-Metadata-macaroon': config.macaroon, 'content-type': 'application/json' };
|
|
26
|
+
const json = async (path, init = {}) => {
|
|
27
|
+
const res = await fetch(`${config.url}${path}`, {
|
|
28
|
+
method: init.method ?? 'GET',
|
|
29
|
+
headers,
|
|
30
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
31
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? 15_000)
|
|
32
|
+
});
|
|
33
|
+
return { ok: res.ok, status: res.status, json: await res.json().catch(() => null) };
|
|
34
|
+
};
|
|
35
|
+
// Reads a chunked NDJSON stream until `until` returns a value. A non-2xx
|
|
36
|
+
// response's body rides back in `error` - grpc-gateway puts the actual
|
|
37
|
+
// refusal reason there, and payInvoice needs to read it.
|
|
38
|
+
const streamUntil = async (path, init, until) => {
|
|
39
|
+
const res = await fetch(`${config.url}${path}`, {
|
|
40
|
+
method: init.method ?? 'GET',
|
|
41
|
+
headers,
|
|
42
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
43
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? 90_000)
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok || !res.body) {
|
|
46
|
+
const error = await res.text().catch(() => '');
|
|
47
|
+
return { status: res.status, result: undefined, error };
|
|
48
|
+
}
|
|
49
|
+
const reader = res.body.getReader();
|
|
50
|
+
const decoder = new TextDecoder();
|
|
51
|
+
let buffer = '';
|
|
52
|
+
try {
|
|
53
|
+
for (;;) {
|
|
54
|
+
const { done, value } = await reader.read();
|
|
55
|
+
if (value)
|
|
56
|
+
buffer += decoder.decode(value, { stream: true });
|
|
57
|
+
const lines = buffer.split('\n');
|
|
58
|
+
buffer = lines.pop() ?? '';
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (!line.trim())
|
|
61
|
+
continue;
|
|
62
|
+
let event;
|
|
63
|
+
try {
|
|
64
|
+
event = JSON.parse(line);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const payment = event?.result ?? event;
|
|
70
|
+
const result = until(payment);
|
|
71
|
+
if (result !== undefined)
|
|
72
|
+
return { status: res.status, result };
|
|
73
|
+
}
|
|
74
|
+
if (done)
|
|
75
|
+
return { status: res.status, result: undefined };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
reader.cancel().catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
name: 'lnd',
|
|
84
|
+
async createInvoice({ amountMsat, preimageHex, memo }) {
|
|
85
|
+
const res = await json('/v1/invoices', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: { value_msat: String(amountMsat), memo, r_preimage: hexToBase64(preimageHex) }
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok || typeof res.json?.payment_request !== 'string') {
|
|
90
|
+
throw new Error(`lnd did not return a payment_request (${res.status}).`);
|
|
91
|
+
}
|
|
92
|
+
return { pr: res.json.payment_request };
|
|
93
|
+
},
|
|
94
|
+
async payInvoice({ pr, feeLimitMsat }) {
|
|
95
|
+
const { result, error } = await streamUntil('/v2/router/send', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: { payment_request: pr, timeout_seconds: 60, fee_limit_msat: String(feeLimitMsat) },
|
|
98
|
+
timeoutMs: 90_000
|
|
99
|
+
}, payment => (payment?.status === 'SUCCEEDED' || payment?.status === 'FAILED' ? payment : undefined));
|
|
100
|
+
if (!result) {
|
|
101
|
+
// lnd refuses a send whose payment hash it already holds - on a
|
|
102
|
+
// shared node that is somebody else's payment, and nothing went
|
|
103
|
+
// out for THIS call. Distinct from ambiguity: safely restorable.
|
|
104
|
+
if (error && /already exists|AlreadyExists|payment is in transition/i.test(error)) {
|
|
105
|
+
throw new PaymentAlreadyKnownError('lnd already has a payment for this hash');
|
|
106
|
+
}
|
|
107
|
+
// The stream ended without a terminal status - genuinely ambiguous.
|
|
108
|
+
throw new Error('lnd did not report a terminal payment status.');
|
|
109
|
+
}
|
|
110
|
+
if (result.status !== 'SUCCEEDED') {
|
|
111
|
+
const reason = FAILURE_REASONS[result.failure_reason] ?? `Payment failed: ${result.failure_reason ?? 'unknown'}.`;
|
|
112
|
+
throw new PaymentFailedError(reason);
|
|
113
|
+
}
|
|
114
|
+
const preimageHex = result.payment_preimage ? decodeHexOrBase64(result.payment_preimage) : null;
|
|
115
|
+
if (!preimageHex)
|
|
116
|
+
throw new Error('lnd did not return a payment_preimage.');
|
|
117
|
+
const feeMsat = result.fee_msat !== undefined ? Number(result.fee_msat) : null;
|
|
118
|
+
return { preimageHex, feeMsat };
|
|
119
|
+
},
|
|
120
|
+
async isPaymentComplete(paymentHashHex) {
|
|
121
|
+
const { status, result } = await streamUntil(`/v2/router/track/${encodeURIComponent(hexToBase64(paymentHashHex))}?no_inflight_updates=true`, { timeoutMs: 15_000 }, payment => {
|
|
122
|
+
if (payment?.status === 'SUCCEEDED')
|
|
123
|
+
return true;
|
|
124
|
+
if (payment?.status === 'FAILED')
|
|
125
|
+
return false;
|
|
126
|
+
if (payment?.status) {
|
|
127
|
+
// Anything non-terminal must not read as "confirmed not paid".
|
|
128
|
+
throw new PaymentPendingError(`lnd reports payment status ${payment.status}`);
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
});
|
|
132
|
+
// A payment lnd never attempted is a 404 - safe to report incomplete,
|
|
133
|
+
// since no HTLC was ever sent for it.
|
|
134
|
+
if (status === 404)
|
|
135
|
+
return false;
|
|
136
|
+
if (result === undefined)
|
|
137
|
+
throw new Error('lnd did not report a payment status.');
|
|
138
|
+
return result;
|
|
139
|
+
},
|
|
140
|
+
async isInvoiceSettled(paymentHashHex) {
|
|
141
|
+
const res = await json(`/v1/invoice/${paymentHashHex}`);
|
|
142
|
+
return res.ok && Boolean(res.json?.settled);
|
|
143
|
+
},
|
|
144
|
+
async invoicePreimage(paymentHashHex) {
|
|
145
|
+
const res = await json(`/v1/invoice/${paymentHashHex}`);
|
|
146
|
+
if (!res.ok || !res.json?.settled || typeof res.json?.r_preimage !== 'string')
|
|
147
|
+
return null;
|
|
148
|
+
const preimageHex = decodeHexOrBase64(res.json.r_preimage);
|
|
149
|
+
return verifyPreimage(preimageHex, paymentHashHex) ? preimageHex : null;
|
|
150
|
+
},
|
|
151
|
+
async paymentPreimage(paymentHashHex) {
|
|
152
|
+
const { result } = await streamUntil(`/v2/router/track/${encodeURIComponent(hexToBase64(paymentHashHex))}?no_inflight_updates=true`, { timeoutMs: 15_000 }, payment => {
|
|
153
|
+
if (payment?.status === 'SUCCEEDED')
|
|
154
|
+
return payment.payment_preimage ?? null;
|
|
155
|
+
if (payment?.status === 'FAILED')
|
|
156
|
+
return null;
|
|
157
|
+
return undefined;
|
|
158
|
+
});
|
|
159
|
+
if (typeof result !== 'string')
|
|
160
|
+
return null;
|
|
161
|
+
const preimageHex = decodeHexOrBase64(result);
|
|
162
|
+
return verifyPreimage(preimageHex, paymentHashHex) ? preimageHex : null;
|
|
163
|
+
},
|
|
164
|
+
async nodeInfo() {
|
|
165
|
+
const res = await json('/v1/getinfo');
|
|
166
|
+
if (!res.ok)
|
|
167
|
+
return {};
|
|
168
|
+
const uris = res.json?.uris ?? [];
|
|
169
|
+
const color = typeof res.json?.color === 'string' ? `#${res.json.color.replace(/^#/, '')}` : undefined;
|
|
170
|
+
const numChannels = Number(res.json?.num_active_channels);
|
|
171
|
+
const numPeers = Number(res.json?.num_peers);
|
|
172
|
+
// Total public capacity, best-effort: the macaroon may not carry
|
|
173
|
+
// offchain:read, and the discovery endpoint works fine without it.
|
|
174
|
+
let capacityMsat;
|
|
175
|
+
const channels = await json('/v1/channels');
|
|
176
|
+
if (channels.ok && Array.isArray(channels.json?.channels)) {
|
|
177
|
+
capacityMsat = channels.json.channels.reduce((sum, channel) => sum + Number(channel.capacity ?? 0) * 1000, 0);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...(res.json?.alias ? { alias: res.json.alias } : {}),
|
|
181
|
+
...(uris[0] || res.json?.identity_pubkey ? { uri: uris[0] ?? res.json.identity_pubkey } : {}),
|
|
182
|
+
...(color && /^#[0-9a-fA-F]{6}$/.test(color) ? { color } : {}),
|
|
183
|
+
// !== undefined narrows for exactOptionalPropertyTypes; the isFinite
|
|
184
|
+
// half keeps a NaN from an unparseable channel capacity out.
|
|
185
|
+
...(capacityMsat !== undefined && Number.isFinite(capacityMsat) ? { capacityMsat } : {}),
|
|
186
|
+
...(Number.isSafeInteger(numChannels) ? { numChannels } : {}),
|
|
187
|
+
...(Number.isSafeInteger(numPeers) ? { numPeers } : {})
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type NodeInfo = {
|
|
2
|
+
alias?: string;
|
|
3
|
+
uri?: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
capacityMsat?: number;
|
|
6
|
+
numChannels?: number;
|
|
7
|
+
numPeers?: number;
|
|
8
|
+
};
|
|
9
|
+
export type PaymentOutcome = {
|
|
10
|
+
preimageHex: string | null;
|
|
11
|
+
feeMsat: number | null;
|
|
12
|
+
};
|
|
13
|
+
export declare class PaymentFailedError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export declare class PaymentPendingError extends Error {
|
|
16
|
+
}
|
|
17
|
+
export declare class PaymentAlreadyKnownError extends Error {
|
|
18
|
+
}
|
|
19
|
+
export interface LightningBackend {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
createInvoice(args: {
|
|
22
|
+
amountMsat: number;
|
|
23
|
+
preimageHex: string;
|
|
24
|
+
memo: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
pr: string;
|
|
27
|
+
}>;
|
|
28
|
+
payInvoice(args: {
|
|
29
|
+
pr: string;
|
|
30
|
+
feeLimitMsat: number;
|
|
31
|
+
}): Promise<PaymentOutcome>;
|
|
32
|
+
isPaymentComplete(paymentHashHex: string): Promise<boolean>;
|
|
33
|
+
isInvoiceSettled(paymentHashHex: string): Promise<boolean>;
|
|
34
|
+
invoicePreimage(paymentHashHex: string): Promise<string | null>;
|
|
35
|
+
paymentPreimage(paymentHashHex: string): Promise<string | null>;
|
|
36
|
+
nodeInfo?(): Promise<NodeInfo>;
|
|
37
|
+
close?(): void | Promise<void>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The funding source. Every amount is integer milli-satoshis.
|
|
2
|
+
//
|
|
3
|
+
// The one non-negotiable capability is createInvoice with a CALLER-SUPPLIED
|
|
4
|
+
// preimage: a LUD-25 mint invoice's preimage IS the bearer note's spend
|
|
5
|
+
// secret, so the mint must know it for certain before the invoice exists.
|
|
6
|
+
// lnd (r_preimage) and cln (invoice preimage=) support this; phoenixd and
|
|
7
|
+
// NIP-47 make_invoice do not, which is why neither can back a mint.
|
|
8
|
+
// The funding source's immediate answer to the payment attempt was a clean,
|
|
9
|
+
// terminal failure - no route, rejected, expired. Distinct from a dropped
|
|
10
|
+
// connection or timeout, where the payment may still have gone out. Even
|
|
11
|
+
// this "clean" failure is only trusted after isPaymentComplete confirms it:
|
|
12
|
+
// a malicious payee holding a hodl invoice can make a backend report
|
|
13
|
+
// failure while an HTLC it already sent stays locked.
|
|
14
|
+
export class PaymentFailedError extends Error {
|
|
15
|
+
}
|
|
16
|
+
// The payment (or the query about it) has no terminal answer yet. Never to
|
|
17
|
+
// be read as "not paid".
|
|
18
|
+
export class PaymentPendingError extends Error {
|
|
19
|
+
}
|
|
20
|
+
// The funding source already holds a payment for this hash that THIS call
|
|
21
|
+
// did not create - on a shared node, another mint (or the operator) paid
|
|
22
|
+
// it. Nothing went out on our behalf, so the caller's melt is safely
|
|
23
|
+
// restorable; confirming by hash would confirm against foreign money.
|
|
24
|
+
export class PaymentAlreadyKnownError extends Error {
|
|
25
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
|
|
4
|
+
import { buildNoteUrl, hashK1 } from 'lnurlcash-kit';
|
|
5
|
+
import { configFromEnv } from "./config.js";
|
|
6
|
+
import { createMoneyer } from "./server.js";
|
|
7
|
+
// moneyer - strike LNURLcash bearer notes.
|
|
8
|
+
//
|
|
9
|
+
// moneyer run against the configured MONEYER_* environment
|
|
10
|
+
// moneyer --dev in-memory fake funding source, an ephemeral signing
|
|
11
|
+
// key, and one funded 21-sat note printed for a wallet
|
|
12
|
+
// to play with. Nothing here is payable.
|
|
13
|
+
const { values } = parseArgs({
|
|
14
|
+
options: {
|
|
15
|
+
dev: { type: 'boolean', default: false },
|
|
16
|
+
help: { type: 'boolean', default: false }
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (values.help) {
|
|
20
|
+
console.log([
|
|
21
|
+
'moneyer - an LNURLcash (LUD-25) mint',
|
|
22
|
+
'',
|
|
23
|
+
'Usage: moneyer [--dev]',
|
|
24
|
+
'',
|
|
25
|
+
' --dev fake funding source, in-memory store, ephemeral signing key,',
|
|
26
|
+
' and a funded 21 sat note printed at startup. Unpayable, for',
|
|
27
|
+
' wallets to develop against.',
|
|
28
|
+
'',
|
|
29
|
+
'Configuration is MONEYER_* environment variables - see README.md.'
|
|
30
|
+
].join('\n'));
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
const env = { ...process.env };
|
|
34
|
+
if (values.dev) {
|
|
35
|
+
env.MONEYER_BACKEND = 'fake';
|
|
36
|
+
env.MONEYER_DB ??= ':memory:';
|
|
37
|
+
env.MONEYER_SIGNING_KEY ??= bytesToHex(randomBytes(32));
|
|
38
|
+
}
|
|
39
|
+
const config = configFromEnv(env);
|
|
40
|
+
if (config.backend.kind === 'fake' && !values.dev) {
|
|
41
|
+
console.error('The fake funding source mints unpayable invoices - refusing to run it outside --dev.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const log = (message) => console.error(`[moneyer] ${message}`);
|
|
45
|
+
const moneyer = await createMoneyer(config, { log });
|
|
46
|
+
console.log(`moneyer listening on ${moneyer.url}`);
|
|
47
|
+
console.log(` lightning address: ${config.username}@${new URL(config.publicOrigin ?? moneyer.url).host}`);
|
|
48
|
+
if (moneyer.signer)
|
|
49
|
+
console.log(` mint pubkey: ${moneyer.signer.pubkey}`);
|
|
50
|
+
console.log(` funding source: ${moneyer.backend.name}`);
|
|
51
|
+
if (values.dev) {
|
|
52
|
+
const k1 = bytesToHex(randomBytes(32));
|
|
53
|
+
moneyer.store.creditNote(hashK1(k1), 21_000);
|
|
54
|
+
console.log(` a 21 sat note: ${buildNoteUrl(`${moneyer.url}/w`, k1, 21_000)}`);
|
|
55
|
+
console.log('\nnothing here is payable - the fake funding source invents its invoices');
|
|
56
|
+
}
|
|
57
|
+
const shutdown = () => {
|
|
58
|
+
moneyer.close().then(() => process.exit(0), () => process.exit(1));
|
|
59
|
+
};
|
|
60
|
+
process.on('SIGINT', shutdown);
|
|
61
|
+
process.on('SIGTERM', shutdown);
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MintFee } from 'lnurlcash-kit';
|
|
2
|
+
export type BackendConfig = {
|
|
3
|
+
kind: 'fake';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'cln';
|
|
6
|
+
url: string;
|
|
7
|
+
rune: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'lnd';
|
|
10
|
+
url: string;
|
|
11
|
+
macaroon: string;
|
|
12
|
+
};
|
|
13
|
+
export type MoneyerConfig = {
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
publicOrigin?: string;
|
|
17
|
+
username: string;
|
|
18
|
+
description: string;
|
|
19
|
+
minSendableMsat: number;
|
|
20
|
+
maxSendableMsat: number;
|
|
21
|
+
minMintMsat: number;
|
|
22
|
+
mintFee: MintFee | null;
|
|
23
|
+
signingKey?: string;
|
|
24
|
+
dbPath: string;
|
|
25
|
+
backend: BackendConfig;
|
|
26
|
+
verify: boolean;
|
|
27
|
+
walletUrl?: string;
|
|
28
|
+
maxK1s: number;
|
|
29
|
+
sunset: boolean;
|
|
30
|
+
};
|
|
31
|
+
export declare const DEFAULTS: {
|
|
32
|
+
readonly host: "127.0.0.1";
|
|
33
|
+
readonly port: 3737;
|
|
34
|
+
readonly username: "mint";
|
|
35
|
+
readonly description: "an LNURLcash note";
|
|
36
|
+
readonly minSendableMsat: 1000;
|
|
37
|
+
readonly maxSendableMsat: 100000000;
|
|
38
|
+
readonly minMintMsat: 1000;
|
|
39
|
+
readonly dbPath: "moneyer.sqlite";
|
|
40
|
+
readonly verify: true;
|
|
41
|
+
readonly maxK1s: 21;
|
|
42
|
+
readonly sunset: false;
|
|
43
|
+
};
|
|
44
|
+
export declare const configFromEnv: (env?: NodeJS.ProcessEnv) => MoneyerConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const DEFAULTS = {
|
|
2
|
+
host: '127.0.0.1',
|
|
3
|
+
port: 3737,
|
|
4
|
+
username: 'mint',
|
|
5
|
+
description: 'an LNURLcash note',
|
|
6
|
+
minSendableMsat: 1000,
|
|
7
|
+
maxSendableMsat: 100_000_000,
|
|
8
|
+
minMintMsat: 1000,
|
|
9
|
+
dbPath: 'moneyer.sqlite',
|
|
10
|
+
verify: true,
|
|
11
|
+
maxK1s: 21,
|
|
12
|
+
sunset: false
|
|
13
|
+
};
|
|
14
|
+
const int = (value, fallback) => {
|
|
15
|
+
if (value === undefined || value === '')
|
|
16
|
+
return fallback;
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
19
|
+
throw new Error(`Expected a non-negative integer, got ${JSON.stringify(value)}.`);
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
};
|
|
23
|
+
const flag = (value, fallback) => {
|
|
24
|
+
if (value === undefined || value === '')
|
|
25
|
+
return fallback;
|
|
26
|
+
return value !== '0' && value.toLowerCase() !== 'false';
|
|
27
|
+
};
|
|
28
|
+
const HEX32 = /^[0-9a-f]{64}$/i;
|
|
29
|
+
// Reads MONEYER_* from the environment. Throws rather than guessing: a mint
|
|
30
|
+
// that starts with a half-understood configuration is holding other
|
|
31
|
+
// people's money on a misunderstanding.
|
|
32
|
+
export const configFromEnv = (env = process.env) => {
|
|
33
|
+
const baseFeeMsat = int(env.MONEYER_BASE_FEE_MSAT, 0);
|
|
34
|
+
const feePpm = int(env.MONEYER_FEE_PPM, 0);
|
|
35
|
+
if (feePpm >= 1_000_000) {
|
|
36
|
+
throw new Error('MONEYER_FEE_PPM must be below 1000000 - a 100% fee can never net a note.');
|
|
37
|
+
}
|
|
38
|
+
const signingKey = env.MONEYER_SIGNING_KEY;
|
|
39
|
+
if (signingKey !== undefined && !HEX32.test(signingKey)) {
|
|
40
|
+
throw new Error('MONEYER_SIGNING_KEY must be 32 bytes of hex.');
|
|
41
|
+
}
|
|
42
|
+
const kind = env.MONEYER_BACKEND ?? 'fake';
|
|
43
|
+
let backend;
|
|
44
|
+
if (kind === 'fake') {
|
|
45
|
+
backend = { kind: 'fake' };
|
|
46
|
+
}
|
|
47
|
+
else if (kind === 'cln') {
|
|
48
|
+
if (!env.MONEYER_BACKEND_URL || !env.MONEYER_BACKEND_RUNE) {
|
|
49
|
+
throw new Error('The cln backend needs MONEYER_BACKEND_URL and MONEYER_BACKEND_RUNE.');
|
|
50
|
+
}
|
|
51
|
+
backend = { kind: 'cln', url: env.MONEYER_BACKEND_URL, rune: env.MONEYER_BACKEND_RUNE };
|
|
52
|
+
}
|
|
53
|
+
else if (kind === 'lnd') {
|
|
54
|
+
if (!env.MONEYER_BACKEND_URL || !env.MONEYER_BACKEND_MACAROON) {
|
|
55
|
+
throw new Error('The lnd backend needs MONEYER_BACKEND_URL and MONEYER_BACKEND_MACAROON.');
|
|
56
|
+
}
|
|
57
|
+
backend = { kind: 'lnd', url: env.MONEYER_BACKEND_URL, macaroon: env.MONEYER_BACKEND_MACAROON };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(`Unknown MONEYER_BACKEND ${JSON.stringify(kind)} - fake, cln or lnd.`);
|
|
61
|
+
}
|
|
62
|
+
const minSendableMsat = int(env.MONEYER_MIN_SENDABLE_MSAT, DEFAULTS.minSendableMsat);
|
|
63
|
+
const maxSendableMsat = int(env.MONEYER_MAX_SENDABLE_MSAT, DEFAULTS.maxSendableMsat);
|
|
64
|
+
if (minSendableMsat > maxSendableMsat) {
|
|
65
|
+
throw new Error('MONEYER_MIN_SENDABLE_MSAT exceeds MONEYER_MAX_SENDABLE_MSAT.');
|
|
66
|
+
}
|
|
67
|
+
// Wallets are sent to this origin for every callback, so a malformed one
|
|
68
|
+
// must fail at startup, not as a 500 on the first request.
|
|
69
|
+
const publicOrigin = env.MONEYER_PUBLIC_ORIGIN;
|
|
70
|
+
if (publicOrigin) {
|
|
71
|
+
let protocol;
|
|
72
|
+
try {
|
|
73
|
+
protocol = new URL(publicOrigin).protocol;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new Error(`MONEYER_PUBLIC_ORIGIN is not a URL: ${JSON.stringify(publicOrigin)}.`);
|
|
77
|
+
}
|
|
78
|
+
if (protocol !== 'https:' && protocol !== 'http:') {
|
|
79
|
+
throw new Error(`MONEYER_PUBLIC_ORIGIN must be http or https, got ${JSON.stringify(publicOrigin)}.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
host: env.MONEYER_HOST ?? DEFAULTS.host,
|
|
84
|
+
port: int(env.MONEYER_PORT, DEFAULTS.port),
|
|
85
|
+
...(publicOrigin ? { publicOrigin } : {}),
|
|
86
|
+
username: env.MONEYER_USERNAME ?? DEFAULTS.username,
|
|
87
|
+
description: env.MONEYER_DESCRIPTION ?? DEFAULTS.description,
|
|
88
|
+
minSendableMsat,
|
|
89
|
+
maxSendableMsat,
|
|
90
|
+
minMintMsat: int(env.MONEYER_MIN_MINT_MSAT, DEFAULTS.minMintMsat),
|
|
91
|
+
mintFee: baseFeeMsat === 0 && feePpm === 0 ? null : { baseFeeMsat, feePpm },
|
|
92
|
+
...(signingKey ? { signingKey: signingKey.toLowerCase() } : {}),
|
|
93
|
+
dbPath: env.MONEYER_DB ?? DEFAULTS.dbPath,
|
|
94
|
+
backend,
|
|
95
|
+
verify: flag(env.MONEYER_VERIFY, DEFAULTS.verify),
|
|
96
|
+
...(env.MONEYER_WALLET_URL ? { walletUrl: env.MONEYER_WALLET_URL.replace(/\/+$/, '') } : {}),
|
|
97
|
+
maxK1s: int(env.MONEYER_MAX_K1S, DEFAULTS.maxK1s),
|
|
98
|
+
sunset: flag(env.MONEYER_SUNSET, DEFAULTS.sunset)
|
|
99
|
+
};
|
|
100
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { configFromEnv, DEFAULTS, type MoneyerConfig, type BackendConfig } from './config.ts';
|
|
2
|
+
export { createMoneyer, type Moneyer, type MoneyerDeps } from './server.ts';
|
|
3
|
+
export { NoteStore, NotePendingError, NoteUnavailableError, OutputCollisionError, type NoteRow, type NoteState, type MeltRow, type MintInvoiceRow } from './store.ts';
|
|
4
|
+
export { createNoteSigner, noteIdSignatureDigest, type NoteSigner } from './signing.ts';
|
|
5
|
+
export { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError, type LightningBackend, type NodeInfo, type PaymentOutcome } from './backends/types.ts';
|
|
6
|
+
export { createFakeBackend, type FakeBackend, type FakePayMode } from './backends/fake.ts';
|
|
7
|
+
export { createClnBackend } from './backends/cln.ts';
|
|
8
|
+
export { createLndBackend } from './backends/lnd.ts';
|
|
9
|
+
export { fakeBolt11 } from './backends/fake-bolt11.ts';
|
|
10
|
+
export { runMelt, reconcilePendingMelts, type MeltJob, type MeltDeps } from './melt.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// moneyer - an LNURLcash (LUD-25) mint.
|
|
2
|
+
//
|
|
3
|
+
// A bearer note is an ordinary LUD-03 withdrawRequest link whose k1 IS the
|
|
4
|
+
// asset. This SERVICE mints them against a paid invoice whose preimage
|
|
5
|
+
// becomes the note's spend secret, honours rotate/split/merge/melt on the
|
|
6
|
+
// withdraw callback, signs every note it mints for offline verification,
|
|
7
|
+
// and holds the melt discipline: burn only on confirmed payment, restore
|
|
8
|
+
// only on confirmed non-payment, park everything else as pending.
|
|
9
|
+
//
|
|
10
|
+
// Independent implementation. Reference stack (dni, MIT):
|
|
11
|
+
// spec https://github.com/lnurl/luds/pull/301
|
|
12
|
+
// mint https://github.com/dni/lnurl-mint
|
|
13
|
+
// wallet https://github.com/dni/lnurl-wallet
|
|
14
|
+
export { configFromEnv, DEFAULTS } from "./config.js";
|
|
15
|
+
export { createMoneyer } from "./server.js";
|
|
16
|
+
export { NoteStore, NotePendingError, NoteUnavailableError, OutputCollisionError } from "./store.js";
|
|
17
|
+
export { createNoteSigner, noteIdSignatureDigest } from "./signing.js";
|
|
18
|
+
export { PaymentAlreadyKnownError, PaymentFailedError, PaymentPendingError } from "./backends/types.js";
|
|
19
|
+
export { createFakeBackend } from "./backends/fake.js";
|
|
20
|
+
export { createClnBackend } from "./backends/cln.js";
|
|
21
|
+
export { createLndBackend } from "./backends/lnd.js";
|
|
22
|
+
export { fakeBolt11 } from "./backends/fake-bolt11.js";
|
|
23
|
+
export { runMelt, reconcilePendingMelts } from "./melt.js";
|
package/dist/landing.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { applyMintFee } from 'lnurlcash-kit';
|
|
2
|
+
// The mint's face: one self-contained page at GET /, no build step, no
|
|
3
|
+
// external assets. It states what a visitor needs before trusting a mint
|
|
4
|
+
// with sats: who it is, what it charges, the key its notes verify
|
|
5
|
+
// against, and how to pay it. Anything fancier belongs to the product
|
|
6
|
+
// site, not the money service.
|
|
7
|
+
const escapeHtml = (value) => value.replace(/[&<>"']/g, char => `&#${char.charCodeAt(0)};`);
|
|
8
|
+
export const landingPage = (args) => {
|
|
9
|
+
const { config, host, mintPubkey, nodeInfo } = args;
|
|
10
|
+
const address = `${config.username}@${host}`;
|
|
11
|
+
const fee = config.mintFee;
|
|
12
|
+
const feeLine = fee
|
|
13
|
+
? `${fee.baseFeeMsat > 0 ? `${fee.baseFeeMsat} msat flat` : ''}${fee.baseFeeMsat > 0 && fee.feePpm > 0 ? ' + ' : ''}${fee.feePpm > 0 ? `${fee.feePpm / 10_000}%` : ''}`
|
|
14
|
+
: 'none';
|
|
15
|
+
const maxNet = fee ? applyMintFee(config.maxSendableMsat, fee) : config.maxSendableMsat;
|
|
16
|
+
const sats = (msat) => `${(msat / 1000).toLocaleString('en-GB')} sat`;
|
|
17
|
+
return `<!doctype html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="utf-8"/>
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
22
|
+
<title>${escapeHtml(host)} - an LNURLcash mint</title>
|
|
23
|
+
<meta name="description" content="A moneyer strikes Lightning bearer notes. Pay ${escapeHtml(address)} and the invoice's preimage becomes your note."/>
|
|
24
|
+
<style>
|
|
25
|
+
:root{--bg:#0e0f12;--raise:#16181d;--line:rgba(226,233,242,.09);--ink:#eef1f6;--dim:#98a0ac;--accent:#c9ced8;--accent-deep:#8f97a4}
|
|
26
|
+
@media(prefers-color-scheme:light){:root{--bg:#f3f4f6;--raise:#fff;--line:rgba(30,38,50,.12);--ink:#1c2027;--dim:#6b7380;--accent:#6f7784;--accent-deep:#4d545f}}
|
|
27
|
+
*{box-sizing:border-box;margin:0}
|
|
28
|
+
body{background:var(--bg);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;-webkit-font-smoothing:antialiased;min-height:100vh;display:grid;place-items:center;padding:28px;
|
|
29
|
+
background-image:radial-gradient(900px 420px at 50% -160px,rgba(201,206,216,.13),transparent 70%)}
|
|
30
|
+
main{max-width:560px;width:100%;display:flex;flex-direction:column;gap:18px;animation:rise .6s cubic-bezier(.2,.7,.2,1) both}
|
|
31
|
+
@keyframes rise{from{opacity:0;transform:translateY(16px)}}
|
|
32
|
+
.mark{width:64px;height:64px;color:var(--accent);animation:glow 3.2s ease-in-out infinite}
|
|
33
|
+
@keyframes glow{50%{filter:drop-shadow(0 0 14px rgba(201,206,216,.5))}}
|
|
34
|
+
h1{font-size:clamp(30px,7vw,40px);letter-spacing:-.5px}
|
|
35
|
+
h1 small{display:block;font-size:16px;color:var(--dim);font-weight:500;margin-top:8px;line-height:1.5}
|
|
36
|
+
.addr{display:flex;align-items:center;gap:12px;background:linear-gradient(135deg,var(--accent),var(--accent-deep));color:#14161b;border-radius:20px;padding:20px 22px;font-weight:750;font-size:clamp(17px,4.6vw,22px);word-break:break-all;box-shadow:0 14px 38px -14px rgba(160,170,185,.5)}
|
|
37
|
+
.addr svg{width:26px;height:26px;flex:none}
|
|
38
|
+
.card{background:var(--raise);border:1px solid var(--line);border-radius:20px;padding:20px 22px;display:flex;flex-direction:column;gap:12px}
|
|
39
|
+
.kv{display:flex;justify-content:space-between;gap:16px;font-size:15px}
|
|
40
|
+
.kv span{color:var(--dim)}
|
|
41
|
+
.kv code{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;word-break:break-all;text-align:right}
|
|
42
|
+
p.small{color:var(--dim);font-size:13.5px;line-height:1.65;text-align:center}
|
|
43
|
+
a{color:var(--accent)}
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<main>
|
|
48
|
+
<svg class="mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.6v8.8"/><path d="M15.4 9.4c-.7-1.1-1.9-1.8-3.4-1.8-2 0-3.6 1.1-3.6 2.7 0 3.4 7.2 1.8 7.2 5 0 1.6-1.6 2.7-3.6 2.7-1.5 0-2.7-.7-3.4-1.8"/></svg>
|
|
49
|
+
<h1>${escapeHtml(nodeInfo.alias ?? 'moneyer')}<small>An LNURLcash mint. Pay the address below and the invoice's payment preimage <em>is</em> your bearer note - money as a secret you hold.</small></h1>
|
|
50
|
+
<div class="addr"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4.5 13.5H11L9.5 22 18 10.5h-6.5L13 2z"/></svg>${escapeHtml(address)}</div>
|
|
51
|
+
<div class="card">
|
|
52
|
+
<div class="kv"><span>mints</span><b>${sats(config.minSendableMsat)} to ${sats(config.maxSendableMsat)}</b></div>
|
|
53
|
+
<div class="kv"><span>mint fee</span><b>${escapeHtml(feeLine)}</b></div>
|
|
54
|
+
<div class="kv"><span>largest note</span><b>${sats(maxNet)}</b></div>
|
|
55
|
+
${mintPubkey ? `<div class="kv"><span>notes signed by</span><code>${escapeHtml(mintPubkey)}</code></div>` : '<div class="kv"><span>note signatures</span><b>not offered</b></div>'}
|
|
56
|
+
${nodeInfo.uri ? `<div class="kv"><span>node</span><code>${escapeHtml(nodeInfo.uri)}</code></div>` : ''}
|
|
57
|
+
${config.sunset ? '<div class="kv"><span>status</span><b>sunsetting - redeem only</b></div>' : ''}
|
|
58
|
+
</div>
|
|
59
|
+
<p class="small">Works with any LUD-25 wallet - <a href="https://github.com/forgesworn/notecase">notecase</a> among them. Verify a note offline against the signing key above.<br/>Independent implementation of the <a href="https://github.com/lnurl/luds/pull/301">LNURLcash draft</a> - graded by <a href="https://github.com/TheCryptoDonkey/lnurlcash-conformance">lnurlcash-conformance</a>.</p>
|
|
60
|
+
</main>
|
|
61
|
+
</body>
|
|
62
|
+
</html>`;
|
|
63
|
+
};
|
package/dist/melt.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LightningBackend } from './backends/types.ts';
|
|
2
|
+
import type { NoteStore } from './store.ts';
|
|
3
|
+
export type MeltJob = {
|
|
4
|
+
paymentHash: string;
|
|
5
|
+
noteId: string;
|
|
6
|
+
pr: string;
|
|
7
|
+
amountMsat: number;
|
|
8
|
+
};
|
|
9
|
+
export type MeltDeps = {
|
|
10
|
+
store: NoteStore;
|
|
11
|
+
backend: LightningBackend;
|
|
12
|
+
feeLimitMsat: (amountMsat: number) => number;
|
|
13
|
+
confirmDelaysMs?: number[];
|
|
14
|
+
log?: (message: string) => void;
|
|
15
|
+
};
|
|
16
|
+
export declare const runMelt: (job: MeltJob, deps: MeltDeps) => Promise<void>;
|
|
17
|
+
export declare const reconcilePendingMelts: (store: NoteStore, backend: LightningBackend, inFlight: ReadonlySet<string>, log?: (message: string) => void) => Promise<void>;
|