@forgesworn/moneyer 0.1.2 → 0.2.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 +170 -0
- package/README.md +447 -11
- package/THREAT-MODEL.md +15 -6
- package/dist/admin.d.ts +12 -0
- package/dist/admin.js +410 -0
- package/dist/announce.d.ts +12 -0
- package/dist/announce.js +82 -0
- package/dist/backends/cln.js +30 -8
- package/dist/backends/fake-bolt11.d.ts +1 -1
- package/dist/backends/fake-bolt11.js +3 -1
- package/dist/backends/fake.d.ts +7 -1
- package/dist/backends/fake.js +27 -11
- package/dist/backends/lnd.js +30 -6
- package/dist/backends/types.d.ts +3 -0
- package/dist/cli.js +25 -3
- package/dist/config.d.ts +29 -0
- package/dist/config.js +194 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +42 -2
- package/dist/melt.d.ts +1 -0
- package/dist/melt.js +2 -1
- package/dist/names.d.ts +39 -0
- package/dist/names.js +172 -0
- package/dist/privacy.d.ts +2 -0
- package/dist/privacy.js +14 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +561 -41
- package/dist/signing.d.ts +2 -0
- package/dist/signing.js +34 -0
- package/dist/stats.d.ts +31 -0
- package/dist/stats.js +115 -0
- package/dist/store.d.ts +105 -3
- package/dist/store.js +355 -19
- package/dist/version.d.ts +1 -0
- package/dist/version.js +17 -0
- package/dist/zap.d.ts +52 -0
- package/dist/zap.js +294 -0
- package/llms.txt +25 -8
- package/package.json +4 -3
package/dist/landing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MINT_KNOWS, MINT_KNOWS_HEADING } from "./privacy.js";
|
|
1
2
|
import { applyMintFee } from 'lnurlcash-kit';
|
|
2
3
|
// The mint's face: one self-contained page at GET /, no build step, no
|
|
3
4
|
// external assets. It states what a visitor needs before trusting a mint
|
|
@@ -7,6 +8,7 @@ import { applyMintFee } from 'lnurlcash-kit';
|
|
|
7
8
|
const escapeHtml = (value) => value.replace(/[&<>"']/g, char => `&#${char.charCodeAt(0)};`);
|
|
8
9
|
export const landingPage = (args) => {
|
|
9
10
|
const { config, host, mintPubkey, nodeInfo } = args;
|
|
11
|
+
const stats = args.stats ?? null;
|
|
10
12
|
const address = `${config.username}@${host}`;
|
|
11
13
|
const fee = config.mintFee;
|
|
12
14
|
const feeLine = fee
|
|
@@ -14,12 +16,37 @@ export const landingPage = (args) => {
|
|
|
14
16
|
: 'none';
|
|
15
17
|
const maxNet = fee ? applyMintFee(config.maxSendableMsat, fee) : config.maxSendableMsat;
|
|
16
18
|
const sats = (msat) => `${(msat / 1000).toLocaleString('en-GB')} sat`;
|
|
19
|
+
const title = config.name ?? nodeInfo.alias ?? 'moneyer';
|
|
20
|
+
// Contacts are shown as text, not links: a mailto or an npub someone
|
|
21
|
+
// else chose is not something this page should hand a click to. The
|
|
22
|
+
// terms are a link because a URL the operator set is the point of it.
|
|
23
|
+
// "coverage 1.92x (outstanding 48,120 sat, node 92,400 sat)", or the
|
|
24
|
+
// ratio alone in ratio-only mode. A mint with nothing outstanding has
|
|
25
|
+
// no ratio to state, so it says so in words instead.
|
|
26
|
+
const coverageLine = (() => {
|
|
27
|
+
if (!stats)
|
|
28
|
+
return null;
|
|
29
|
+
if (stats.coverage !== undefined) {
|
|
30
|
+
const ratio = `${stats.coverage.toFixed(2)}\u00d7`;
|
|
31
|
+
return stats.outstandingMsat === undefined || stats.localBalanceMsat === undefined
|
|
32
|
+
? ratio
|
|
33
|
+
: `${ratio} (outstanding ${sats(stats.outstandingMsat)}, node ${sats(stats.localBalanceMsat)})`;
|
|
34
|
+
}
|
|
35
|
+
if (stats.outstandingMsat === 0)
|
|
36
|
+
return 'nothing outstanding';
|
|
37
|
+
return stats.outstandingMsat === undefined ? null : `outstanding ${sats(stats.outstandingMsat)}`;
|
|
38
|
+
})();
|
|
39
|
+
const contacts = [
|
|
40
|
+
config.contact?.email ? { label: 'email', value: config.contact.email } : null,
|
|
41
|
+
config.contact?.nostr ? { label: 'nostr', value: config.contact.nostr } : null,
|
|
42
|
+
config.contact?.url ? { label: 'contact', value: config.contact.url } : null
|
|
43
|
+
].filter(entry => entry !== null);
|
|
17
44
|
return `<!doctype html>
|
|
18
45
|
<html lang="en">
|
|
19
46
|
<head>
|
|
20
47
|
<meta charset="utf-8"/>
|
|
21
48
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
22
|
-
<title>${escapeHtml(
|
|
49
|
+
<title>${escapeHtml(title)} - an LNURLcash mint</title>
|
|
23
50
|
<meta name="description" content="A moneyer strikes Lightning bearer notes. Pay ${escapeHtml(address)} and the invoice's preimage becomes your note."/>
|
|
24
51
|
<style>
|
|
25
52
|
:root{--bg:#0e0f12;--raise:#16181d;--line:rgba(226,233,242,.09);--ink:#eef1f6;--dim:#98a0ac;--accent:#c9ced8;--accent-deep:#8f97a4}
|
|
@@ -40,13 +67,19 @@ h1 small{display:block;font-size:16px;color:var(--dim);font-weight:500;margin-to
|
|
|
40
67
|
.kv span{color:var(--dim)}
|
|
41
68
|
.kv code{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;word-break:break-all;text-align:right}
|
|
42
69
|
p.small{color:var(--dim);font-size:13.5px;line-height:1.65;text-align:center}
|
|
70
|
+
.knows{background:var(--raise);border:1px solid var(--line);border-radius:20px;padding:20px 22px;display:flex;flex-direction:column;gap:10px}
|
|
71
|
+
.knows h2{font-size:15px;letter-spacing:.04em;text-transform:uppercase;color:var(--dim);font-weight:600}
|
|
72
|
+
.knows p{font-size:14px;line-height:1.65}
|
|
73
|
+
.motd{background:var(--raise);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:14px;padding:14px 18px;font-size:14.5px;line-height:1.6}
|
|
74
|
+
.motd b{display:block;font-size:12px;letter-spacing:.09em;text-transform:uppercase;color:var(--dim);margin-bottom:4px}
|
|
43
75
|
a{color:var(--accent)}
|
|
44
76
|
</style>
|
|
45
77
|
</head>
|
|
46
78
|
<body>
|
|
47
79
|
<main>
|
|
48
80
|
<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(
|
|
81
|
+
<h1>${escapeHtml(title)}<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>
|
|
82
|
+
${config.motd ? `<div class="motd"><b>notice</b>${escapeHtml(config.motd)}</div>` : ''}
|
|
50
83
|
<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
84
|
<div class="card">
|
|
52
85
|
<div class="kv"><span>mints</span><b>${sats(config.minSendableMsat)} to ${sats(config.maxSendableMsat)}</b></div>
|
|
@@ -54,8 +87,15 @@ a{color:var(--accent)}
|
|
|
54
87
|
<div class="kv"><span>largest note</span><b>${sats(maxNet)}</b></div>
|
|
55
88
|
${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
89
|
${nodeInfo.uri ? `<div class="kv"><span>node</span><code>${escapeHtml(nodeInfo.uri)}</code></div>` : ''}
|
|
90
|
+
${coverageLine ? `<div class="kv"><span>coverage</span><b>${escapeHtml(coverageLine)}</b></div>` : ''}
|
|
57
91
|
${config.sunset ? '<div class="kv"><span>status</span><b>sunsetting - redeem only</b></div>' : ''}
|
|
92
|
+
${contacts.map(entry => `<div class="kv"><span>${entry.label}</span><code>${escapeHtml(entry.value)}</code></div>`).join('\n')}
|
|
93
|
+
${config.tosUrl ? `<div class="kv"><span>terms</span><a href="${escapeHtml(config.tosUrl)}" rel="noopener noreferrer">${escapeHtml(config.tosUrl)}</a></div>` : ''}
|
|
58
94
|
</div>
|
|
95
|
+
<section class="knows">
|
|
96
|
+
<h2>${escapeHtml(MINT_KNOWS_HEADING)}</h2>
|
|
97
|
+
${MINT_KNOWS.map(paragraph => `<p>${escapeHtml(paragraph)}</p>`).join('\n')}
|
|
98
|
+
</section>
|
|
59
99
|
<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
100
|
</main>
|
|
61
101
|
</body>
|
package/dist/melt.d.ts
CHANGED
package/dist/melt.js
CHANGED
|
@@ -32,7 +32,8 @@ export const runMelt = async (job, deps) => {
|
|
|
32
32
|
try {
|
|
33
33
|
outcome = await deps.backend.payInvoice({
|
|
34
34
|
pr: job.pr,
|
|
35
|
-
feeLimitMsat: deps.feeLimitMsat(job.amountMsat)
|
|
35
|
+
feeLimitMsat: deps.feeLimitMsat(job.amountMsat),
|
|
36
|
+
...(job.payAmountMsat !== undefined ? { amountMsat: job.payAmountMsat } : {})
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
catch (err) {
|
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NoteStore } from './store.ts';
|
|
2
|
+
export declare const NIP98_KIND = 27235;
|
|
3
|
+
export declare const NIP98_WINDOW_SECS = 60;
|
|
4
|
+
export declare const NAME_RULE: RegExp;
|
|
5
|
+
export declare const ALWAYS_RESERVED: readonly ["_", "admin", "mint"];
|
|
6
|
+
export declare const FREE_NAMES_PER_PUBKEY = 3;
|
|
7
|
+
export type Nip98Failure = {
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
export type Nip98Success = {
|
|
11
|
+
pubkey: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const validateNip98: (authorization: string | undefined, request: {
|
|
14
|
+
url: string;
|
|
15
|
+
method: string;
|
|
16
|
+
body: string;
|
|
17
|
+
nowSecs?: number;
|
|
18
|
+
}) => Nip98Success | Nip98Failure;
|
|
19
|
+
export type NameRefusal = {
|
|
20
|
+
reason: string;
|
|
21
|
+
status: number;
|
|
22
|
+
};
|
|
23
|
+
export type NameGranted = {
|
|
24
|
+
name: string;
|
|
25
|
+
pubkey: string;
|
|
26
|
+
paidMsat: number;
|
|
27
|
+
};
|
|
28
|
+
export declare const isRefusal: (result: NameGranted | NameRefusal) => result is NameRefusal;
|
|
29
|
+
export declare const registerName: (args: {
|
|
30
|
+
store: NoteStore;
|
|
31
|
+
pubkey: string;
|
|
32
|
+
body: {
|
|
33
|
+
name?: unknown;
|
|
34
|
+
note?: unknown;
|
|
35
|
+
};
|
|
36
|
+
priceMsat: number | undefined;
|
|
37
|
+
reserved: string[];
|
|
38
|
+
host: string;
|
|
39
|
+
}) => NameGranted | NameRefusal;
|
package/dist/names.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
|
+
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
3
|
+
import { verifyEvent } from 'nostr-tools/pure';
|
|
4
|
+
import { hashK1, noteK1 } from 'lnurlcash-kit';
|
|
5
|
+
import { NotePendingError, NoteStore, NoteUnavailableError } from "./store.js";
|
|
6
|
+
// Self-service lightning addresses.
|
|
7
|
+
//
|
|
8
|
+
// A name here is a lightning address whose payouts are bearer notes
|
|
9
|
+
// gift-wrapped to the holder's own Nostr key: custodial for the seconds
|
|
10
|
+
// between the zap settling and the wrap going out, theirs afterwards.
|
|
11
|
+
// Anyone with an npub can have one, which is a reason to run a mint and a
|
|
12
|
+
// reason to install a wallet.
|
|
13
|
+
//
|
|
14
|
+
// The identity is the NIP-98 signature and nothing else. There is no
|
|
15
|
+
// account, no password and no recovery: the key that signed the
|
|
16
|
+
// registration owns the name.
|
|
17
|
+
export const NIP98_KIND = 27235;
|
|
18
|
+
// Sixty seconds, as NIP-98 suggests. Long enough for a slow phone, short
|
|
19
|
+
// enough that a captured Authorization header is worth little.
|
|
20
|
+
export const NIP98_WINDOW_SECS = 60;
|
|
21
|
+
// Three to thirty-two characters, starting with a letter or digit. The
|
|
22
|
+
// same shape a lightning address local part and a NIP-05 name can both
|
|
23
|
+
// carry without quoting.
|
|
24
|
+
export const NAME_RULE = /^[a-z0-9][a-z0-9_.-]{2,31}$/;
|
|
25
|
+
// Never registrable, whatever the mint is called: `_` is LUD-16's
|
|
26
|
+
// bare-domain alias, and the other two are what a person types when they
|
|
27
|
+
// mean the operator.
|
|
28
|
+
export const ALWAYS_RESERVED = ['_', 'admin', 'mint'];
|
|
29
|
+
// How many free names one pubkey may hold. Paid registration needs no cap
|
|
30
|
+
// - the price is the cap.
|
|
31
|
+
export const FREE_NAMES_PER_PUBKEY = 3;
|
|
32
|
+
// NIP-98 over one request. Deliberately checked here rather than through
|
|
33
|
+
// a helper: the payload tag must commit to the RAW body this handler
|
|
34
|
+
// read, not to a re-serialisation of it, or a client whose JSON differs
|
|
35
|
+
// from ours by a space is refused for no reason.
|
|
36
|
+
export const validateNip98 = (authorization, request) => {
|
|
37
|
+
const header = authorization?.trim();
|
|
38
|
+
if (!header)
|
|
39
|
+
return { reason: 'Missing NIP-98 Authorization header.' };
|
|
40
|
+
const match = /^Nostr\s+(.+)$/i.exec(header);
|
|
41
|
+
if (!match)
|
|
42
|
+
return { reason: 'Authorization must be "Nostr <base64 event>".' };
|
|
43
|
+
let event;
|
|
44
|
+
try {
|
|
45
|
+
event = JSON.parse(Buffer.from(match[1].trim(), 'base64').toString('utf8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { reason: 'Authorization is not a base64 Nostr event.' };
|
|
49
|
+
}
|
|
50
|
+
if (event?.kind !== NIP98_KIND)
|
|
51
|
+
return { reason: `Authorization event must be kind ${NIP98_KIND}.` };
|
|
52
|
+
if (!verifyEvent(event))
|
|
53
|
+
return { reason: 'Authorization event signature does not verify.' };
|
|
54
|
+
const nowSecs = request.nowSecs ?? Math.floor(Date.now() / 1000);
|
|
55
|
+
if (Math.abs(nowSecs - event.created_at) > NIP98_WINDOW_SECS) {
|
|
56
|
+
return { reason: `Authorization event is not within ${NIP98_WINDOW_SECS} seconds of now.` };
|
|
57
|
+
}
|
|
58
|
+
const tag = (name) => event.tags.find(t => t[0] === name)?.[1];
|
|
59
|
+
// The URL is compared without its query string, and with a trailing
|
|
60
|
+
// slash ignored: those are the two differences a proxy or an HTTP
|
|
61
|
+
// client introduces on its own.
|
|
62
|
+
const canonical = (value) => {
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(value);
|
|
65
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, '')}`;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const signedUrl = tag('u');
|
|
72
|
+
if (!signedUrl || canonical(signedUrl) === null || canonical(signedUrl) !== canonical(request.url)) {
|
|
73
|
+
return { reason: 'Authorization event was signed for a different URL.' };
|
|
74
|
+
}
|
|
75
|
+
if ((tag('method') ?? '').toUpperCase() !== request.method.toUpperCase()) {
|
|
76
|
+
return { reason: 'Authorization event was signed for a different method.' };
|
|
77
|
+
}
|
|
78
|
+
const payload = tag('payload');
|
|
79
|
+
if (request.body.length > 0) {
|
|
80
|
+
if (!payload)
|
|
81
|
+
return { reason: 'Authorization event has no payload tag for this body.' };
|
|
82
|
+
if (payload.toLowerCase() !== bytesToHex(sha256(utf8ToBytes(request.body)))) {
|
|
83
|
+
return { reason: 'Authorization event payload tag does not match the body.' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { pubkey: event.pubkey };
|
|
87
|
+
};
|
|
88
|
+
const refuse = (reason, status) => ({ reason, status });
|
|
89
|
+
export const isRefusal = (result) => 'reason' in result;
|
|
90
|
+
// The whole registration, minus the transport. Burning the note and
|
|
91
|
+
// inserting the name are the two mutations, in that order: a name that
|
|
92
|
+
// exists without payment is worse than a note burned without a name,
|
|
93
|
+
// and the INSERT is what settles a race between two people asking for
|
|
94
|
+
// the same name at once.
|
|
95
|
+
export const registerName = (args) => {
|
|
96
|
+
const { store, pubkey, priceMsat } = args;
|
|
97
|
+
if (priceMsat === undefined)
|
|
98
|
+
return refuse('This mint is not registering names.', 404);
|
|
99
|
+
const raw = typeof args.body.name === 'string' ? args.body.name.trim().toLowerCase() : '';
|
|
100
|
+
if (!NAME_RULE.test(raw)) {
|
|
101
|
+
return refuse('A name is 3 to 32 characters of a-z, 0-9, dot, dash or underscore, starting with a letter or digit.', 400);
|
|
102
|
+
}
|
|
103
|
+
if ([...ALWAYS_RESERVED, ...args.reserved.map(name => name.toLowerCase())].includes(raw)) {
|
|
104
|
+
return refuse('That name is reserved.', 403);
|
|
105
|
+
}
|
|
106
|
+
if (store.zapName(raw))
|
|
107
|
+
return refuse('That name is taken.', 409);
|
|
108
|
+
let paidMsat = 0;
|
|
109
|
+
let noteId;
|
|
110
|
+
if (priceMsat === 0) {
|
|
111
|
+
// Free names are rationed per key. The per-address limit is the
|
|
112
|
+
// reverse proxy's job; this is the one the mint can enforce itself.
|
|
113
|
+
if (store.zapNameCountFor(pubkey) >= FREE_NAMES_PER_PUBKEY) {
|
|
114
|
+
return refuse(`One key may hold ${FREE_NAMES_PER_PUBKEY} free names.`, 403);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const offered = typeof args.body.note === 'string' ? args.body.note.trim() : '';
|
|
119
|
+
if (!offered)
|
|
120
|
+
return refuse(`This name costs ${priceMsat} msat - send a note of this mint in "note".`, 402);
|
|
121
|
+
// A bare secret is accepted as readily as a full note URL; a URL for
|
|
122
|
+
// somebody else's mint is not, however good the note is. The URL is
|
|
123
|
+
// read here rather than through the kit's resolver, which refuses a
|
|
124
|
+
// plain-http service - correct for a wallet reaching a stranger, and
|
|
125
|
+
// wrong for a mint reading a note it minted itself.
|
|
126
|
+
let k1 = null;
|
|
127
|
+
if (/^[0-9a-f]{64}$/i.test(offered)) {
|
|
128
|
+
k1 = offered.toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
let url;
|
|
132
|
+
try {
|
|
133
|
+
url = new URL(offered.replace(/^lnurlw:\/\//i, 'https://'));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return refuse('That is not a note.', 400);
|
|
137
|
+
}
|
|
138
|
+
if (url.host !== args.host)
|
|
139
|
+
return refuse('That note is not from this mint.', 400);
|
|
140
|
+
k1 = noteK1(url.toString());
|
|
141
|
+
}
|
|
142
|
+
if (!k1)
|
|
143
|
+
return refuse('That is not a note.', 400);
|
|
144
|
+
const note = store.noteById(hashK1(k1));
|
|
145
|
+
if (!note || note.state === 'burned')
|
|
146
|
+
return refuse('That note is spent or was never minted here.', 400);
|
|
147
|
+
if (note.state === 'pending')
|
|
148
|
+
return refuse('That note has a melt in flight.', 409);
|
|
149
|
+
if (note.amountMsat < priceMsat) {
|
|
150
|
+
return refuse(`That note is worth ${note.amountMsat} msat and a name costs ${priceMsat} msat.`, 402);
|
|
151
|
+
}
|
|
152
|
+
// The note is burned outright, with no change: the whole of it pays
|
|
153
|
+
// for the name, which is why the price is published and a wallet
|
|
154
|
+
// splits before it asks. Liabilities drop by exactly this much and
|
|
155
|
+
// the sats become revenue.
|
|
156
|
+
noteId = note.id;
|
|
157
|
+
paidMsat = note.amountMsat;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
store.buyZapName({ name: raw, pubkey, ...(noteId ? { noteId } : {}), paidMsat });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (err instanceof NotePendingError)
|
|
164
|
+
return refuse('That note has a melt in flight.', 409);
|
|
165
|
+
if (err instanceof NoteUnavailableError)
|
|
166
|
+
return refuse('That note is spent or was never minted here.', 400);
|
|
167
|
+
// The INSERT lost a race. Nothing was burned: the transaction took
|
|
168
|
+
// the name and the note together or not at all.
|
|
169
|
+
return refuse('That name is taken.', 409);
|
|
170
|
+
}
|
|
171
|
+
return { name: raw, pubkey, paidMsat };
|
|
172
|
+
};
|
package/dist/privacy.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// What the mint knows, in one place so the README, the landing page and
|
|
2
|
+
// the mint's own site cannot drift apart on it.
|
|
3
|
+
//
|
|
4
|
+
// The one property a LUD-25 mint cannot offer is blindness. A reader
|
|
5
|
+
// arriving from an ecash wallet will assume otherwise unless told, so
|
|
6
|
+
// this is written to be read before anyone trusts the mint with sats,
|
|
7
|
+
// not buried in a threat model.
|
|
8
|
+
export const MINT_KNOWS_HEADING = 'What the mint knows';
|
|
9
|
+
export const MINT_KNOWS = [
|
|
10
|
+
'This mint knows every note it has issued and what each is worth. It knows every rotate, split and merge, the links between them - which note became which, and when - and the network address the request came from. It knows the invoice a melt paid.',
|
|
11
|
+
'It does not know who holds a note between those operations. A note handed to someone else offline leaves no trace here until they rotate it, which is one reason a wallet rotates on receipt.',
|
|
12
|
+
'The wallet-side mitigations are weak, and worth naming as weak. A Tor or SOCKS proxy hides the address, not the links. Rotating at unpredictable times blurs the timing, not the graph. Nothing a holder does stops this mint seeing the chain of notes it struck.',
|
|
13
|
+
'The design was chosen anyway because it needs no new cryptography, any LUD-03 wallet can cash a note out, and verifying a note offline needs a signature and nothing else. The privacy story is trust the operator, and that is worth saying plainly.'
|
|
14
|
+
];
|
package/dist/server.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { MoneyerConfig } from './config.ts';
|
|
|
2
2
|
import { NoteStore } from './store.ts';
|
|
3
3
|
import { type NoteSigner } from './signing.ts';
|
|
4
4
|
import { type LightningBackend } from './backends/types.ts';
|
|
5
|
+
import { type NostrTransport, type ZapBridge } from './zap.ts';
|
|
6
|
+
import { type MintStats } from './stats.ts';
|
|
5
7
|
import { type WebAssets } from './web-assets.ts';
|
|
6
8
|
export type Moneyer = {
|
|
7
9
|
url: string;
|
|
@@ -10,7 +12,11 @@ export type Moneyer = {
|
|
|
10
12
|
store: NoteStore;
|
|
11
13
|
backend: LightningBackend;
|
|
12
14
|
signer: NoteSigner | null;
|
|
15
|
+
zap: ZapBridge | null;
|
|
13
16
|
reconcile: () => Promise<void>;
|
|
17
|
+
stats: () => Promise<MintStats>;
|
|
18
|
+
publishStats: () => Promise<void>;
|
|
19
|
+
publishAnnouncement: () => Promise<void>;
|
|
14
20
|
close: () => Promise<void>;
|
|
15
21
|
};
|
|
16
22
|
export type MoneyerDeps = {
|
|
@@ -19,6 +25,13 @@ export type MoneyerDeps = {
|
|
|
19
25
|
log?: (message: string) => void;
|
|
20
26
|
confirmDelaysMs?: number[];
|
|
21
27
|
webAssets?: WebAssets | null;
|
|
28
|
+
nostr?: NostrTransport;
|
|
29
|
+
zapPollMs?: number;
|
|
30
|
+
statsPublishMs?: number;
|
|
22
31
|
};
|
|
32
|
+
export declare const describeFee: (fee: {
|
|
33
|
+
baseFeeMsat: number;
|
|
34
|
+
feePpm: number;
|
|
35
|
+
}, roundedToSat: boolean) => string;
|
|
23
36
|
export declare const createMoneyer: (config: MoneyerConfig, deps?: MoneyerDeps) => Promise<Moneyer>;
|
|
24
37
|
export declare const sweepExpiredMintInvoices: (store: NoteStore, nowMs?: number) => number;
|