1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
// One-time genesis for a Concord community (CORD-02 §1): publishes the two
// owner-signed editions (community metadata + #general) and the owner's Join,
// then prints the join material to embed in the app.
//
// bun run tools/concord-genesis.js <owner-privkey-hex> <name> <relay> [relay...]
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { getPublicKey } from "nostr-tools/pure";
import { Relay, useWebSocketImplementation } from "nostr-tools/relay";
import WebSocket from "ws";
import {
communityId,
controlKey,
guestbookKey,
makeEdition,
wrapRumor,
msTags,
} from "../src/concord.js";
try { useWebSocketImplementation(WebSocket); } catch {}
let [skHex, name, ...relays] = process.argv.slice(2);
if (!skHex || !name || !relays.length) {
console.error("usage: bun run tools/concord-genesis.js <owner-privkey-hex> <name> <relay...>");
process.exit(1);
}
let ownerSk = hexToBytes(skHex);
let owner = getPublicKey(ownerSk);
let ownerSalt = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
let communityRoot = crypto.getRandomValues(new Uint8Array(32));
let cid = communityId(owner, ownerSalt);
let generalId = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
let control = controlKey(communityRoot, cid, 0);
let guestbook = guestbookKey(communityRoot, cid, 0);
let now = Date.now();
let metadata = makeEdition(
{ vsk: 0, eid: cid, version: 1, content: JSON.stringify({ name, relays }) },
now
);
let general = makeEdition(
{ vsk: 2, eid: generalId, version: 1, content: JSON.stringify({ name: "general", private: false }) },
now + 1
);
let { created_at, ms } = msTags(now + 2);
let join = { kind: 3306, content: "join", tags: [ms], created_at };
let events = [
await wrapRumor({ ...metadata, pubkey: owner }, ownerSk, control, { plaintext: true }),
await wrapRumor({ ...general, pubkey: owner }, ownerSk, control, { plaintext: true }),
await wrapRumor({ ...join, pubkey: owner }, ownerSk, guestbook),
];
for (let url of relays) {
try {
let r = await Relay.connect(url);
for (let e of events) await r.publish(e);
r.close();
console.error(`published ${events.length} events to ${url}`);
} catch (e) {
console.error(`FAILED ${url}: ${e.message ?? e}`);
}
}
// Join material (CORD-02 §8) — the bundle's membership subset. Contains the
// community_root: embedding it in the app makes every app user a member.
console.log(
JSON.stringify(
{
community_id: cid,
owner,
owner_salt: ownerSalt,
community_root: bytesToHex(communityRoot),
root_epoch: 0,
channels: [{ id: generalId, name: "general" }],
relays,
name,
},
null,
2
)
);
process.exit(0);
|