@forgesworn/moneyer 0.12.0 → 0.13.1
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 +21 -0
- package/README.md +25 -0
- package/THREAT-MODEL.md +8 -0
- package/dist/names.d.ts +3 -0
- package/dist/names.js +27 -6
- package/dist/server.js +12 -3
- package/dist/store.d.ts +13 -0
- package/dist/store.js +64 -14
- package/dist/zap.d.ts +1 -0
- package/dist/zap.js +35 -10
- package/llms.txt +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.1 - 2026-09-11
|
|
4
|
+
|
|
5
|
+
- A name's owner can set or clear its `cx1` while self-service registration
|
|
6
|
+
is closed. Closing registration stops new names; it no longer stops the
|
|
7
|
+
owner of an existing one, an operator name included, from choosing where
|
|
8
|
+
it pays. A request for a new name still gets the same 404.
|
|
9
|
+
|
|
10
|
+
## 0.13.0 - 2026-09-11
|
|
11
|
+
|
|
12
|
+
- **Names that pay to the holder's own keys.** `POST /names` takes a `cx1`,
|
|
13
|
+
a watch-only branch, and the key that owns a name can set or clear one
|
|
14
|
+
later. A zap to such a name is credited to the holder's next key on the
|
|
15
|
+
branch, and the gift wrap carries only where to look and the index:
|
|
16
|
+
`/w?p=<cp1>&amount=&sig=<cs1>&i=`, plus an `i` tag. The mint never holds
|
|
17
|
+
a secret for it.
|
|
18
|
+
- The index is taken when the zap settles, never when the invoice is made,
|
|
19
|
+
so an unpaid invoice leaves no gap in the holder's scan. A key that
|
|
20
|
+
already holds a note is skipped.
|
|
21
|
+
- Names gain two columns, `cx1` and `next_index`, added in place on first
|
|
22
|
+
start.
|
|
23
|
+
|
|
3
24
|
## 0.12.0 - 2026-09-11
|
|
4
25
|
|
|
5
26
|
- LUD-25's renamed parameters are accepted: `p` for the informational
|
package/README.md
CHANGED
|
@@ -542,6 +542,31 @@ A registered name resolves on both rails at once:
|
|
|
542
542
|
The discovery endpoint advertises `namePriceMsat` while registration is
|
|
543
543
|
open, so a wallet can offer the flow without asking.
|
|
544
544
|
|
|
545
|
+
### A name that pays to your own keys (LUD-25 Part 2)
|
|
546
|
+
|
|
547
|
+
Add `"cx1": "cx1..."` to the registration body, or send `{"name", "cx1"}`
|
|
548
|
+
again later from the key that owns the name, and payments to it stop being
|
|
549
|
+
custodial. A `cx1` is a watch-only branch: from it the mint can work out each
|
|
550
|
+
of the holder's note keys in turn, but never spend one.
|
|
551
|
+
|
|
552
|
+
When a zap to the name settles, the mint takes the next index on the branch
|
|
553
|
+
and credits the note to that key. It skips any key already holding a note,
|
|
554
|
+
and an invoice nobody pays takes no index at all, so a wallet scanning its
|
|
555
|
+
branch never meets a gap it did not make. The gift wrap still goes to the
|
|
556
|
+
owner's npub, but it carries no secret: a lookup URL,
|
|
557
|
+
`https://mint.example/w?p=<cp1>&amount=<msat>&sig=<cs1>&i=<index>`, plus an
|
|
558
|
+
`i` tag. The wallet derives the key at that index, checks the certificate,
|
|
559
|
+
and spends with its own `ck1`. A wallet that only knows note URLs sees no
|
|
560
|
+
`k1` and passes the wrap by, and the note waits at the mint for a scan of the
|
|
561
|
+
branch.
|
|
562
|
+
|
|
563
|
+
`"cx1": null` clears the branch and puts the name back on the custodial path.
|
|
564
|
+
This works while registration is closed too: closing it stops new names, not
|
|
565
|
+
the owner of one choosing where it pays, including a name the operator set up
|
|
566
|
+
in `MONEYER_ZAP_NAMES`.
|
|
567
|
+
Sending a new branch starts it at index 0; sending the same one again keeps
|
|
568
|
+
its place. Zap receipts are unchanged.
|
|
569
|
+
|
|
545
570
|
## Reaching the mint over Tor
|
|
546
571
|
|
|
547
572
|
Set `MONEYER_ONION_URL` to this mint's hidden service address and a request
|
package/THREAT-MODEL.md
CHANGED
|
@@ -90,6 +90,14 @@ only the buyer ever held, the preimage is bearer material for nothing, and
|
|
|
90
90
|
what `/verify` serves is an ordinary payment proof. Wallets should prefer
|
|
91
91
|
that path wherever a mint advertises `mintToHash`.
|
|
92
92
|
|
|
93
|
+
**What a `cx1` on a name gives away.** A name with a watch-only branch is
|
|
94
|
+
paid to the holder's keys, so the mint never holds a secret for those notes.
|
|
95
|
+
But the mint can work out every key on the branch, so it can link every
|
|
96
|
+
payment to that name, and see when each note is spent. That is no more than
|
|
97
|
+
a custodial name already reveals. A wallet should keep the branch for
|
|
98
|
+
receiving and rotate what arrives onto keys the mint cannot enumerate, which
|
|
99
|
+
is what notecase and lnurl-wallet do.
|
|
100
|
+
|
|
93
101
|
## Out of scope
|
|
94
102
|
|
|
95
103
|
- Compromise of the host or the funding source credentials: at that point
|
package/dist/names.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export type NameGranted = {
|
|
|
24
24
|
name: string;
|
|
25
25
|
pubkey: string;
|
|
26
26
|
paidMsat: number;
|
|
27
|
+
cx1: string | null;
|
|
28
|
+
updated: boolean;
|
|
27
29
|
};
|
|
28
30
|
export declare const isRefusal: (result: NameGranted | NameRefusal) => result is NameRefusal;
|
|
29
31
|
export declare const registerName: (args: {
|
|
@@ -32,6 +34,7 @@ export declare const registerName: (args: {
|
|
|
32
34
|
body: {
|
|
33
35
|
name?: unknown;
|
|
34
36
|
note?: unknown;
|
|
37
|
+
cx1?: unknown;
|
|
35
38
|
};
|
|
36
39
|
priceMsat: number | undefined;
|
|
37
40
|
reserved: string[];
|
package/dist/names.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
2
|
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
3
3
|
import { verifyEvent } from 'nostr-tools/pure';
|
|
4
|
-
import { hashK1, noteK1 } from 'lnurlcash-kit';
|
|
4
|
+
import { decodeCx1, hashK1, noteK1 } from 'lnurlcash-kit';
|
|
5
5
|
import { NotePendingError, NoteStore, NoteUnavailableError } from "./store.js";
|
|
6
6
|
// Self-service lightning addresses.
|
|
7
7
|
//
|
|
@@ -94,8 +94,6 @@ export const isRefusal = (result) => 'reason' in result;
|
|
|
94
94
|
// the same name at once.
|
|
95
95
|
export const registerName = (args) => {
|
|
96
96
|
const { store, pubkey, priceMsat } = args;
|
|
97
|
-
if (priceMsat === undefined)
|
|
98
|
-
return refuse('This mint is not registering names.', 404);
|
|
99
97
|
const raw = typeof args.body.name === 'string' ? args.body.name.trim().toLowerCase() : '';
|
|
100
98
|
if (!NAME_RULE.test(raw)) {
|
|
101
99
|
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);
|
|
@@ -103,8 +101,31 @@ export const registerName = (args) => {
|
|
|
103
101
|
if ([...ALWAYS_RESERVED, ...args.reserved.map(name => name.toLowerCase())].includes(raw)) {
|
|
104
102
|
return refuse('That name is reserved.', 403);
|
|
105
103
|
}
|
|
106
|
-
|
|
104
|
+
// LUD-25 Part 2: a watch-only branch, so payments to the name are minted
|
|
105
|
+
// to the holder's own keys. Absent leaves it as it is; null clears it.
|
|
106
|
+
let cx1;
|
|
107
|
+
if (args.body.cx1 === null) {
|
|
108
|
+
cx1 = null;
|
|
109
|
+
}
|
|
110
|
+
else if (args.body.cx1 !== undefined) {
|
|
111
|
+
const given = typeof args.body.cx1 === 'string' ? args.body.cx1.trim().toLowerCase() : '';
|
|
112
|
+
if (!decodeCx1(given))
|
|
113
|
+
return refuse('That is not a cx1.', 400);
|
|
114
|
+
cx1 = given;
|
|
115
|
+
}
|
|
116
|
+
const existing = store.zapName(raw);
|
|
117
|
+
if (existing) {
|
|
118
|
+
// Its owner may set or clear the branch; nobody else may touch it.
|
|
119
|
+
if (existing.pubkey === pubkey && cx1 !== undefined) {
|
|
120
|
+
store.setZapNameCx1(raw, cx1);
|
|
121
|
+
return { name: raw, pubkey, paidMsat: 0, cx1, updated: true };
|
|
122
|
+
}
|
|
107
123
|
return refuse('That name is taken.', 409);
|
|
124
|
+
}
|
|
125
|
+
// Checked after the owner's update above: closing registration stops new
|
|
126
|
+
// names, not a holder choosing where their own name pays.
|
|
127
|
+
if (priceMsat === undefined)
|
|
128
|
+
return refuse('This mint is not registering names.', 404);
|
|
108
129
|
let paidMsat = 0;
|
|
109
130
|
let noteId;
|
|
110
131
|
if (priceMsat === 0) {
|
|
@@ -157,7 +178,7 @@ export const registerName = (args) => {
|
|
|
157
178
|
paidMsat = note.amountMsat;
|
|
158
179
|
}
|
|
159
180
|
try {
|
|
160
|
-
store.buyZapName({ name: raw, pubkey, ...(noteId ? { noteId } : {}), paidMsat });
|
|
181
|
+
store.buyZapName({ name: raw, pubkey, ...(noteId ? { noteId } : {}), paidMsat, cx1: cx1 ?? null });
|
|
161
182
|
}
|
|
162
183
|
catch (err) {
|
|
163
184
|
if (err instanceof NotePendingError)
|
|
@@ -168,5 +189,5 @@ export const registerName = (args) => {
|
|
|
168
189
|
// the name and the note together or not at all.
|
|
169
190
|
return refuse('That name is taken.', 409);
|
|
170
191
|
}
|
|
171
|
-
return { name: raw, pubkey, paidMsat };
|
|
192
|
+
return { name: raw, pubkey, paidMsat, cx1: cx1 ?? null, updated: false };
|
|
172
193
|
};
|
package/dist/server.js
CHANGED
|
@@ -167,6 +167,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
167
167
|
mintFeeLine,
|
|
168
168
|
feeInWords,
|
|
169
169
|
verify: config.verify,
|
|
170
|
+
certify: (noteId, amountMsat) => certify({ id: noteId, cp1: true }, amountMsat),
|
|
170
171
|
// configFromEnv guarantees this when zap is set; a caller
|
|
171
172
|
// building the config by hand gets the same rule.
|
|
172
173
|
origin: config.publicOrigin ?? (() => {
|
|
@@ -448,7 +449,10 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
448
449
|
// a POST because it creates something, and it is authenticated by
|
|
449
450
|
// NIP-98 rather than by anything this mint has to store.
|
|
450
451
|
if (requestUrl.pathname === '/names' && req.method === 'POST') {
|
|
451
|
-
|
|
452
|
+
// Closed registration still lets a name's owner set where it pays;
|
|
453
|
+
// registerName refuses a new name there. Without zaps there are no
|
|
454
|
+
// names at all.
|
|
455
|
+
if (!zap)
|
|
452
456
|
return fail('This mint is not registering names.', 404);
|
|
453
457
|
const body = await readBody(req);
|
|
454
458
|
if (body === null)
|
|
@@ -480,14 +484,19 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
480
484
|
});
|
|
481
485
|
if (isRefusal(result))
|
|
482
486
|
return fail(result.reason, result.status);
|
|
483
|
-
log(
|
|
487
|
+
log(result.updated
|
|
488
|
+
? `name ${result.name} ${result.cx1 ? 'now pays to a branch' : 'pays out custodially again'}`
|
|
489
|
+
: `name ${result.name} registered to ${result.pubkey.slice(0, 8)} for ${result.paidMsat} msat`);
|
|
484
490
|
return send({
|
|
485
491
|
status: 'OK',
|
|
486
492
|
name: result.name,
|
|
487
493
|
pubkey: result.pubkey,
|
|
488
494
|
address: `${result.name}@${host}`,
|
|
489
495
|
priceMsat: config.namePriceMsat,
|
|
490
|
-
paidMsat: result.paidMsat
|
|
496
|
+
paidMsat: result.paidMsat,
|
|
497
|
+
// With a cx1 the name pays to the holder's own keys; without, as a
|
|
498
|
+
// note gift-wrapped to them.
|
|
499
|
+
cx1: result.cx1
|
|
491
500
|
});
|
|
492
501
|
}
|
|
493
502
|
// Every LNURL endpoint below is a GET. The method matters: /w/cb
|
package/dist/store.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type ZapNameRow = {
|
|
|
53
53
|
createdAt: number;
|
|
54
54
|
paidMsat: number;
|
|
55
55
|
source: 'env' | 'self';
|
|
56
|
+
cx1: string | null;
|
|
57
|
+
nextIndex: number;
|
|
56
58
|
};
|
|
57
59
|
export type NoteListRow = NoteRow & {
|
|
58
60
|
createdAt: number;
|
|
@@ -126,16 +128,27 @@ export declare class NoteStore {
|
|
|
126
128
|
unsettledZapInvoices(): ZapInvoiceRow[];
|
|
127
129
|
deleteUnsettledZapInvoice(paymentHash: string): void;
|
|
128
130
|
settleZapInvoice(paymentHash: string, noteId: string, wrapJson: string, receiptJson: string | null): boolean;
|
|
131
|
+
settleZapInvoiceToKey(paymentHash: string, keyAt: (index: number) => string | null, build: (noteId: string, index: number) => {
|
|
132
|
+
wrapJson: string;
|
|
133
|
+
receiptJson: string | null;
|
|
134
|
+
}): {
|
|
135
|
+
noteId: string;
|
|
136
|
+
index: number;
|
|
137
|
+
} | null;
|
|
129
138
|
unpublishedZaps(): ZapInvoiceRow[];
|
|
130
139
|
markZapPublished(paymentHash: string): void;
|
|
140
|
+
private static readonly NAME_COLUMNS;
|
|
141
|
+
private nameRow;
|
|
131
142
|
zapName(name: string): ZapNameRow | null;
|
|
132
143
|
zapNames(): ZapNameRow[];
|
|
144
|
+
setZapNameCx1(name: string, cx1: string | null): void;
|
|
133
145
|
putOperatorZapName(name: string, pubkey: string): void;
|
|
134
146
|
buyZapName(args: {
|
|
135
147
|
name: string;
|
|
136
148
|
pubkey: string;
|
|
137
149
|
noteId?: string;
|
|
138
150
|
paidMsat: number;
|
|
151
|
+
cx1?: string | null;
|
|
139
152
|
}): void;
|
|
140
153
|
removeZapName(name: string): boolean;
|
|
141
154
|
zapNameCountFor(pubkey: string): number;
|
package/dist/store.js
CHANGED
|
@@ -117,6 +117,12 @@ export class NoteStore {
|
|
|
117
117
|
// Two invoices may not name the same note. SQLite counts NULLs as
|
|
118
118
|
// distinct, so every unbound invoice still fits.
|
|
119
119
|
this.db.exec('CREATE UNIQUE INDEX IF NOT EXISTS mint_invoices_output_id ON mint_invoices (output_id)');
|
|
120
|
+
const nameColumns = this.db.prepare('PRAGMA table_info(zap_names)').all().map(column => column.name);
|
|
121
|
+
if (!nameColumns.includes('cx1'))
|
|
122
|
+
this.db.exec('ALTER TABLE zap_names ADD COLUMN cx1 TEXT');
|
|
123
|
+
if (!nameColumns.includes('next_index')) {
|
|
124
|
+
this.db.exec('ALTER TABLE zap_names ADD COLUMN next_index INTEGER NOT NULL DEFAULT 0');
|
|
125
|
+
}
|
|
120
126
|
this.db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);');
|
|
121
127
|
// The moment this mint stopped publishing preimages for notes nobody
|
|
122
128
|
// named. Written once, the first time a build carrying this code opens
|
|
@@ -440,6 +446,39 @@ export class NoteStore {
|
|
|
440
446
|
});
|
|
441
447
|
}
|
|
442
448
|
// Settled zaps whose wrap has not yet reached a relay.
|
|
449
|
+
// A zap to a name with a cx1, minted to the next unused key on its
|
|
450
|
+
// branch. The index is taken here, at settlement, never when the invoice
|
|
451
|
+
// is made, so an invoice nobody pays leaves no gap in the holder's scan.
|
|
452
|
+
// A key that already names a note is skipped, as LUD-25 requires.
|
|
453
|
+
// `keyAt` answers null for an index that yields no usable key.
|
|
454
|
+
settleZapInvoiceToKey(paymentHash, keyAt, build) {
|
|
455
|
+
return this.tx(() => {
|
|
456
|
+
const row = this.zapInvoiceByHash(paymentHash);
|
|
457
|
+
if (!row || row.settled)
|
|
458
|
+
return null;
|
|
459
|
+
const name = this.zapName(row.name);
|
|
460
|
+
if (!name?.cx1)
|
|
461
|
+
return null;
|
|
462
|
+
let index = name.nextIndex;
|
|
463
|
+
let noteId = null;
|
|
464
|
+
for (let tries = 0; tries < 1000 && noteId === null; tries++) {
|
|
465
|
+
const candidate = keyAt(index);
|
|
466
|
+
if (candidate !== null && !this.outputIdInUse(candidate))
|
|
467
|
+
noteId = candidate;
|
|
468
|
+
else
|
|
469
|
+
index += 1;
|
|
470
|
+
}
|
|
471
|
+
if (noteId === null)
|
|
472
|
+
throw new Error(`no free key on the branch of ${row.name}`);
|
|
473
|
+
this.insertNote(noteId, row.netMsat);
|
|
474
|
+
this.db.prepare('UPDATE zap_names SET next_index = ? WHERE name = ?').run(index + 1, row.name);
|
|
475
|
+
const { wrapJson, receiptJson } = build(noteId, index);
|
|
476
|
+
this.db
|
|
477
|
+
.prepare('UPDATE zap_invoices SET settled = 1, note_id = ?, wrap_json = ?, receipt_json = ?, settled_at = ? WHERE payment_hash = ?')
|
|
478
|
+
.run(noteId, wrapJson, receiptJson, Date.now(), paymentHash);
|
|
479
|
+
return { noteId, index };
|
|
480
|
+
});
|
|
481
|
+
}
|
|
443
482
|
unpublishedZaps() {
|
|
444
483
|
const rows = this.db
|
|
445
484
|
.prepare(`SELECT ${NoteStore.ZAP_COLUMNS} FROM zap_invoices WHERE settled = 1 AND published_at IS NULL`)
|
|
@@ -458,25 +497,36 @@ export class NoteStore {
|
|
|
458
497
|
// One table for both kinds: names the operator set in the environment
|
|
459
498
|
// and names people registered themselves. The lookup path that serves a
|
|
460
499
|
// zap does not care which is which, so there is one of it.
|
|
500
|
+
static NAME_COLUMNS = 'name, pubkey, created_at, paid_msat, source, cx1, next_index';
|
|
501
|
+
nameRow(row) {
|
|
502
|
+
return {
|
|
503
|
+
name: row.name,
|
|
504
|
+
pubkey: row.pubkey,
|
|
505
|
+
createdAt: row.created_at,
|
|
506
|
+
paidMsat: row.paid_msat,
|
|
507
|
+
source: row.source,
|
|
508
|
+
cx1: row.cx1 ?? null,
|
|
509
|
+
nextIndex: row.next_index
|
|
510
|
+
};
|
|
511
|
+
}
|
|
461
512
|
zapName(name) {
|
|
462
513
|
const row = this.db
|
|
463
|
-
.prepare(
|
|
514
|
+
.prepare(`SELECT ${NoteStore.NAME_COLUMNS} FROM zap_names WHERE name = ?`)
|
|
464
515
|
.get(name.toLowerCase());
|
|
465
|
-
|
|
466
|
-
return null;
|
|
467
|
-
return { name: row.name, pubkey: row.pubkey, createdAt: row.created_at, paidMsat: row.paid_msat, source: row.source };
|
|
516
|
+
return row ? this.nameRow(row) : null;
|
|
468
517
|
}
|
|
469
518
|
zapNames() {
|
|
470
519
|
const rows = this.db
|
|
471
|
-
.prepare(
|
|
520
|
+
.prepare(`SELECT ${NoteStore.NAME_COLUMNS} FROM zap_names ORDER BY name`)
|
|
472
521
|
.all();
|
|
473
|
-
return rows.map(row => (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
522
|
+
return rows.map(row => this.nameRow(row));
|
|
523
|
+
}
|
|
524
|
+
// A new branch starts at index 0; the same branch sent again keeps its
|
|
525
|
+
// place, so re-registering cannot walk the holder's keys backwards.
|
|
526
|
+
setZapNameCx1(name, cx1) {
|
|
527
|
+
this.db
|
|
528
|
+
.prepare('UPDATE zap_names SET next_index = CASE WHEN cx1 IS ? THEN next_index ELSE 0 END, cx1 = ? WHERE name = ?')
|
|
529
|
+
.run(cx1, cx1, name.toLowerCase());
|
|
480
530
|
}
|
|
481
531
|
// The operator's own names, re-applied at every startup. Idempotent, and
|
|
482
532
|
// the environment wins: it is the operator's mint, and a name they put
|
|
@@ -499,8 +549,8 @@ export class NoteStore {
|
|
|
499
549
|
this.setNoteState(args.noteId, 'burned');
|
|
500
550
|
}
|
|
501
551
|
this.db
|
|
502
|
-
.prepare("INSERT INTO zap_names (name, pubkey, created_at, paid_msat, source) VALUES (?, ?, ?, ?, 'self')")
|
|
503
|
-
.run(args.name.toLowerCase(), args.pubkey, Date.now(), args.paidMsat);
|
|
552
|
+
.prepare("INSERT INTO zap_names (name, pubkey, created_at, paid_msat, source, cx1) VALUES (?, ?, ?, ?, 'self', ?)")
|
|
553
|
+
.run(args.name.toLowerCase(), args.pubkey, Date.now(), args.paidMsat, args.cx1 ?? null);
|
|
504
554
|
});
|
|
505
555
|
}
|
|
506
556
|
removeZapName(name) {
|
package/dist/zap.d.ts
CHANGED
package/dist/zap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
2
|
-
import { hashK1 } from 'lnurlcash-kit';
|
|
2
|
+
import { decodeCx1, deriveNotePubkey, encodeCp1, hashK1 } from 'lnurlcash-kit';
|
|
3
3
|
import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
|
|
4
4
|
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
|
5
5
|
import { SimplePool } from 'nostr-tools/pool';
|
|
@@ -171,8 +171,7 @@ export const createZapBridge = (deps) => {
|
|
|
171
171
|
store.recordZapInvoice({ paymentHash, name: lowered, recipient, pr, grossMsat: amountMsat, netMsat: net, zapRequest });
|
|
172
172
|
return { pr, ...(deps.verify ? { verify: `${origin}/verify/${paymentHash}` } : {}) };
|
|
173
173
|
};
|
|
174
|
-
|
|
175
|
-
const buildWrap = (row, k1) => {
|
|
174
|
+
const wrapTags = (row) => {
|
|
176
175
|
const tags = [
|
|
177
176
|
['p', row.recipient],
|
|
178
177
|
['amount', String(row.netMsat)],
|
|
@@ -192,15 +191,20 @@ export const createZapBridge = (deps) => {
|
|
|
192
191
|
// older one simply does not see this.
|
|
193
192
|
tags.push(['description', row.zapRequest]);
|
|
194
193
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
tags,
|
|
200
|
-
content: `${origin}/w?k1=${k1}&amount=${row.netMsat}`
|
|
201
|
-
};
|
|
194
|
+
return tags;
|
|
195
|
+
};
|
|
196
|
+
const wrap = (row, tags, content) => {
|
|
197
|
+
const rumor = { kind: NOTE_KIND, pubkey, created_at: Math.floor(now() / 1000), tags, content };
|
|
202
198
|
return wrapEvent(rumor, secret, row.recipient);
|
|
203
199
|
};
|
|
200
|
+
// The note as the recipient's wallet will paste it, wrapped to them.
|
|
201
|
+
const buildWrap = (row, k1) => wrap(row, wrapTags(row), `${origin}/w?k1=${k1}&amount=${row.netMsat}`);
|
|
202
|
+
// A zap to a name with a cx1: the note already belongs to the holder's
|
|
203
|
+
// key, so the wrap carries no secret at all, only where to look (`p`),
|
|
204
|
+
// the certificate, and the index `i` the holder derives the key at. A
|
|
205
|
+
// reader that knows only note URLs sees no k1 and passes it by, and the
|
|
206
|
+
// note waits at the mint for a scan to find it.
|
|
207
|
+
const buildKeyWrap = (row, noteId, index) => wrap(row, [...wrapTags(row), ['i', String(index)]], `${origin}/w?p=${encodeCp1(hexToBytes(noteId))}&amount=${row.netMsat}&sig=${deps.certify(noteId, row.netMsat)}&i=${index}`);
|
|
204
208
|
// NIP-57 receipt, without the preimage tag: it is optional there, and
|
|
205
209
|
// here it would only invite someone to mistake it for the note.
|
|
206
210
|
const buildReceipt = (row) => {
|
|
@@ -222,6 +226,27 @@ export const createZapBridge = (deps) => {
|
|
|
222
226
|
}
|
|
223
227
|
if (!paid)
|
|
224
228
|
continue;
|
|
229
|
+
const branch = decodeCx1(store.zapName(row.name)?.cx1 ?? '');
|
|
230
|
+
if (branch) {
|
|
231
|
+
const receipt = buildReceipt(row);
|
|
232
|
+
const keyAt = (index) => {
|
|
233
|
+
try {
|
|
234
|
+
return bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, index));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const landed = store.settleZapInvoiceToKey(row.paymentHash, keyAt, (noteId, index) => ({
|
|
241
|
+
wrapJson: JSON.stringify(buildKeyWrap(row, noteId, index)),
|
|
242
|
+
receiptJson: receipt ? JSON.stringify(receipt) : null
|
|
243
|
+
}));
|
|
244
|
+
if (landed) {
|
|
245
|
+
minted += 1;
|
|
246
|
+
log(`zap: ${row.name} received ${row.netMsat} msat; note minted to key ${landed.index}, wrap parked`);
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
225
250
|
const k1 = bytesToHex(randomBytes(32));
|
|
226
251
|
const noteId = hashK1(k1);
|
|
227
252
|
const wrap = buildWrap(row, k1);
|
package/llms.txt
CHANGED
|
@@ -91,3 +91,9 @@ NoteStore - SQLite store; notes by sha256(k1), never secrets
|
|
|
91
91
|
/verify/{payment_hash} LUD-21 verify (mint invoices AND melt payments)
|
|
92
92
|
/w LUD-03 informational GET by k1 (secret or ck1) or p (hash or cp1, older h); a cp1 note carries its cs1 as sig
|
|
93
93
|
/w/cb melt / rotate / split / merge; inputs secrets or ck1, outputs p1/p2 hashes or cp1 (or older h/h2); cp1 outputs certified as cs1
|
|
94
|
+
POST /names claim a name (NIP-98; the signer owns it); body {name, note?, cx1?}.
|
|
95
|
+
The owner may resend {name, cx1} to set a branch, or cx1: null to clear it
|
|
96
|
+
/z/cb/{name} zap invoice for a name. With a cx1 the settled note is credited to the
|
|
97
|
+
holder's next key, and the kind 2525 gift wrap carries no secret:
|
|
98
|
+
/w?p=<cp1>&amount=&sig=<cs1>&i=<index> plus an `i` tag. The index is
|
|
99
|
+
taken at settlement, never at the invoice, and occupied keys are skipped
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
|
|
5
5
|
"author": "TheCryptoDonkey",
|
|
6
6
|
"license": "MIT",
|