@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/dist/names.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { sha256 } from '@noble/hashes/sha2.js';
2
- import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
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, hashK1, noteK1 } from '@lnurlcash/kit';
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 (/^[0-9a-f]{64}$/i.test(offered)) {
149
- k1 = offered.toLowerCase();
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 = store.noteById(hashK1(k1));
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>;