@forgesworn/moneyer 0.16.2 → 0.17.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 +76 -0
- package/README.md +69 -31
- package/TERMS-TEMPLATE.md +14 -0
- package/THREAT-MODEL.md +43 -11
- package/dist/admin.js +24 -14
- package/dist/cli.js +3 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/kernel.d.ts +10 -0
- package/dist/kernel.js +105 -0
- package/dist/names.d.ts +12 -2
- package/dist/names.js +50 -7
- package/dist/server.d.ts +2 -0
- package/dist/server.js +201 -139
- package/dist/spend.d.ts +85 -0
- package/dist/spend.js +454 -0
- package/dist/store.d.ts +6 -1
- package/dist/store.js +91 -11
- package/dist/zap.js +11 -6
- package/llms.txt +22 -10
- package/package.json +3 -2
- package/scripts/kernel-verifier.py +59 -0
package/dist/names.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
|
-
import {
|
|
2
|
+
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
3
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
3
4
|
import { verifyEvent } from 'nostr-tools/pure';
|
|
4
|
-
import { decodeCx1,
|
|
5
|
+
import { decodeCx1, noteK1 } from '@lnurlcash/kit';
|
|
5
6
|
import { NotePendingError, NoteStore, NoteUnavailableError } from "./store.js";
|
|
7
|
+
import { NOTE_PURPOSE_WALLET, decodeSpend, deriveNotePubkey } from "./spend.js";
|
|
6
8
|
// Self-service lightning addresses.
|
|
7
9
|
//
|
|
8
10
|
// A name here is a lightning address whose payouts are bearer notes
|
|
@@ -85,6 +87,35 @@ export const validateNip98 = (authorization, request) => {
|
|
|
85
87
|
}
|
|
86
88
|
return { pubkey: event.pubkey };
|
|
87
89
|
};
|
|
90
|
+
// LUD-25's address proof. A cx1 is public, so pointing a name at one, or
|
|
91
|
+
// away from one, has to be proven by the branch itself: a BIP-340
|
|
92
|
+
// signature by its index-0 key over sha256("LNURLcash:<action>:<domain>:
|
|
93
|
+
// <name>"). The domain is this mint's own, so a proof one mint has seen
|
|
94
|
+
// cannot be replayed at another; the action and name are in it so a
|
|
95
|
+
// register cannot be replayed as an unregister, or for another name.
|
|
96
|
+
export const addressProofVerifies = (args) => {
|
|
97
|
+
if (typeof args.sig !== 'string' || !/^[0-9a-f]{128}$/i.test(args.sig))
|
|
98
|
+
return false;
|
|
99
|
+
const branch = decodeCx1(args.cx1);
|
|
100
|
+
if (!branch)
|
|
101
|
+
return false;
|
|
102
|
+
let firstKey;
|
|
103
|
+
try {
|
|
104
|
+
firstKey = deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, NOTE_PURPOSE_WALLET, 0);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const signature = hexToBytes(args.sig.toLowerCase());
|
|
110
|
+
return args.domains.some(domain => {
|
|
111
|
+
try {
|
|
112
|
+
return schnorr.verify(signature, sha256(utf8ToBytes(`LNURLcash:${args.action}:${domain}:${args.name}`)), firstKey);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
};
|
|
88
119
|
const refuse = (reason, status) => ({ reason, status });
|
|
89
120
|
export const isRefusal = (result) => 'reason' in result;
|
|
90
121
|
// The whole registration, minus the transport. Burning the note and
|
|
@@ -92,7 +123,7 @@ export const isRefusal = (result) => 'reason' in result;
|
|
|
92
123
|
// exists without payment is worse than a note burned without a name,
|
|
93
124
|
// and the INSERT is what settles a race between two people asking for
|
|
94
125
|
// the same name at once.
|
|
95
|
-
export const registerName = (args) => {
|
|
126
|
+
export const registerName = async (args) => {
|
|
96
127
|
const { store, pubkey, priceMsat } = args;
|
|
97
128
|
const raw = typeof args.body.name === 'string' ? args.body.name.trim().toLowerCase() : '';
|
|
98
129
|
if (!NAME_RULE.test(raw)) {
|
|
@@ -113,10 +144,19 @@ export const registerName = (args) => {
|
|
|
113
144
|
return refuse('That is not a cx1.', 400);
|
|
114
145
|
cx1 = given;
|
|
115
146
|
}
|
|
147
|
+
const proven = (branch, action) => addressProofVerifies({ cx1: branch, action, name: raw, domains: args.domains, sig: args.body.sig });
|
|
148
|
+
const unproven = refuse('Setting or clearing a cx1 needs "sig": the branch\'s index-0 key signing sha256("LNURLcash:register:<domain>:<name>"), or :unregister: to clear.', 403);
|
|
116
149
|
const existing = store.zapName(raw);
|
|
117
150
|
if (existing) {
|
|
118
|
-
// Its owner may set or clear the branch; nobody else may touch it.
|
|
151
|
+
// Its owner may set or clear the branch; nobody else may touch it. The
|
|
152
|
+
// NIP-98 key owns the name, and the branch on file must agree to any
|
|
153
|
+
// change of where it pays: LUD-25 has an overwrite proven by the
|
|
154
|
+
// branch currently registered, not the one being switched to.
|
|
119
155
|
if (existing.pubkey === pubkey && cx1 !== undefined) {
|
|
156
|
+
if (cx1 === null && existing.cx1 && !proven(existing.cx1, 'unregister'))
|
|
157
|
+
return unproven;
|
|
158
|
+
if (cx1 !== null && !proven(existing.cx1 ?? cx1, 'register'))
|
|
159
|
+
return unproven;
|
|
120
160
|
store.setZapNameCx1(raw, cx1);
|
|
121
161
|
return { name: raw, pubkey, paidMsat: 0, cx1, updated: true };
|
|
122
162
|
}
|
|
@@ -126,6 +166,9 @@ export const registerName = (args) => {
|
|
|
126
166
|
// names, not a holder choosing where their own name pays.
|
|
127
167
|
if (priceMsat === undefined)
|
|
128
168
|
return refuse('This mint is not registering names.', 404);
|
|
169
|
+
// A fresh name pointed at a branch proves control of that branch.
|
|
170
|
+
if (typeof cx1 === 'string' && !proven(cx1, 'register'))
|
|
171
|
+
return unproven;
|
|
129
172
|
let paidMsat = 0;
|
|
130
173
|
let noteId;
|
|
131
174
|
if (priceMsat === 0) {
|
|
@@ -145,8 +188,8 @@ export const registerName = (args) => {
|
|
|
145
188
|
// plain-http service - correct for a wallet reaching a stranger, and
|
|
146
189
|
// wrong for a mint reading a note it minted itself.
|
|
147
190
|
let k1 = null;
|
|
148
|
-
if (
|
|
149
|
-
k1 = offered
|
|
191
|
+
if (decodeSpend(offered)) {
|
|
192
|
+
k1 = offered;
|
|
150
193
|
}
|
|
151
194
|
else {
|
|
152
195
|
let url;
|
|
@@ -162,7 +205,7 @@ export const registerName = (args) => {
|
|
|
162
205
|
}
|
|
163
206
|
if (!k1)
|
|
164
207
|
return refuse('That is not a note.', 400);
|
|
165
|
-
const note =
|
|
208
|
+
const note = await args.openNote(k1);
|
|
166
209
|
if (!note || note.state === 'burned')
|
|
167
210
|
return refuse('That note is spent or was never minted here.', 400);
|
|
168
211
|
if (note.state === 'pending')
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MoneyerConfig } from './config.ts';
|
|
2
2
|
import { NoteStore } from './store.ts';
|
|
3
3
|
import { type NoteSigner } from './signing.ts';
|
|
4
|
+
import { type ScriptVerifier } from './spend.ts';
|
|
4
5
|
import { type LightningBackend } from './backends/types.ts';
|
|
5
6
|
import { type NostrTransport, type ZapBridge } from './zap.ts';
|
|
6
7
|
import { type MintStats } from './stats.ts';
|
|
@@ -28,6 +29,7 @@ export type MoneyerDeps = {
|
|
|
28
29
|
nostr?: NostrTransport;
|
|
29
30
|
zapPollMs?: number;
|
|
30
31
|
statsPublishMs?: number;
|
|
32
|
+
scriptVerifier?: ScriptVerifier;
|
|
31
33
|
};
|
|
32
34
|
export { describeFee } from './fee-words.ts';
|
|
33
35
|
export declare const createMoneyer: (config: MoneyerConfig, deps?: MoneyerDeps) => Promise<Moneyer>;
|