@toon-protocol/relay 1.3.3 → 2.0.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/README.md +51 -93
- package/dist/chunk-745ADETR.js +1027 -0
- package/dist/chunk-745ADETR.js.map +1 -0
- package/dist/cli.js +38 -206
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +107 -977
- package/dist/index.js +6 -447
- package/dist/index.js.map +1 -1
- package/package.json +4 -9
- package/dist/chunk-ZKWFGHZ7.js +0 -2029
- package/dist/chunk-ZKWFGHZ7.js.map +0 -1
package/dist/chunk-ZKWFGHZ7.js
DELETED
|
@@ -1,2029 +0,0 @@
|
|
|
1
|
-
// src/types.ts
|
|
2
|
-
var DEFAULT_RELAY_CONFIG = {
|
|
3
|
-
port: 7e3,
|
|
4
|
-
host: "0.0.0.0",
|
|
5
|
-
maxConnections: 100,
|
|
6
|
-
maxSubscriptionsPerConnection: 20,
|
|
7
|
-
maxFiltersPerSubscription: 10,
|
|
8
|
-
databasePath: ":memory:"
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// src/filters/matchFilter.ts
|
|
12
|
-
function matchFilter(event, filter) {
|
|
13
|
-
if (Object.keys(filter).length === 0) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
if (filter.ids !== void 0 && filter.ids.length > 0) {
|
|
17
|
-
const matches = filter.ids.some((id) => event.id.startsWith(id));
|
|
18
|
-
if (!matches) return false;
|
|
19
|
-
}
|
|
20
|
-
if (filter.authors !== void 0 && filter.authors.length > 0) {
|
|
21
|
-
const matches = filter.authors.some(
|
|
22
|
-
(author) => event.pubkey.startsWith(author)
|
|
23
|
-
);
|
|
24
|
-
if (!matches) return false;
|
|
25
|
-
}
|
|
26
|
-
if (filter.kinds !== void 0 && filter.kinds.length > 0) {
|
|
27
|
-
if (!filter.kinds.includes(event.kind)) return false;
|
|
28
|
-
}
|
|
29
|
-
if (filter.since !== void 0) {
|
|
30
|
-
if (event.created_at < filter.since) return false;
|
|
31
|
-
}
|
|
32
|
-
if (filter.until !== void 0) {
|
|
33
|
-
if (event.created_at > filter.until) return false;
|
|
34
|
-
}
|
|
35
|
-
for (const key of Object.keys(filter)) {
|
|
36
|
-
if (key.startsWith("#") && key.length === 2) {
|
|
37
|
-
const tagName = key.slice(1);
|
|
38
|
-
const filterValues = filter[key];
|
|
39
|
-
if (filterValues !== void 0 && filterValues.length > 0) {
|
|
40
|
-
const eventTagValues = event.tags.filter((tag) => tag[0] === tagName).map((tag) => tag[1]);
|
|
41
|
-
const hasMatch = filterValues.some((v) => eventTagValues.includes(v));
|
|
42
|
-
if (!hasMatch) return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// src/storage/InMemoryEventStore.ts
|
|
50
|
-
var InMemoryEventStore = class {
|
|
51
|
-
events = /* @__PURE__ */ new Map();
|
|
52
|
-
store(event) {
|
|
53
|
-
this.events.set(event.id, event);
|
|
54
|
-
}
|
|
55
|
-
get(id) {
|
|
56
|
-
return this.events.get(id);
|
|
57
|
-
}
|
|
58
|
-
query(filters) {
|
|
59
|
-
const allEvents = Array.from(this.events.values());
|
|
60
|
-
if (filters.length === 0) {
|
|
61
|
-
return allEvents.sort((a, b) => b.created_at - a.created_at);
|
|
62
|
-
}
|
|
63
|
-
const matchingEvents = [];
|
|
64
|
-
for (const event of allEvents) {
|
|
65
|
-
for (const filter of filters) {
|
|
66
|
-
if (matchFilter(event, filter)) {
|
|
67
|
-
matchingEvents.push(event);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
matchingEvents.sort((a, b) => b.created_at - a.created_at);
|
|
73
|
-
const limitFilter = filters.find((f) => f.limit !== void 0);
|
|
74
|
-
if (limitFilter?.limit !== void 0) {
|
|
75
|
-
return matchingEvents.slice(0, limitFilter.limit);
|
|
76
|
-
}
|
|
77
|
-
return matchingEvents;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Close the storage backend (no-op for in-memory store).
|
|
81
|
-
*/
|
|
82
|
-
close() {
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// src/storage/SqliteEventStore.ts
|
|
87
|
-
import Database from "better-sqlite3";
|
|
88
|
-
var SCHEMA_SQL = `
|
|
89
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
90
|
-
id TEXT PRIMARY KEY,
|
|
91
|
-
pubkey TEXT NOT NULL,
|
|
92
|
-
kind INTEGER NOT NULL,
|
|
93
|
-
content TEXT NOT NULL,
|
|
94
|
-
tags TEXT NOT NULL,
|
|
95
|
-
created_at INTEGER NOT NULL,
|
|
96
|
-
sig TEXT NOT NULL,
|
|
97
|
-
received_at INTEGER NOT NULL
|
|
98
|
-
)
|
|
99
|
-
`;
|
|
100
|
-
var INDEX_SQL = [
|
|
101
|
-
"CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey)",
|
|
102
|
-
"CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind)",
|
|
103
|
-
"CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at)",
|
|
104
|
-
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind)"
|
|
105
|
-
];
|
|
106
|
-
function initializeSchema(db) {
|
|
107
|
-
db.exec(SCHEMA_SQL);
|
|
108
|
-
for (const indexSql of INDEX_SQL) {
|
|
109
|
-
db.exec(indexSql);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
var RelayError = class extends Error {
|
|
113
|
-
constructor(message, code) {
|
|
114
|
-
super(message);
|
|
115
|
-
this.code = code;
|
|
116
|
-
this.name = "RelayError";
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
function isReplaceableKind(kind) {
|
|
120
|
-
return kind >= 1e4 && kind <= 19999 && !(kind >= 10032 && kind <= 10099);
|
|
121
|
-
}
|
|
122
|
-
function isParameterizedReplaceableKind(kind) {
|
|
123
|
-
return kind >= 3e4 && kind <= 39999 || kind >= 10032 && kind <= 10099;
|
|
124
|
-
}
|
|
125
|
-
function getDTagValue(tags) {
|
|
126
|
-
const dTag = tags.find((tag) => tag[0] === "d");
|
|
127
|
-
return dTag?.[1] ?? "";
|
|
128
|
-
}
|
|
129
|
-
var SqliteEventStore = class {
|
|
130
|
-
db;
|
|
131
|
-
insertStmt;
|
|
132
|
-
getStmt;
|
|
133
|
-
deleteByPubkeyKindStmt;
|
|
134
|
-
deleteByPubkeyKindDTagStmt;
|
|
135
|
-
getByPubkeyKindStmt;
|
|
136
|
-
getByPubkeyKindDTagStmt;
|
|
137
|
-
/**
|
|
138
|
-
* Create a new SqliteEventStore.
|
|
139
|
-
* @param dbPath - Path to the database file. Use ':memory:' for in-memory database.
|
|
140
|
-
*/
|
|
141
|
-
constructor(dbPath = ":memory:") {
|
|
142
|
-
try {
|
|
143
|
-
this.db = new Database(dbPath);
|
|
144
|
-
initializeSchema(this.db);
|
|
145
|
-
this.insertStmt = this.db.prepare(`
|
|
146
|
-
INSERT OR REPLACE INTO events (id, pubkey, kind, content, tags, created_at, sig, received_at)
|
|
147
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
148
|
-
`);
|
|
149
|
-
this.getStmt = this.db.prepare("SELECT * FROM events WHERE id = ?");
|
|
150
|
-
this.deleteByPubkeyKindStmt = this.db.prepare(
|
|
151
|
-
"DELETE FROM events WHERE pubkey = ? AND kind = ?"
|
|
152
|
-
);
|
|
153
|
-
this.deleteByPubkeyKindDTagStmt = this.db.prepare(
|
|
154
|
-
"DELETE FROM events WHERE pubkey = ? AND kind = ? AND json_extract(tags, '$') LIKE ?"
|
|
155
|
-
);
|
|
156
|
-
this.getByPubkeyKindStmt = this.db.prepare(
|
|
157
|
-
"SELECT id, created_at FROM events WHERE pubkey = ? AND kind = ?"
|
|
158
|
-
);
|
|
159
|
-
this.getByPubkeyKindDTagStmt = this.db.prepare(
|
|
160
|
-
"SELECT id, created_at FROM events WHERE pubkey = ? AND kind = ? AND tags LIKE ?"
|
|
161
|
-
);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
throw new RelayError(
|
|
164
|
-
`Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`,
|
|
165
|
-
"STORAGE_ERROR"
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Store an event in the database.
|
|
171
|
-
* Handles replaceable and parameterized replaceable events according to NIP-01.
|
|
172
|
-
*/
|
|
173
|
-
store(event) {
|
|
174
|
-
try {
|
|
175
|
-
const tagsJson = JSON.stringify(event.tags);
|
|
176
|
-
const receivedAt = Math.floor(Date.now() / 1e3);
|
|
177
|
-
if (isReplaceableKind(event.kind)) {
|
|
178
|
-
this.storeReplaceableEvent(event, tagsJson, receivedAt);
|
|
179
|
-
} else if (isParameterizedReplaceableKind(event.kind)) {
|
|
180
|
-
this.storeParameterizedReplaceableEvent(event, tagsJson, receivedAt);
|
|
181
|
-
} else {
|
|
182
|
-
const insertOrIgnore = this.db.prepare(`
|
|
183
|
-
INSERT OR IGNORE INTO events (id, pubkey, kind, content, tags, created_at, sig, received_at)
|
|
184
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
185
|
-
`);
|
|
186
|
-
insertOrIgnore.run(
|
|
187
|
-
event.id,
|
|
188
|
-
event.pubkey,
|
|
189
|
-
event.kind,
|
|
190
|
-
event.content,
|
|
191
|
-
tagsJson,
|
|
192
|
-
event.created_at,
|
|
193
|
-
event.sig,
|
|
194
|
-
receivedAt
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
} catch (error) {
|
|
198
|
-
if (error instanceof RelayError) {
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
201
|
-
throw new RelayError(
|
|
202
|
-
`Failed to store event: ${error instanceof Error ? error.message : String(error)}`,
|
|
203
|
-
"STORAGE_ERROR"
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Store a replaceable event (kinds 10000-19999).
|
|
209
|
-
* Only keeps the latest event per pubkey+kind.
|
|
210
|
-
*/
|
|
211
|
-
storeReplaceableEvent(event, tagsJson, receivedAt) {
|
|
212
|
-
const existing = this.getByPubkeyKindStmt.get(event.pubkey, event.kind);
|
|
213
|
-
if (existing) {
|
|
214
|
-
if (event.created_at > existing.created_at || event.created_at === existing.created_at && event.id < existing.id) {
|
|
215
|
-
const transaction = this.db.transaction(() => {
|
|
216
|
-
this.deleteByPubkeyKindStmt.run(event.pubkey, event.kind);
|
|
217
|
-
this.insertStmt.run(
|
|
218
|
-
event.id,
|
|
219
|
-
event.pubkey,
|
|
220
|
-
event.kind,
|
|
221
|
-
event.content,
|
|
222
|
-
tagsJson,
|
|
223
|
-
event.created_at,
|
|
224
|
-
event.sig,
|
|
225
|
-
receivedAt
|
|
226
|
-
);
|
|
227
|
-
});
|
|
228
|
-
transaction();
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
this.insertStmt.run(
|
|
232
|
-
event.id,
|
|
233
|
-
event.pubkey,
|
|
234
|
-
event.kind,
|
|
235
|
-
event.content,
|
|
236
|
-
tagsJson,
|
|
237
|
-
event.created_at,
|
|
238
|
-
event.sig,
|
|
239
|
-
receivedAt
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Store a parameterized replaceable event (kinds 30000-39999).
|
|
245
|
-
* Only keeps the latest event per pubkey+kind+d-tag.
|
|
246
|
-
*/
|
|
247
|
-
storeParameterizedReplaceableEvent(event, tagsJson, receivedAt) {
|
|
248
|
-
const dTagValue = getDTagValue(event.tags);
|
|
249
|
-
let existing;
|
|
250
|
-
if (dTagValue === "") {
|
|
251
|
-
const candidates = this.db.prepare(
|
|
252
|
-
"SELECT id, created_at, tags FROM events WHERE pubkey = ? AND kind = ?"
|
|
253
|
-
).all(event.pubkey, event.kind);
|
|
254
|
-
for (const candidate of candidates) {
|
|
255
|
-
const candidateTags = JSON.parse(candidate.tags);
|
|
256
|
-
const candidateDTagValue = getDTagValue(candidateTags);
|
|
257
|
-
if (candidateDTagValue === "") {
|
|
258
|
-
existing = { id: candidate.id, created_at: candidate.created_at };
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
const dTagPattern = `%["d","${dTagValue}"%`;
|
|
264
|
-
existing = this.getByPubkeyKindDTagStmt.get(
|
|
265
|
-
event.pubkey,
|
|
266
|
-
event.kind,
|
|
267
|
-
dTagPattern
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
if (existing) {
|
|
271
|
-
if (event.created_at > existing.created_at || event.created_at === existing.created_at && event.id < existing.id) {
|
|
272
|
-
const transaction = this.db.transaction(() => {
|
|
273
|
-
this.db.prepare("DELETE FROM events WHERE id = ?").run(existing.id);
|
|
274
|
-
this.insertStmt.run(
|
|
275
|
-
event.id,
|
|
276
|
-
event.pubkey,
|
|
277
|
-
event.kind,
|
|
278
|
-
event.content,
|
|
279
|
-
tagsJson,
|
|
280
|
-
event.created_at,
|
|
281
|
-
event.sig,
|
|
282
|
-
receivedAt
|
|
283
|
-
);
|
|
284
|
-
});
|
|
285
|
-
transaction();
|
|
286
|
-
}
|
|
287
|
-
} else {
|
|
288
|
-
this.insertStmt.run(
|
|
289
|
-
event.id,
|
|
290
|
-
event.pubkey,
|
|
291
|
-
event.kind,
|
|
292
|
-
event.content,
|
|
293
|
-
tagsJson,
|
|
294
|
-
event.created_at,
|
|
295
|
-
event.sig,
|
|
296
|
-
receivedAt
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Retrieve an event by its ID.
|
|
302
|
-
*/
|
|
303
|
-
get(id) {
|
|
304
|
-
try {
|
|
305
|
-
const row = this.getStmt.get(id);
|
|
306
|
-
if (!row) {
|
|
307
|
-
return void 0;
|
|
308
|
-
}
|
|
309
|
-
return {
|
|
310
|
-
id: row.id,
|
|
311
|
-
pubkey: row.pubkey,
|
|
312
|
-
kind: row.kind,
|
|
313
|
-
content: row.content,
|
|
314
|
-
tags: JSON.parse(row.tags),
|
|
315
|
-
created_at: row.created_at,
|
|
316
|
-
sig: row.sig
|
|
317
|
-
};
|
|
318
|
-
} catch (error) {
|
|
319
|
-
throw new RelayError(
|
|
320
|
-
`Failed to get event: ${error instanceof Error ? error.message : String(error)}`,
|
|
321
|
-
"STORAGE_ERROR"
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Query events matching any of the provided filters.
|
|
327
|
-
*/
|
|
328
|
-
query(filters) {
|
|
329
|
-
try {
|
|
330
|
-
const { sql, params } = this.buildQuerySql(filters);
|
|
331
|
-
const stmt = this.db.prepare(sql);
|
|
332
|
-
const rows = stmt.all(...params);
|
|
333
|
-
return rows.map((row) => ({
|
|
334
|
-
id: row.id,
|
|
335
|
-
pubkey: row.pubkey,
|
|
336
|
-
kind: row.kind,
|
|
337
|
-
content: row.content,
|
|
338
|
-
tags: JSON.parse(row.tags),
|
|
339
|
-
created_at: row.created_at,
|
|
340
|
-
sig: row.sig
|
|
341
|
-
}));
|
|
342
|
-
} catch (error) {
|
|
343
|
-
throw new RelayError(
|
|
344
|
-
`Failed to query events: ${error instanceof Error ? error.message : String(error)}`,
|
|
345
|
-
"STORAGE_ERROR"
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Build SQL query from filters.
|
|
351
|
-
*/
|
|
352
|
-
buildQuerySql(filters) {
|
|
353
|
-
if (filters.length === 0) {
|
|
354
|
-
return {
|
|
355
|
-
sql: "SELECT * FROM events ORDER BY created_at DESC",
|
|
356
|
-
params: []
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
const conditions = [];
|
|
360
|
-
const params = [];
|
|
361
|
-
for (const filter of filters) {
|
|
362
|
-
const filterConditions = [];
|
|
363
|
-
if (filter.ids?.length) {
|
|
364
|
-
const idConditions = filter.ids.map(() => "id LIKE ?");
|
|
365
|
-
filterConditions.push(`(${idConditions.join(" OR ")})`);
|
|
366
|
-
params.push(...filter.ids.map((id) => `${id}%`));
|
|
367
|
-
}
|
|
368
|
-
if (filter.authors?.length) {
|
|
369
|
-
const authorConditions = filter.authors.map(() => "pubkey LIKE ?");
|
|
370
|
-
filterConditions.push(`(${authorConditions.join(" OR ")})`);
|
|
371
|
-
params.push(...filter.authors.map((a) => `${a}%`));
|
|
372
|
-
}
|
|
373
|
-
if (filter.kinds?.length) {
|
|
374
|
-
filterConditions.push(
|
|
375
|
-
`kind IN (${filter.kinds.map(() => "?").join(", ")})`
|
|
376
|
-
);
|
|
377
|
-
params.push(...filter.kinds);
|
|
378
|
-
}
|
|
379
|
-
if (filter.since !== void 0) {
|
|
380
|
-
filterConditions.push("created_at >= ?");
|
|
381
|
-
params.push(filter.since);
|
|
382
|
-
}
|
|
383
|
-
if (filter.until !== void 0) {
|
|
384
|
-
filterConditions.push("created_at <= ?");
|
|
385
|
-
params.push(filter.until);
|
|
386
|
-
}
|
|
387
|
-
for (const [key, values] of Object.entries(filter)) {
|
|
388
|
-
if (key.startsWith("#") && Array.isArray(values) && values.length > 0) {
|
|
389
|
-
const tagName = key.slice(1);
|
|
390
|
-
const tagConditions = values.map(() => `tags LIKE ?`);
|
|
391
|
-
filterConditions.push(`(${tagConditions.join(" OR ")})`);
|
|
392
|
-
params.push(...values.map((v) => `%["${tagName}","${v}"%`));
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (filterConditions.length > 0) {
|
|
396
|
-
conditions.push(`(${filterConditions.join(" AND ")})`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
let sql = "SELECT * FROM events";
|
|
400
|
-
if (conditions.length > 0) {
|
|
401
|
-
sql += ` WHERE ${conditions.join(" OR ")}`;
|
|
402
|
-
}
|
|
403
|
-
sql += " ORDER BY created_at DESC";
|
|
404
|
-
const limitFilter = filters.find((f) => f.limit !== void 0);
|
|
405
|
-
if (limitFilter?.limit !== void 0) {
|
|
406
|
-
sql += " LIMIT ?";
|
|
407
|
-
params.push(limitFilter.limit);
|
|
408
|
-
}
|
|
409
|
-
return { sql, params };
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Close the database connection.
|
|
413
|
-
*/
|
|
414
|
-
close() {
|
|
415
|
-
this.db.close();
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// src/toon/index.ts
|
|
420
|
-
import {
|
|
421
|
-
encodeEventToToon,
|
|
422
|
-
encodeEventToToonString,
|
|
423
|
-
ToonEncodeError
|
|
424
|
-
} from "@toon-protocol/core";
|
|
425
|
-
import { decodeEventFromToon, ToonDecodeError } from "@toon-protocol/core";
|
|
426
|
-
|
|
427
|
-
// src/websocket/ConnectionHandler.ts
|
|
428
|
-
var ConnectionHandler = class {
|
|
429
|
-
constructor(ws, eventStore, config = {}) {
|
|
430
|
-
this.ws = ws;
|
|
431
|
-
this.eventStore = eventStore;
|
|
432
|
-
this.config = { ...DEFAULT_RELAY_CONFIG, ...config };
|
|
433
|
-
}
|
|
434
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
435
|
-
config;
|
|
436
|
-
/**
|
|
437
|
-
* Handle an incoming message from the WebSocket.
|
|
438
|
-
*/
|
|
439
|
-
handleMessage(data) {
|
|
440
|
-
console.log(`[ConnectionHandler] Received message:`, data.slice(0, 150));
|
|
441
|
-
let message;
|
|
442
|
-
try {
|
|
443
|
-
const parsed = JSON.parse(data);
|
|
444
|
-
if (!Array.isArray(parsed)) {
|
|
445
|
-
this.sendNotice("error: invalid message format, expected JSON array");
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
message = parsed;
|
|
449
|
-
} catch {
|
|
450
|
-
this.sendNotice("error: invalid JSON");
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
const messageType = message[0];
|
|
454
|
-
console.log(`[ConnectionHandler] Message type: ${messageType}`);
|
|
455
|
-
if (messageType === "REQ") {
|
|
456
|
-
const subscriptionId = message[1];
|
|
457
|
-
const filters = message.slice(2);
|
|
458
|
-
this.handleReq(subscriptionId, filters);
|
|
459
|
-
} else if (messageType === "EVENT") {
|
|
460
|
-
const event = message[1];
|
|
461
|
-
this.handleEvent(event);
|
|
462
|
-
} else if (messageType === "CLOSE") {
|
|
463
|
-
const subscriptionId = message[1];
|
|
464
|
-
this.handleClose(subscriptionId);
|
|
465
|
-
} else {
|
|
466
|
-
this.sendNotice(`error: unknown message type: ${messageType}`);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Handle a REQ message to create/update a subscription.
|
|
471
|
-
*/
|
|
472
|
-
handleReq(subscriptionId, filters) {
|
|
473
|
-
if (typeof subscriptionId !== "string" || subscriptionId.length === 0) {
|
|
474
|
-
this.sendNotice("error: invalid subscription id");
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (!this.subscriptions.has(subscriptionId)) {
|
|
478
|
-
if (this.subscriptions.size >= this.config.maxSubscriptionsPerConnection) {
|
|
479
|
-
this.sendNotice("error: too many subscriptions");
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (filters.length > this.config.maxFiltersPerSubscription) {
|
|
484
|
-
this.sendNotice("error: too many filters");
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
this.subscriptions.set(subscriptionId, {
|
|
488
|
-
id: subscriptionId,
|
|
489
|
-
filters
|
|
490
|
-
});
|
|
491
|
-
console.log(
|
|
492
|
-
`[ConnectionHandler] REQ: ${subscriptionId}, filters:`,
|
|
493
|
-
JSON.stringify(filters).slice(0, 100)
|
|
494
|
-
);
|
|
495
|
-
const events = this.eventStore.query(filters);
|
|
496
|
-
console.log(
|
|
497
|
-
`[ConnectionHandler] Query returned ${events.length} events for ${subscriptionId}`
|
|
498
|
-
);
|
|
499
|
-
for (const event of events) {
|
|
500
|
-
console.log(
|
|
501
|
-
`[ConnectionHandler] Sending event ${event.id.slice(0, 16)}... to ${subscriptionId}`
|
|
502
|
-
);
|
|
503
|
-
this.sendEvent(subscriptionId, event);
|
|
504
|
-
}
|
|
505
|
-
console.log(`[ConnectionHandler] Sending EOSE for ${subscriptionId}`);
|
|
506
|
-
this.sendEose(subscriptionId);
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Handle an EVENT message from a WebSocket client.
|
|
510
|
-
*
|
|
511
|
-
* Rejects all external writes — the relay is ILP-gated (pay to write).
|
|
512
|
-
* Events are only stored through the ILP packet handler which calls
|
|
513
|
-
* eventStore.store() directly and then broadcastEvent() to notify subscribers.
|
|
514
|
-
*/
|
|
515
|
-
handleEvent(event) {
|
|
516
|
-
this.sendOk(event.id, false, "restricted: writes require ILP payment");
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Handle a CLOSE message to terminate a subscription.
|
|
520
|
-
*/
|
|
521
|
-
handleClose(subscriptionId) {
|
|
522
|
-
this.subscriptions.delete(subscriptionId);
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Push a new event to all matching subscriptions on this connection.
|
|
526
|
-
* Used when events are stored outside the WebSocket flow (e.g., via ILP).
|
|
527
|
-
*/
|
|
528
|
-
notifyNewEvent(event) {
|
|
529
|
-
for (const sub of this.subscriptions.values()) {
|
|
530
|
-
const matches = sub.filters.some((f) => matchFilter(event, f));
|
|
531
|
-
if (matches) {
|
|
532
|
-
this.sendEvent(sub.id, event);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Clean up all subscriptions for this connection.
|
|
538
|
-
*/
|
|
539
|
-
cleanup() {
|
|
540
|
-
this.subscriptions.clear();
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Get the number of active subscriptions.
|
|
544
|
-
*/
|
|
545
|
-
getSubscriptionCount() {
|
|
546
|
-
return this.subscriptions.size;
|
|
547
|
-
}
|
|
548
|
-
sendEvent(subscriptionId, event) {
|
|
549
|
-
this.send(["EVENT", subscriptionId, encodeEventToToonString(event)]);
|
|
550
|
-
}
|
|
551
|
-
sendEose(subscriptionId) {
|
|
552
|
-
this.send(["EOSE", subscriptionId]);
|
|
553
|
-
}
|
|
554
|
-
sendOk(eventId, success, message) {
|
|
555
|
-
this.send(["OK", eventId, success, message]);
|
|
556
|
-
}
|
|
557
|
-
sendNotice(message) {
|
|
558
|
-
this.send(["NOTICE", message]);
|
|
559
|
-
}
|
|
560
|
-
send(message) {
|
|
561
|
-
if (this.ws.readyState === 1) {
|
|
562
|
-
this.ws.send(JSON.stringify(message));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
// src/websocket/NostrRelayServer.ts
|
|
568
|
-
import { WebSocketServer } from "ws";
|
|
569
|
-
var NostrRelayServer = class {
|
|
570
|
-
constructor(config = {}, eventStore) {
|
|
571
|
-
this.eventStore = eventStore;
|
|
572
|
-
this.config = { ...DEFAULT_RELAY_CONFIG, ...config };
|
|
573
|
-
}
|
|
574
|
-
wss = null;
|
|
575
|
-
handlers = /* @__PURE__ */ new Map();
|
|
576
|
-
config;
|
|
577
|
-
/**
|
|
578
|
-
* Start the WebSocket server.
|
|
579
|
-
*/
|
|
580
|
-
async start() {
|
|
581
|
-
return new Promise((resolve, reject) => {
|
|
582
|
-
try {
|
|
583
|
-
this.wss = new WebSocketServer({
|
|
584
|
-
port: this.config.port,
|
|
585
|
-
host: this.config.host
|
|
586
|
-
});
|
|
587
|
-
this.wss.on("connection", (ws) => {
|
|
588
|
-
this.handleConnection(ws);
|
|
589
|
-
});
|
|
590
|
-
this.wss.on("error", (error) => {
|
|
591
|
-
console.error("[NostrRelayServer] Server error:", error.message);
|
|
592
|
-
});
|
|
593
|
-
this.wss.on("listening", () => {
|
|
594
|
-
const address = this.wss?.address();
|
|
595
|
-
if (address && typeof address === "object") {
|
|
596
|
-
console.log(`[NostrRelayServer] Listening on port ${address.port}`);
|
|
597
|
-
}
|
|
598
|
-
resolve();
|
|
599
|
-
});
|
|
600
|
-
} catch (error) {
|
|
601
|
-
reject(error);
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Stop the WebSocket server and close all connections.
|
|
607
|
-
*/
|
|
608
|
-
async stop() {
|
|
609
|
-
return new Promise((resolve) => {
|
|
610
|
-
if (!this.wss) {
|
|
611
|
-
resolve();
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
for (const [ws, handler] of this.handlers) {
|
|
615
|
-
handler.cleanup();
|
|
616
|
-
ws.close();
|
|
617
|
-
}
|
|
618
|
-
this.handlers.clear();
|
|
619
|
-
this.wss.close(() => {
|
|
620
|
-
this.wss = null;
|
|
621
|
-
resolve();
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Get the port the server is listening on.
|
|
627
|
-
* Returns 0 if the server is not started.
|
|
628
|
-
*/
|
|
629
|
-
getPort() {
|
|
630
|
-
if (!this.wss) return 0;
|
|
631
|
-
const address = this.wss.address();
|
|
632
|
-
if (address && typeof address === "object") {
|
|
633
|
-
return address.port;
|
|
634
|
-
}
|
|
635
|
-
return 0;
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Get the number of connected clients.
|
|
639
|
-
*/
|
|
640
|
-
getClientCount() {
|
|
641
|
-
return this.handlers.size;
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Broadcast an event to all connected clients with matching subscriptions.
|
|
645
|
-
* Call this after storing an event outside the WebSocket flow (e.g., via ILP)
|
|
646
|
-
* so that discovery subscribers are notified.
|
|
647
|
-
*/
|
|
648
|
-
broadcastEvent(event) {
|
|
649
|
-
for (const handler of this.handlers.values()) {
|
|
650
|
-
handler.notifyNewEvent(event);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
handleConnection(ws) {
|
|
654
|
-
if (this.handlers.size >= this.config.maxConnections) {
|
|
655
|
-
ws.close(1013, "max connections reached");
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
console.log("[NostrRelayServer] Client connected");
|
|
659
|
-
const handler = new ConnectionHandler(ws, this.eventStore, this.config);
|
|
660
|
-
this.handlers.set(ws, handler);
|
|
661
|
-
ws.on("message", (data) => {
|
|
662
|
-
const message = typeof data === "string" ? data : data.toString();
|
|
663
|
-
handler.handleMessage(message);
|
|
664
|
-
});
|
|
665
|
-
ws.on("close", () => {
|
|
666
|
-
console.log("[NostrRelayServer] Client disconnected");
|
|
667
|
-
handler.cleanup();
|
|
668
|
-
this.handlers.delete(ws);
|
|
669
|
-
});
|
|
670
|
-
ws.on("error", (error) => {
|
|
671
|
-
console.error("[NostrRelayServer] Client error:", error.message);
|
|
672
|
-
handler.cleanup();
|
|
673
|
-
this.handlers.delete(ws);
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
// src/subscriber/RelaySubscriber.ts
|
|
679
|
-
import { SimplePool } from "nostr-tools/pool";
|
|
680
|
-
import { verifyEvent } from "nostr-tools/pure";
|
|
681
|
-
var RelaySubscriber = class {
|
|
682
|
-
config;
|
|
683
|
-
eventStore;
|
|
684
|
-
pool;
|
|
685
|
-
started = false;
|
|
686
|
-
/**
|
|
687
|
-
* @param config - Subscriber configuration
|
|
688
|
-
* @param eventStore - Storage backend to write events into
|
|
689
|
-
* @param pool - Optional SimplePool instance (creates new one if not provided)
|
|
690
|
-
*/
|
|
691
|
-
constructor(config, eventStore, pool) {
|
|
692
|
-
this.config = config;
|
|
693
|
-
this.eventStore = eventStore;
|
|
694
|
-
this.pool = pool ?? new SimplePool();
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Start subscribing to the configured upstream relays.
|
|
698
|
-
*
|
|
699
|
-
* @returns Handle with unsubscribe() to stop the subscription
|
|
700
|
-
* @throws Error if already started
|
|
701
|
-
*/
|
|
702
|
-
start() {
|
|
703
|
-
if (this.started) {
|
|
704
|
-
throw new Error("RelaySubscriber already started");
|
|
705
|
-
}
|
|
706
|
-
this.started = true;
|
|
707
|
-
const shouldVerify = this.config.verifySignatures !== false;
|
|
708
|
-
let isUnsubscribed = false;
|
|
709
|
-
const subCloser = this.pool.subscribeMany(
|
|
710
|
-
this.config.relayUrls,
|
|
711
|
-
this.config.filter,
|
|
712
|
-
{
|
|
713
|
-
onevent: (event) => {
|
|
714
|
-
if (isUnsubscribed) return;
|
|
715
|
-
if (shouldVerify && !verifyEvent(event)) {
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
try {
|
|
719
|
-
this.eventStore.store(event);
|
|
720
|
-
} catch (error) {
|
|
721
|
-
console.warn(
|
|
722
|
-
"[RelaySubscriber] Failed to store event:",
|
|
723
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
);
|
|
729
|
-
return {
|
|
730
|
-
unsubscribe: () => {
|
|
731
|
-
if (!isUnsubscribed) {
|
|
732
|
-
isUnsubscribed = true;
|
|
733
|
-
subCloser.close();
|
|
734
|
-
this.started = false;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
// src/launcher/handlers/event-storage-handler.ts
|
|
742
|
-
function createEventStorageHandler(config) {
|
|
743
|
-
const { eventStore } = config;
|
|
744
|
-
return async (ctx) => {
|
|
745
|
-
const event = ctx.decode();
|
|
746
|
-
eventStore.store(event);
|
|
747
|
-
return ctx.accept({ eventId: event.id, storedAt: Date.now() });
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// src/launcher/handlers/x402-pricing.ts
|
|
752
|
-
function calculateX402Price(config, toonLength) {
|
|
753
|
-
const clampedBuffer = Math.max(0, Math.min(200, config.routingBufferPercent));
|
|
754
|
-
const basePrice = config.basePricePerByte * BigInt(toonLength);
|
|
755
|
-
const buffer = basePrice * BigInt(clampedBuffer) / 100n;
|
|
756
|
-
return basePrice + buffer;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// src/launcher/handlers/x402-types.ts
|
|
760
|
-
var EIP_3009_TYPES = {
|
|
761
|
-
TransferWithAuthorization: [
|
|
762
|
-
{ name: "from", type: "address" },
|
|
763
|
-
{ name: "to", type: "address" },
|
|
764
|
-
{ name: "value", type: "uint256" },
|
|
765
|
-
{ name: "validAfter", type: "uint256" },
|
|
766
|
-
{ name: "validBefore", type: "uint256" },
|
|
767
|
-
{ name: "nonce", type: "bytes32" }
|
|
768
|
-
]
|
|
769
|
-
};
|
|
770
|
-
var USDC_EIP712_DOMAIN = {
|
|
771
|
-
name: "USD Coin",
|
|
772
|
-
version: "2"
|
|
773
|
-
};
|
|
774
|
-
var USDC_ABI = [
|
|
775
|
-
{
|
|
776
|
-
name: "balanceOf",
|
|
777
|
-
type: "function",
|
|
778
|
-
stateMutability: "view",
|
|
779
|
-
inputs: [{ name: "account", type: "address" }],
|
|
780
|
-
outputs: [{ name: "", type: "uint256" }]
|
|
781
|
-
},
|
|
782
|
-
{
|
|
783
|
-
name: "authorizationState",
|
|
784
|
-
type: "function",
|
|
785
|
-
stateMutability: "view",
|
|
786
|
-
inputs: [
|
|
787
|
-
{ name: "authorizer", type: "address" },
|
|
788
|
-
{ name: "nonce", type: "bytes32" }
|
|
789
|
-
],
|
|
790
|
-
outputs: [{ name: "", type: "bool" }]
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
name: "transferWithAuthorization",
|
|
794
|
-
type: "function",
|
|
795
|
-
stateMutability: "nonpayable",
|
|
796
|
-
inputs: [
|
|
797
|
-
{ name: "from", type: "address" },
|
|
798
|
-
{ name: "to", type: "address" },
|
|
799
|
-
{ name: "value", type: "uint256" },
|
|
800
|
-
{ name: "validAfter", type: "uint256" },
|
|
801
|
-
{ name: "validBefore", type: "uint256" },
|
|
802
|
-
{ name: "nonce", type: "bytes32" },
|
|
803
|
-
{ name: "v", type: "uint8" },
|
|
804
|
-
{ name: "r", type: "bytes32" },
|
|
805
|
-
{ name: "s", type: "bytes32" }
|
|
806
|
-
],
|
|
807
|
-
outputs: []
|
|
808
|
-
}
|
|
809
|
-
];
|
|
810
|
-
|
|
811
|
-
// src/launcher/handlers/x402-preflight.ts
|
|
812
|
-
import { verifyTypedData } from "viem";
|
|
813
|
-
import { shallowParseToon } from "@toon-protocol/core/toon";
|
|
814
|
-
async function runPreflight(authorization, toonData, destination, config) {
|
|
815
|
-
const checksPerformed = [];
|
|
816
|
-
checksPerformed.push("eip3009-signature");
|
|
817
|
-
try {
|
|
818
|
-
const domain = {
|
|
819
|
-
...USDC_EIP712_DOMAIN,
|
|
820
|
-
chainId: config.chainConfig.chainId,
|
|
821
|
-
verifyingContract: config.chainConfig.usdcAddress
|
|
822
|
-
};
|
|
823
|
-
const valid = await verifyTypedData({
|
|
824
|
-
address: authorization.from,
|
|
825
|
-
domain,
|
|
826
|
-
types: EIP_3009_TYPES,
|
|
827
|
-
primaryType: "TransferWithAuthorization",
|
|
828
|
-
message: {
|
|
829
|
-
from: authorization.from,
|
|
830
|
-
to: authorization.to,
|
|
831
|
-
value: authorization.value,
|
|
832
|
-
validAfter: BigInt(authorization.validAfter),
|
|
833
|
-
validBefore: BigInt(authorization.validBefore),
|
|
834
|
-
nonce: authorization.nonce
|
|
835
|
-
},
|
|
836
|
-
signature: encodeSignature(authorization)
|
|
837
|
-
});
|
|
838
|
-
if (!valid) {
|
|
839
|
-
return {
|
|
840
|
-
passed: false,
|
|
841
|
-
failedCheck: "eip3009-signature",
|
|
842
|
-
checksPerformed
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
} catch {
|
|
846
|
-
return { passed: false, failedCheck: "eip3009-signature", checksPerformed };
|
|
847
|
-
}
|
|
848
|
-
checksPerformed.push("usdc-balance");
|
|
849
|
-
if (config.publicClient) {
|
|
850
|
-
try {
|
|
851
|
-
const balance = await config.publicClient.readContract({
|
|
852
|
-
address: config.chainConfig.usdcAddress,
|
|
853
|
-
abi: USDC_ABI,
|
|
854
|
-
functionName: "balanceOf",
|
|
855
|
-
args: [authorization.from]
|
|
856
|
-
});
|
|
857
|
-
if (balance < authorization.value) {
|
|
858
|
-
return { passed: false, failedCheck: "usdc-balance", checksPerformed };
|
|
859
|
-
}
|
|
860
|
-
} catch {
|
|
861
|
-
return { passed: false, failedCheck: "usdc-balance", checksPerformed };
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
checksPerformed.push("nonce-freshness");
|
|
865
|
-
if (config.publicClient) {
|
|
866
|
-
try {
|
|
867
|
-
const used = await config.publicClient.readContract({
|
|
868
|
-
address: config.chainConfig.usdcAddress,
|
|
869
|
-
abi: USDC_ABI,
|
|
870
|
-
functionName: "authorizationState",
|
|
871
|
-
args: [
|
|
872
|
-
authorization.from,
|
|
873
|
-
authorization.nonce
|
|
874
|
-
]
|
|
875
|
-
});
|
|
876
|
-
if (used) {
|
|
877
|
-
return {
|
|
878
|
-
passed: false,
|
|
879
|
-
failedCheck: "nonce-freshness",
|
|
880
|
-
checksPerformed
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
} catch {
|
|
884
|
-
return { passed: false, failedCheck: "nonce-freshness", checksPerformed };
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
checksPerformed.push("toon-shallow-parse");
|
|
888
|
-
let toonMeta;
|
|
889
|
-
try {
|
|
890
|
-
const toonBytes = Buffer.from(toonData, "base64");
|
|
891
|
-
toonMeta = shallowParseToon(toonBytes);
|
|
892
|
-
} catch {
|
|
893
|
-
return {
|
|
894
|
-
passed: false,
|
|
895
|
-
failedCheck: "toon-shallow-parse",
|
|
896
|
-
checksPerformed
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
checksPerformed.push("schnorr-signature");
|
|
900
|
-
if (!config.devMode && config.schnorrVerify) {
|
|
901
|
-
try {
|
|
902
|
-
const valid = await config.schnorrVerify(toonMeta);
|
|
903
|
-
if (!valid) {
|
|
904
|
-
return {
|
|
905
|
-
passed: false,
|
|
906
|
-
failedCheck: "schnorr-signature",
|
|
907
|
-
checksPerformed
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
} catch {
|
|
911
|
-
return {
|
|
912
|
-
passed: false,
|
|
913
|
-
failedCheck: "schnorr-signature",
|
|
914
|
-
checksPerformed
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
checksPerformed.push("destination-reachability");
|
|
919
|
-
if (config.eventStore) {
|
|
920
|
-
try {
|
|
921
|
-
const events = config.eventStore.query([{ kinds: [10032] }]);
|
|
922
|
-
if (events.length === 0) {
|
|
923
|
-
return {
|
|
924
|
-
passed: false,
|
|
925
|
-
failedCheck: "destination-reachability",
|
|
926
|
-
checksPerformed
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
} catch {
|
|
930
|
-
return {
|
|
931
|
-
passed: false,
|
|
932
|
-
failedCheck: "destination-reachability",
|
|
933
|
-
checksPerformed
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
return { passed: true, checksPerformed };
|
|
938
|
-
}
|
|
939
|
-
function encodeSignature(auth) {
|
|
940
|
-
const r = auth.r.startsWith("0x") ? auth.r.slice(2) : auth.r;
|
|
941
|
-
const s = auth.s.startsWith("0x") ? auth.s.slice(2) : auth.s;
|
|
942
|
-
const v = auth.v.toString(16).padStart(2, "0");
|
|
943
|
-
return `0x${r}${s}${v}`;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// src/launcher/handlers/x402-settlement.ts
|
|
947
|
-
async function settleEip3009(authorization, config) {
|
|
948
|
-
try {
|
|
949
|
-
const hash = await config.walletClient.writeContract({
|
|
950
|
-
address: config.chainConfig.usdcAddress,
|
|
951
|
-
abi: USDC_ABI,
|
|
952
|
-
functionName: "transferWithAuthorization",
|
|
953
|
-
args: [
|
|
954
|
-
authorization.from,
|
|
955
|
-
authorization.to,
|
|
956
|
-
authorization.value,
|
|
957
|
-
BigInt(authorization.validAfter),
|
|
958
|
-
BigInt(authorization.validBefore),
|
|
959
|
-
authorization.nonce,
|
|
960
|
-
authorization.v,
|
|
961
|
-
authorization.r,
|
|
962
|
-
authorization.s
|
|
963
|
-
],
|
|
964
|
-
chain: null,
|
|
965
|
-
// Use the wallet client's configured chain
|
|
966
|
-
account: config.walletClient.account ?? null
|
|
967
|
-
});
|
|
968
|
-
if (config.publicClient) {
|
|
969
|
-
const receipt = await config.publicClient.waitForTransactionReceipt({
|
|
970
|
-
hash
|
|
971
|
-
});
|
|
972
|
-
if (receipt.status === "reverted") {
|
|
973
|
-
return {
|
|
974
|
-
success: false,
|
|
975
|
-
error: "Transaction reverted on-chain"
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return {
|
|
980
|
-
success: true,
|
|
981
|
-
txHash: hash
|
|
982
|
-
};
|
|
983
|
-
} catch (error) {
|
|
984
|
-
const message = error instanceof Error ? error.message : "Unknown settlement error";
|
|
985
|
-
return {
|
|
986
|
-
success: false,
|
|
987
|
-
error: message
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// src/launcher/handlers/x402-publish-handler.ts
|
|
993
|
-
import { buildIlpPrepare, encodeEventToToon as encodeEventToToon2 } from "@toon-protocol/core";
|
|
994
|
-
function createX402Handler(config) {
|
|
995
|
-
const encoder = config.toonEncoder ?? encodeEventToToon2;
|
|
996
|
-
if (config.x402Enabled && (!config.facilitatorAddress || !/^0x[0-9a-fA-F]{40}$/.test(config.facilitatorAddress))) {
|
|
997
|
-
throw new Error(
|
|
998
|
-
"x402 enabled but facilitatorAddress is not a valid EVM address"
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
return {
|
|
1002
|
-
async handlePublish(c) {
|
|
1003
|
-
if (!config.x402Enabled) {
|
|
1004
|
-
return c.json({ error: "x402 not enabled" }, 404);
|
|
1005
|
-
}
|
|
1006
|
-
let body;
|
|
1007
|
-
try {
|
|
1008
|
-
body = await c.req.json();
|
|
1009
|
-
} catch {
|
|
1010
|
-
return c.json({ error: "Invalid request body" }, 400);
|
|
1011
|
-
}
|
|
1012
|
-
if (!body.event || !body.destination) {
|
|
1013
|
-
return c.json(
|
|
1014
|
-
{ error: "Missing required fields: event, destination" },
|
|
1015
|
-
400
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
if (typeof body.destination !== "string" || !body.destination.startsWith("g.")) {
|
|
1019
|
-
return c.json(
|
|
1020
|
-
{ error: "Invalid destination: must be a global ILP address (g.*)" },
|
|
1021
|
-
400
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
let toonBytes;
|
|
1025
|
-
try {
|
|
1026
|
-
toonBytes = encoder(body.event);
|
|
1027
|
-
} catch {
|
|
1028
|
-
return c.json({ error: "Failed to TOON-encode event" }, 400);
|
|
1029
|
-
}
|
|
1030
|
-
const toonBase64 = Buffer.from(toonBytes).toString("base64");
|
|
1031
|
-
const paymentHeader = c.req.header("X-PAYMENT");
|
|
1032
|
-
if (!paymentHeader) {
|
|
1033
|
-
const price = calculateX402Price(
|
|
1034
|
-
{
|
|
1035
|
-
basePricePerByte: config.basePricePerByte,
|
|
1036
|
-
routingBufferPercent: config.routingBufferPercent
|
|
1037
|
-
},
|
|
1038
|
-
toonBytes.length
|
|
1039
|
-
);
|
|
1040
|
-
const pricing = {
|
|
1041
|
-
amount: String(price),
|
|
1042
|
-
facilitatorAddress: config.facilitatorAddress,
|
|
1043
|
-
paymentNetwork: "eip-3009",
|
|
1044
|
-
chainId: config.chainConfig.chainId,
|
|
1045
|
-
usdcAddress: config.chainConfig.usdcAddress
|
|
1046
|
-
};
|
|
1047
|
-
return c.json(pricing, 402);
|
|
1048
|
-
}
|
|
1049
|
-
let authorization;
|
|
1050
|
-
try {
|
|
1051
|
-
const parsed = JSON.parse(paymentHeader);
|
|
1052
|
-
authorization = parseAuthorization(parsed);
|
|
1053
|
-
} catch {
|
|
1054
|
-
return c.json({ error: "Invalid X-PAYMENT header" }, 400);
|
|
1055
|
-
}
|
|
1056
|
-
const preflightConfig = {
|
|
1057
|
-
chainConfig: config.chainConfig,
|
|
1058
|
-
basePricePerByte: config.basePricePerByte,
|
|
1059
|
-
ownPubkey: config.ownPubkey,
|
|
1060
|
-
devMode: config.devMode,
|
|
1061
|
-
publicClient: config.publicClient,
|
|
1062
|
-
eventStore: config.eventStore
|
|
1063
|
-
};
|
|
1064
|
-
try {
|
|
1065
|
-
const preflightFn = config.runPreflightFn ?? runPreflight;
|
|
1066
|
-
const preflightResult = await preflightFn(
|
|
1067
|
-
authorization,
|
|
1068
|
-
toonBase64,
|
|
1069
|
-
body.destination,
|
|
1070
|
-
preflightConfig
|
|
1071
|
-
);
|
|
1072
|
-
if (!preflightResult.passed) {
|
|
1073
|
-
return c.json(
|
|
1074
|
-
{
|
|
1075
|
-
error: `Pre-flight check failed: ${preflightResult.failedCheck}`,
|
|
1076
|
-
failedCheck: preflightResult.failedCheck
|
|
1077
|
-
},
|
|
1078
|
-
400
|
|
1079
|
-
);
|
|
1080
|
-
}
|
|
1081
|
-
} catch {
|
|
1082
|
-
console.error("[x402] Pre-flight error");
|
|
1083
|
-
return c.json({ error: "Internal server error" }, 500);
|
|
1084
|
-
}
|
|
1085
|
-
let settlementResult;
|
|
1086
|
-
try {
|
|
1087
|
-
const settleFn = config.settle ?? settleEip3009;
|
|
1088
|
-
if (!config.settle && !config.walletClient) {
|
|
1089
|
-
console.error("[x402] Settlement error: walletClient not configured");
|
|
1090
|
-
return c.json({ error: "Internal server error" }, 500);
|
|
1091
|
-
}
|
|
1092
|
-
const settlementConfig = {
|
|
1093
|
-
chainConfig: config.chainConfig,
|
|
1094
|
-
walletClient: config.walletClient,
|
|
1095
|
-
publicClient: config.publicClient
|
|
1096
|
-
};
|
|
1097
|
-
settlementResult = await settleFn(authorization, settlementConfig);
|
|
1098
|
-
} catch {
|
|
1099
|
-
console.error("[x402] Settlement error");
|
|
1100
|
-
return c.json({ error: "Internal server error" }, 500);
|
|
1101
|
-
}
|
|
1102
|
-
if (!settlementResult.success) {
|
|
1103
|
-
console.error(
|
|
1104
|
-
"[x402] Settlement failed:",
|
|
1105
|
-
settlementResult.error ?? "unknown"
|
|
1106
|
-
);
|
|
1107
|
-
return c.json({ error: "Settlement failed" }, 400);
|
|
1108
|
-
}
|
|
1109
|
-
const amount = config.basePricePerByte * BigInt(toonBytes.length);
|
|
1110
|
-
const prepareParams = {
|
|
1111
|
-
destination: body.destination,
|
|
1112
|
-
amount,
|
|
1113
|
-
data: toonBytes
|
|
1114
|
-
};
|
|
1115
|
-
const prepare = buildIlpPrepare(prepareParams);
|
|
1116
|
-
let deliveryStatus = "rejected";
|
|
1117
|
-
if (config.ilpClient) {
|
|
1118
|
-
try {
|
|
1119
|
-
const ilpResult = await config.ilpClient.sendIlpPacket(prepare);
|
|
1120
|
-
deliveryStatus = ilpResult.accepted ? "fulfilled" : "rejected";
|
|
1121
|
-
} catch {
|
|
1122
|
-
deliveryStatus = "rejected";
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
const response = {
|
|
1126
|
-
eventId: body.event.id,
|
|
1127
|
-
settlementTxHash: settlementResult.txHash ?? "",
|
|
1128
|
-
deliveryStatus,
|
|
1129
|
-
refundInitiated: false
|
|
1130
|
-
};
|
|
1131
|
-
return c.json(response, 200);
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
function isValidHex(value, expectedLength) {
|
|
1136
|
-
if (value.length !== expectedLength) return false;
|
|
1137
|
-
if (!value.startsWith("0x")) return false;
|
|
1138
|
-
return /^0x[0-9a-fA-F]+$/.test(value);
|
|
1139
|
-
}
|
|
1140
|
-
function parseAuthorization(parsed) {
|
|
1141
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
1142
|
-
throw new Error("Authorization must be an object");
|
|
1143
|
-
}
|
|
1144
|
-
const obj = parsed;
|
|
1145
|
-
const from = obj["from"];
|
|
1146
|
-
const to = obj["to"];
|
|
1147
|
-
const value = obj["value"];
|
|
1148
|
-
const validAfter = obj["validAfter"];
|
|
1149
|
-
const validBefore = obj["validBefore"];
|
|
1150
|
-
const nonce = obj["nonce"];
|
|
1151
|
-
const v = obj["v"];
|
|
1152
|
-
const r = obj["r"];
|
|
1153
|
-
const s = obj["s"];
|
|
1154
|
-
if (typeof from !== "string" || !isValidHex(from, 42)) {
|
|
1155
|
-
throw new Error("Invalid from address");
|
|
1156
|
-
}
|
|
1157
|
-
if (typeof to !== "string" || !isValidHex(to, 42)) {
|
|
1158
|
-
throw new Error("Invalid to address");
|
|
1159
|
-
}
|
|
1160
|
-
if (typeof nonce !== "string" || !isValidHex(nonce, 66)) {
|
|
1161
|
-
throw new Error("Invalid nonce");
|
|
1162
|
-
}
|
|
1163
|
-
if (typeof r !== "string" || !isValidHex(r, 66)) {
|
|
1164
|
-
throw new Error("Invalid r");
|
|
1165
|
-
}
|
|
1166
|
-
if (typeof s !== "string" || !isValidHex(s, 66)) {
|
|
1167
|
-
throw new Error("Invalid s");
|
|
1168
|
-
}
|
|
1169
|
-
if (typeof v !== "number" || v !== 27 && v !== 28) {
|
|
1170
|
-
throw new Error("Invalid v");
|
|
1171
|
-
}
|
|
1172
|
-
const parsedValidAfter = Number(validAfter);
|
|
1173
|
-
const parsedValidBefore = Number(validBefore);
|
|
1174
|
-
if (Number.isNaN(parsedValidAfter) || parsedValidAfter < 0) {
|
|
1175
|
-
throw new Error("Invalid validAfter");
|
|
1176
|
-
}
|
|
1177
|
-
if (Number.isNaN(parsedValidBefore) || parsedValidBefore < 0) {
|
|
1178
|
-
throw new Error("Invalid validBefore");
|
|
1179
|
-
}
|
|
1180
|
-
const valueStr = String(value);
|
|
1181
|
-
let valueBigInt;
|
|
1182
|
-
try {
|
|
1183
|
-
valueBigInt = BigInt(valueStr);
|
|
1184
|
-
} catch {
|
|
1185
|
-
throw new Error("Invalid value");
|
|
1186
|
-
}
|
|
1187
|
-
if (valueBigInt < 0n) {
|
|
1188
|
-
throw new Error("Invalid value: must be non-negative");
|
|
1189
|
-
}
|
|
1190
|
-
return {
|
|
1191
|
-
from,
|
|
1192
|
-
to,
|
|
1193
|
-
value: valueBigInt,
|
|
1194
|
-
validAfter: parsedValidAfter,
|
|
1195
|
-
validBefore: parsedValidBefore,
|
|
1196
|
-
nonce,
|
|
1197
|
-
v,
|
|
1198
|
-
r,
|
|
1199
|
-
s
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// src/launcher/health.ts
|
|
1204
|
-
import { VERSION } from "@toon-protocol/core";
|
|
1205
|
-
function createHealthResponse(config) {
|
|
1206
|
-
const response = {
|
|
1207
|
-
status: "healthy",
|
|
1208
|
-
phase: config.phase,
|
|
1209
|
-
pubkey: config.pubkey,
|
|
1210
|
-
ilpAddress: config.ilpAddress,
|
|
1211
|
-
peerCount: config.peerCount,
|
|
1212
|
-
discoveredPeerCount: config.discoveredPeerCount,
|
|
1213
|
-
channelCount: config.channelCount,
|
|
1214
|
-
pricing: {
|
|
1215
|
-
basePricePerByte: Number(config.basePricePerByte),
|
|
1216
|
-
currency: "USDC"
|
|
1217
|
-
},
|
|
1218
|
-
capabilities: config.x402Enabled ? ["relay", "x402"] : ["relay"],
|
|
1219
|
-
chain: config.chain,
|
|
1220
|
-
version: VERSION,
|
|
1221
|
-
sdk: true,
|
|
1222
|
-
timestamp: Date.now()
|
|
1223
|
-
};
|
|
1224
|
-
if (config.x402Enabled) {
|
|
1225
|
-
response.x402 = {
|
|
1226
|
-
enabled: true,
|
|
1227
|
-
endpoint: "/publish"
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
if (config.tee) {
|
|
1231
|
-
response.tee = config.tee;
|
|
1232
|
-
}
|
|
1233
|
-
return response;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/launcher/town.ts
|
|
1237
|
-
import { mkdirSync } from "fs";
|
|
1238
|
-
import { join } from "path";
|
|
1239
|
-
import { serve } from "@hono/node-server";
|
|
1240
|
-
import { Hono } from "hono";
|
|
1241
|
-
import {
|
|
1242
|
-
HandlerRegistry,
|
|
1243
|
-
createVerificationPipeline,
|
|
1244
|
-
createPricingValidator,
|
|
1245
|
-
createHandlerContext,
|
|
1246
|
-
fromMnemonic,
|
|
1247
|
-
fromSecretKey
|
|
1248
|
-
} from "@toon-protocol/sdk";
|
|
1249
|
-
import {
|
|
1250
|
-
BootstrapService,
|
|
1251
|
-
createDiscoveryTracker,
|
|
1252
|
-
ILP_PEER_INFO_KIND,
|
|
1253
|
-
createDirectIlpClient,
|
|
1254
|
-
createDirectConnectorAdmin,
|
|
1255
|
-
createDirectChannelClient,
|
|
1256
|
-
SocialPeerDiscovery,
|
|
1257
|
-
buildIlpPeerInfoEvent,
|
|
1258
|
-
resolveChainConfig,
|
|
1259
|
-
SeedRelayDiscovery,
|
|
1260
|
-
publishSeedRelayEntry,
|
|
1261
|
-
buildServiceDiscoveryEvent,
|
|
1262
|
-
VERSION as VERSION2
|
|
1263
|
-
} from "@toon-protocol/core";
|
|
1264
|
-
import {
|
|
1265
|
-
shallowParseToon as shallowParseToon2,
|
|
1266
|
-
decodeEventFromToon as decodeEventFromToon2,
|
|
1267
|
-
encodeEventToToon as encodeEventToToon3
|
|
1268
|
-
} from "@toon-protocol/core/toon";
|
|
1269
|
-
import {
|
|
1270
|
-
ConnectorNode,
|
|
1271
|
-
createLogger as createConnectorLogger
|
|
1272
|
-
} from "@toon-protocol/connector";
|
|
1273
|
-
import {
|
|
1274
|
-
createPublicClient,
|
|
1275
|
-
createWalletClient,
|
|
1276
|
-
defineChain,
|
|
1277
|
-
http
|
|
1278
|
-
} from "viem";
|
|
1279
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
1280
|
-
var MAX_PAYLOAD_BASE64_LENGTH = 1048576;
|
|
1281
|
-
function createSubscription(relayUrl, filter, eventStore, activeSubscriptions) {
|
|
1282
|
-
if (!relayUrl.startsWith("ws://") && !relayUrl.startsWith("wss://")) {
|
|
1283
|
-
throw new Error(
|
|
1284
|
-
"Invalid relay URL -- must use WebSocket scheme (ws or wss)"
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
const subscriber = new RelaySubscriber(
|
|
1288
|
-
{ relayUrls: [relayUrl], filter },
|
|
1289
|
-
eventStore
|
|
1290
|
-
);
|
|
1291
|
-
const handle = subscriber.start();
|
|
1292
|
-
let active = true;
|
|
1293
|
-
let _lastSeenTimestamp = 0;
|
|
1294
|
-
void _lastSeenTimestamp;
|
|
1295
|
-
const subscription = {
|
|
1296
|
-
close() {
|
|
1297
|
-
if (!active) return;
|
|
1298
|
-
active = false;
|
|
1299
|
-
handle.unsubscribe();
|
|
1300
|
-
activeSubscriptions.delete(subscription);
|
|
1301
|
-
},
|
|
1302
|
-
relayUrl,
|
|
1303
|
-
isActive() {
|
|
1304
|
-
return active;
|
|
1305
|
-
}
|
|
1306
|
-
};
|
|
1307
|
-
activeSubscriptions.add(subscription);
|
|
1308
|
-
return subscription;
|
|
1309
|
-
}
|
|
1310
|
-
async function startRelay(config) {
|
|
1311
|
-
const hasMnemonic = config.mnemonic !== void 0;
|
|
1312
|
-
const hasSecretKey = config.secretKey !== void 0;
|
|
1313
|
-
if (hasMnemonic && hasSecretKey) {
|
|
1314
|
-
throw new Error(
|
|
1315
|
-
"RelayConfig: provide either mnemonic or secretKey, not both"
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
if (!hasMnemonic && !hasSecretKey) {
|
|
1319
|
-
throw new Error("RelayConfig: one of mnemonic or secretKey is required");
|
|
1320
|
-
}
|
|
1321
|
-
const hasConnector = config.connector !== void 0;
|
|
1322
|
-
const hasConnectorUrl = config.connectorUrl !== void 0;
|
|
1323
|
-
if (hasConnector && hasConnectorUrl) {
|
|
1324
|
-
throw new Error(
|
|
1325
|
-
"RelayConfig: provide either connector or connectorUrl, not both"
|
|
1326
|
-
);
|
|
1327
|
-
}
|
|
1328
|
-
if (hasConnectorUrl && config.ilpAddress === void 0) {
|
|
1329
|
-
throw new Error(
|
|
1330
|
-
"RelayConfig: ilpAddress is required when connectorUrl is set (must fall under the parent connector prefix, e.g. g.townhouse.<self>)"
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
const identity = hasMnemonic ? fromMnemonic(config.mnemonic) : fromSecretKey(config.secretKey);
|
|
1334
|
-
const relayPort = config.relayPort ?? 7100;
|
|
1335
|
-
const blsPort = config.blsPort ?? 3100;
|
|
1336
|
-
const pubkeyShort = identity.pubkey.slice(0, 16);
|
|
1337
|
-
const ilpAddress = config.ilpAddress ?? `g.toon.${pubkeyShort}`;
|
|
1338
|
-
const btpEndpoint = config.btpEndpoint ?? "";
|
|
1339
|
-
const nodeId = config.nodeId ?? `toon-${pubkeyShort}`;
|
|
1340
|
-
const parentPeerId = config.parentPeerId ?? "apex";
|
|
1341
|
-
const parentAuthToken = config.parentAuthToken ?? "";
|
|
1342
|
-
const connectorUrl = config.connectorUrl;
|
|
1343
|
-
const basePricePerByte = config.feePerEvent !== void 0 ? BigInt(config.feePerEvent) : config.basePricePerByte ?? 10n;
|
|
1344
|
-
const routingBufferPercent = config.routingBufferPercent ?? 10;
|
|
1345
|
-
const x402Enabled = config.x402Enabled ?? false;
|
|
1346
|
-
const knownPeers = [...config.knownPeers ?? []];
|
|
1347
|
-
const dataDir = config.dataDir ?? "./data";
|
|
1348
|
-
const devMode = config.devMode ?? false;
|
|
1349
|
-
const ardriveEnabled = config.ardriveEnabled ?? false;
|
|
1350
|
-
const relayUrls = config.relayUrls ?? [`ws://localhost:${relayPort}`];
|
|
1351
|
-
const assetCode = config.assetCode ?? "USD";
|
|
1352
|
-
const assetScale = config.assetScale ?? 6;
|
|
1353
|
-
const discovery = config.discovery ?? "genesis";
|
|
1354
|
-
const announcementTtlSeconds = (() => {
|
|
1355
|
-
const fromEnv = process.env["TOON_ANNOUNCEMENT_TTL_SECONDS"];
|
|
1356
|
-
const raw = fromEnv !== void 0 && fromEnv !== "" ? Number(fromEnv) : config.announcementTtlSeconds;
|
|
1357
|
-
if (raw === void 0) return 3600;
|
|
1358
|
-
if (!Number.isFinite(raw) || raw < 0) return 3600;
|
|
1359
|
-
return Math.floor(raw);
|
|
1360
|
-
})();
|
|
1361
|
-
const seedRelays = config.seedRelays ?? [];
|
|
1362
|
-
const publishSeedEntryFlag = config.publishSeedEntry ?? false;
|
|
1363
|
-
const externalRelayUrl = config.externalRelayUrl ?? (config.ator?.enabled && config.ator.anonAddress ? config.ator.anonAddress : void 0);
|
|
1364
|
-
const requestedChain = process.env["TOON_CHAIN"] || config.chain;
|
|
1365
|
-
const relayOnly = requestedChain === "none";
|
|
1366
|
-
if (relayOnly) {
|
|
1367
|
-
console.log("[Town] connector.relay_only", {
|
|
1368
|
-
reason: "no settlement chain configured (chain=none)"
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
const chainConfig = relayOnly ? {
|
|
1372
|
-
name: "none",
|
|
1373
|
-
chainId: 0,
|
|
1374
|
-
rpcUrl: "",
|
|
1375
|
-
usdcAddress: "",
|
|
1376
|
-
tokenNetworkAddress: "",
|
|
1377
|
-
registryAddress: ""
|
|
1378
|
-
} : resolveChainConfig(config.chain);
|
|
1379
|
-
const chainKey = `evm:base:${chainConfig.chainId}`;
|
|
1380
|
-
const resolvedConfig = {
|
|
1381
|
-
relayPort,
|
|
1382
|
-
blsPort,
|
|
1383
|
-
ilpAddress,
|
|
1384
|
-
btpEndpoint,
|
|
1385
|
-
nodeId,
|
|
1386
|
-
...connectorUrl && { connectorUrl, parentPeerId },
|
|
1387
|
-
basePricePerByte,
|
|
1388
|
-
routingBufferPercent,
|
|
1389
|
-
x402Enabled,
|
|
1390
|
-
knownPeers,
|
|
1391
|
-
dataDir,
|
|
1392
|
-
devMode,
|
|
1393
|
-
ardriveEnabled,
|
|
1394
|
-
relayUrls,
|
|
1395
|
-
assetCode,
|
|
1396
|
-
assetScale,
|
|
1397
|
-
discovery,
|
|
1398
|
-
seedRelays,
|
|
1399
|
-
publishSeedEntry: publishSeedEntryFlag,
|
|
1400
|
-
...externalRelayUrl && { externalRelayUrl },
|
|
1401
|
-
chain: chainConfig.name
|
|
1402
|
-
};
|
|
1403
|
-
let autoCreatedConnector = null;
|
|
1404
|
-
if (!hasConnector) {
|
|
1405
|
-
const btpServerPort = config.btpServerPort ?? 3e3;
|
|
1406
|
-
const connectorLogger = createConnectorLogger(
|
|
1407
|
-
nodeId,
|
|
1408
|
-
process.env["TOON_CONNECTOR_LOG_LEVEL"] ?? "warn"
|
|
1409
|
-
);
|
|
1410
|
-
const routes = [{ prefix: ilpAddress, nextHop: nodeId, priority: 100 }];
|
|
1411
|
-
const peers = [];
|
|
1412
|
-
if (hasConnectorUrl) {
|
|
1413
|
-
peers.push({
|
|
1414
|
-
id: parentPeerId,
|
|
1415
|
-
url: connectorUrl,
|
|
1416
|
-
authToken: parentAuthToken,
|
|
1417
|
-
// Tag the upstream as our PARENT so the embedded connector's
|
|
1418
|
-
// relation-aware logic applies (toon-protocol/connector#78): a child
|
|
1419
|
-
// skips the inbound per-packet-claim requirement for PREPAREs forwarded
|
|
1420
|
-
// by its parent (the parent settles in aggregate and attaches no
|
|
1421
|
-
// per-packet claim to a child). Without this the peer defaults to
|
|
1422
|
-
// 'peer' and the child F06-rejects every parent-forwarded paid packet.
|
|
1423
|
-
// NOTE: `parentPeerId` MUST equal the parent connector's nodeId (its BTP
|
|
1424
|
-
// auth identity), since the connector keys peerRelations by the
|
|
1425
|
-
// auth-declared peerId of the inbound session — not a local alias.
|
|
1426
|
-
relation: "parent",
|
|
1427
|
-
// When the operator publishes their EVM treasury address to the
|
|
1428
|
-
// parent, the apex can open a settlement channel toward this child
|
|
1429
|
-
// without needing to discover the address via kind:10032. The
|
|
1430
|
-
// connector schema treats this field as optional metadata.
|
|
1431
|
-
...config.parentEvmAddress && { evmAddress: config.parentEvmAddress }
|
|
1432
|
-
});
|
|
1433
|
-
routes.push({ prefix: "g", nextHop: parentPeerId, priority: 0 });
|
|
1434
|
-
}
|
|
1435
|
-
const hasSettlementAddresses = !!chainConfig.rpcUrl && !!chainConfig.registryAddress && !!chainConfig.tokenNetworkAddress && !!chainConfig.usdcAddress;
|
|
1436
|
-
let chainProvidersEntry = null;
|
|
1437
|
-
if (hasSettlementAddresses) {
|
|
1438
|
-
const keyHex = config.settlementPrivateKey ?? `0x${Buffer.from(identity.secretKey).toString("hex")}`;
|
|
1439
|
-
if (!/^0x[0-9a-fA-F]{64}$/.test(keyHex)) {
|
|
1440
|
-
throw new Error(
|
|
1441
|
-
`RelayConfig.settlementPrivateKey must be a 0x-prefixed 32-byte hex string (got length ${keyHex.length}); cannot wire chainProviders for ${chainConfig.name}`
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
chainProvidersEntry = {
|
|
1445
|
-
chainType: "evm",
|
|
1446
|
-
chainId: `evm:${chainConfig.chainId}`,
|
|
1447
|
-
rpcUrl: chainConfig.rpcUrl,
|
|
1448
|
-
registryAddress: chainConfig.registryAddress,
|
|
1449
|
-
tokenAddress: chainConfig.usdcAddress,
|
|
1450
|
-
keyId: keyHex
|
|
1451
|
-
};
|
|
1452
|
-
} else {
|
|
1453
|
-
console.warn("[Town] connector.chain_providers_skipped", {
|
|
1454
|
-
chain: chainConfig.name,
|
|
1455
|
-
reason: "missing settlement addresses"
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
const connectorConfig = {
|
|
1459
|
-
nodeId,
|
|
1460
|
-
btpServerPort,
|
|
1461
|
-
environment: "development",
|
|
1462
|
-
deploymentMode: "embedded",
|
|
1463
|
-
peers,
|
|
1464
|
-
routes,
|
|
1465
|
-
localDelivery: { enabled: false },
|
|
1466
|
-
// Children don't expose an admin API — the apex parent is the
|
|
1467
|
-
// operator-facing surface. Disabling avoids a hard runtime dep on
|
|
1468
|
-
// express in the town docker bundle.
|
|
1469
|
-
adminApi: { enabled: false },
|
|
1470
|
-
// Belt-and-braces: zero connector forwarding fee. Combined with the
|
|
1471
|
-
// packet-handler's automatic skip for local-delivery hops this keeps the
|
|
1472
|
-
// child fee surface flat regardless of peering topology.
|
|
1473
|
-
settlement: {
|
|
1474
|
-
connectorFeePercentage: 0
|
|
1475
|
-
},
|
|
1476
|
-
...chainProvidersEntry && { chainProviders: [chainProvidersEntry] }
|
|
1477
|
-
};
|
|
1478
|
-
if (config.ator?.enabled && config.ator.anonAddress) {
|
|
1479
|
-
connectorConfig.transport = {
|
|
1480
|
-
type: "socks5",
|
|
1481
|
-
socksProxy: config.ator.socksProxy ?? "socks5h://127.0.0.1:9050",
|
|
1482
|
-
externalUrl: config.ator.anonAddress,
|
|
1483
|
-
managed: false
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
autoCreatedConnector = new ConnectorNode(connectorConfig, connectorLogger);
|
|
1487
|
-
}
|
|
1488
|
-
const effectiveConnector = config.connector ?? autoCreatedConnector;
|
|
1489
|
-
mkdirSync(dataDir, { recursive: true });
|
|
1490
|
-
const dbPath = join(dataDir, "events.db");
|
|
1491
|
-
const eventStore = new SqliteEventStore(dbPath);
|
|
1492
|
-
const effectiveChainRpcUrls = config.chainRpcUrls ?? (relayOnly ? void 0 : { [chainKey]: chainConfig.rpcUrl });
|
|
1493
|
-
const effectivePreferredTokens = config.preferredTokens ?? (relayOnly ? void 0 : { [chainKey]: chainConfig.usdcAddress });
|
|
1494
|
-
const effectiveTokenNetworks = config.tokenNetworks ?? (chainConfig.tokenNetworkAddress ? { [chainKey]: chainConfig.tokenNetworkAddress } : void 0);
|
|
1495
|
-
let channelClient;
|
|
1496
|
-
let settlementInfo;
|
|
1497
|
-
const hasSettlement = effectiveChainRpcUrls || effectiveTokenNetworks || effectivePreferredTokens || config.settlementAddresses;
|
|
1498
|
-
if (hasSettlement) {
|
|
1499
|
-
const supportedChains = Array.from(
|
|
1500
|
-
/* @__PURE__ */ new Set([
|
|
1501
|
-
...Object.keys(effectiveChainRpcUrls ?? {}),
|
|
1502
|
-
...Object.keys(effectiveTokenNetworks ?? {}),
|
|
1503
|
-
...Object.keys(effectivePreferredTokens ?? {}),
|
|
1504
|
-
...Object.keys(config.settlementAddresses ?? {})
|
|
1505
|
-
])
|
|
1506
|
-
);
|
|
1507
|
-
const settlementAddresses = {};
|
|
1508
|
-
for (const chain of supportedChains) {
|
|
1509
|
-
settlementAddresses[chain] = config.settlementAddresses?.[chain] ?? identity.evmAddress;
|
|
1510
|
-
}
|
|
1511
|
-
settlementInfo = {
|
|
1512
|
-
supportedChains,
|
|
1513
|
-
settlementAddresses,
|
|
1514
|
-
preferredTokens: effectivePreferredTokens,
|
|
1515
|
-
tokenNetworks: effectiveTokenNetworks
|
|
1516
|
-
};
|
|
1517
|
-
if (effectiveConnector.openChannel && effectiveConnector.getChannelState) {
|
|
1518
|
-
channelClient = createDirectChannelClient(
|
|
1519
|
-
effectiveConnector
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
const adminClient = createDirectConnectorAdmin(effectiveConnector);
|
|
1524
|
-
const verifier = createVerificationPipeline({ devMode });
|
|
1525
|
-
const pricer = createPricingValidator({
|
|
1526
|
-
basePricePerByte,
|
|
1527
|
-
ownPubkey: identity.pubkey
|
|
1528
|
-
});
|
|
1529
|
-
const registry = new HandlerRegistry();
|
|
1530
|
-
registry.onDefault(createEventStorageHandler({ eventStore }));
|
|
1531
|
-
const toonDecoder = (toon) => {
|
|
1532
|
-
const bytes = Buffer.from(toon, "base64");
|
|
1533
|
-
return decodeEventFromToon2(bytes);
|
|
1534
|
-
};
|
|
1535
|
-
const handlePacket = async (request) => {
|
|
1536
|
-
if (request.data.length > MAX_PAYLOAD_BASE64_LENGTH) {
|
|
1537
|
-
return { accept: false, code: "F08", message: "Payload too large" };
|
|
1538
|
-
}
|
|
1539
|
-
const toonBytes = Buffer.from(request.data, "base64");
|
|
1540
|
-
let meta;
|
|
1541
|
-
try {
|
|
1542
|
-
meta = shallowParseToon2(toonBytes);
|
|
1543
|
-
} catch {
|
|
1544
|
-
return { accept: false, code: "F06", message: "Invalid TOON payload" };
|
|
1545
|
-
}
|
|
1546
|
-
const verifyResult = await verifier.verify(meta, request.data);
|
|
1547
|
-
if (!verifyResult.verified) {
|
|
1548
|
-
if (verifyResult.rejection) {
|
|
1549
|
-
return verifyResult.rejection;
|
|
1550
|
-
}
|
|
1551
|
-
return { accept: false, code: "F06", message: "Verification failed" };
|
|
1552
|
-
}
|
|
1553
|
-
let amount;
|
|
1554
|
-
try {
|
|
1555
|
-
amount = BigInt(request.amount);
|
|
1556
|
-
} catch {
|
|
1557
|
-
return {
|
|
1558
|
-
accept: false,
|
|
1559
|
-
code: "T00",
|
|
1560
|
-
message: "Invalid payment amount"
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
const priceResult = pricer.validate(meta, amount);
|
|
1564
|
-
if (!priceResult.accepted) {
|
|
1565
|
-
if (priceResult.rejection) {
|
|
1566
|
-
return priceResult.rejection;
|
|
1567
|
-
}
|
|
1568
|
-
return {
|
|
1569
|
-
accept: false,
|
|
1570
|
-
code: "F04",
|
|
1571
|
-
message: "Pricing validation failed"
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
const ctx = createHandlerContext({
|
|
1575
|
-
toon: request.data,
|
|
1576
|
-
meta,
|
|
1577
|
-
amount,
|
|
1578
|
-
destination: request.destination,
|
|
1579
|
-
toonDecoder
|
|
1580
|
-
});
|
|
1581
|
-
try {
|
|
1582
|
-
const result = await registry.dispatch(ctx);
|
|
1583
|
-
if (result.accept) {
|
|
1584
|
-
try {
|
|
1585
|
-
const event = decodeEventFromToon2(toonBytes);
|
|
1586
|
-
wsRelayRef.current?.broadcastEvent(event);
|
|
1587
|
-
} catch {
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
return result;
|
|
1591
|
-
} catch (err) {
|
|
1592
|
-
const errMsg = err instanceof Error ? err.message : "Unknown error";
|
|
1593
|
-
console.error("[Town] Handler dispatch failed:", errMsg);
|
|
1594
|
-
return { accept: false, code: "T00", message: "Internal error" };
|
|
1595
|
-
}
|
|
1596
|
-
};
|
|
1597
|
-
const bootstrapService = new BootstrapService(
|
|
1598
|
-
{
|
|
1599
|
-
knownPeers,
|
|
1600
|
-
ardriveEnabled,
|
|
1601
|
-
defaultRelayUrl: `ws://localhost:${relayPort}`,
|
|
1602
|
-
...settlementInfo && { settlementInfo },
|
|
1603
|
-
ownIlpAddress: ilpAddress,
|
|
1604
|
-
toonEncoder: encodeEventToToon3,
|
|
1605
|
-
toonDecoder: decodeEventFromToon2,
|
|
1606
|
-
basePricePerByte
|
|
1607
|
-
},
|
|
1608
|
-
identity.secretKey,
|
|
1609
|
-
{
|
|
1610
|
-
ilpAddress,
|
|
1611
|
-
btpEndpoint,
|
|
1612
|
-
assetCode,
|
|
1613
|
-
assetScale
|
|
1614
|
-
}
|
|
1615
|
-
);
|
|
1616
|
-
let peerCount = 0;
|
|
1617
|
-
let channelCount = 0;
|
|
1618
|
-
const discoveryTrackerRef = {};
|
|
1619
|
-
const wsRelayRef = {};
|
|
1620
|
-
const app = new Hono();
|
|
1621
|
-
app.get("/health", (c) => {
|
|
1622
|
-
const bootstrapPhase = bootstrapService.getPhase();
|
|
1623
|
-
const dt = discoveryTrackerRef.current;
|
|
1624
|
-
return c.json(
|
|
1625
|
-
createHealthResponse({
|
|
1626
|
-
phase: bootstrapPhase,
|
|
1627
|
-
pubkey: identity.pubkey,
|
|
1628
|
-
ilpAddress,
|
|
1629
|
-
peerCount: (dt ? dt.getPeerCount() : 0) + peerCount,
|
|
1630
|
-
discoveredPeerCount: dt ? dt.getDiscoveredCount() : 0,
|
|
1631
|
-
channelCount,
|
|
1632
|
-
basePricePerByte,
|
|
1633
|
-
x402Enabled,
|
|
1634
|
-
chain: chainConfig.name
|
|
1635
|
-
})
|
|
1636
|
-
);
|
|
1637
|
-
});
|
|
1638
|
-
app.post("/handle-packet", async (c) => {
|
|
1639
|
-
try {
|
|
1640
|
-
const body = await c.req.json();
|
|
1641
|
-
if (body.amount === void 0 || body.amount === null || body.destination === void 0 || body.destination === null || body.data === void 0 || body.data === null) {
|
|
1642
|
-
return c.json(
|
|
1643
|
-
{ accept: false, code: "F00", message: "Missing required fields" },
|
|
1644
|
-
400
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
const result = await handlePacket(body);
|
|
1648
|
-
if (result.accept) {
|
|
1649
|
-
try {
|
|
1650
|
-
const toonBytes = Buffer.from(body.data, "base64");
|
|
1651
|
-
const decoded = decodeEventFromToon2(toonBytes);
|
|
1652
|
-
if (decoded && decoded.kind === ILP_PEER_INFO_KIND) {
|
|
1653
|
-
discoveryTrackerRef.current?.processEvent(decoded);
|
|
1654
|
-
}
|
|
1655
|
-
} catch {
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
return c.json(result, result.accept ? 200 : 400);
|
|
1659
|
-
} catch (error) {
|
|
1660
|
-
console.error("[Town] handle-packet error:", error);
|
|
1661
|
-
return c.json(
|
|
1662
|
-
{ accept: false, code: "T00", message: "Internal server error" },
|
|
1663
|
-
500
|
|
1664
|
-
);
|
|
1665
|
-
}
|
|
1666
|
-
});
|
|
1667
|
-
const ilpClient = createDirectIlpClient(effectiveConnector, {
|
|
1668
|
-
toonDecoder: (bytes) => decodeEventFromToon2(bytes)
|
|
1669
|
-
});
|
|
1670
|
-
let x402WalletClient;
|
|
1671
|
-
let x402PublicClient;
|
|
1672
|
-
if (x402Enabled) {
|
|
1673
|
-
let keyBuffer;
|
|
1674
|
-
try {
|
|
1675
|
-
keyBuffer = Buffer.from(identity.secretKey);
|
|
1676
|
-
const privateKeyHex = `0x${keyBuffer.toString("hex")}`;
|
|
1677
|
-
const account = privateKeyToAccount(privateKeyHex);
|
|
1678
|
-
const viemChain = defineChain({
|
|
1679
|
-
id: chainConfig.chainId,
|
|
1680
|
-
name: chainConfig.name,
|
|
1681
|
-
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
|
|
1682
|
-
rpcUrls: { default: { http: [] } }
|
|
1683
|
-
});
|
|
1684
|
-
x402PublicClient = createPublicClient({
|
|
1685
|
-
chain: viemChain,
|
|
1686
|
-
transport: http(chainConfig.rpcUrl)
|
|
1687
|
-
});
|
|
1688
|
-
x402WalletClient = createWalletClient({
|
|
1689
|
-
account,
|
|
1690
|
-
chain: viemChain,
|
|
1691
|
-
transport: http(chainConfig.rpcUrl)
|
|
1692
|
-
});
|
|
1693
|
-
} catch (error) {
|
|
1694
|
-
throw new Error(
|
|
1695
|
-
`x402 initialization failed: could not derive EVM account from identity key: ${error instanceof Error ? error.message : String(error)}`
|
|
1696
|
-
);
|
|
1697
|
-
} finally {
|
|
1698
|
-
if (keyBuffer) {
|
|
1699
|
-
keyBuffer.fill(0);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
const x402Handler = createX402Handler({
|
|
1704
|
-
x402Enabled,
|
|
1705
|
-
chainConfig,
|
|
1706
|
-
basePricePerByte,
|
|
1707
|
-
routingBufferPercent,
|
|
1708
|
-
facilitatorAddress: config.facilitatorAddress ?? identity.evmAddress,
|
|
1709
|
-
ownPubkey: identity.pubkey,
|
|
1710
|
-
devMode,
|
|
1711
|
-
eventStore,
|
|
1712
|
-
ilpClient,
|
|
1713
|
-
walletClient: x402WalletClient,
|
|
1714
|
-
publicClient: x402PublicClient
|
|
1715
|
-
});
|
|
1716
|
-
app.get("/publish", (c) => x402Handler.handlePublish(c));
|
|
1717
|
-
app.post("/publish", (c) => x402Handler.handlePublish(c));
|
|
1718
|
-
const blsServer = serve({
|
|
1719
|
-
fetch: app.fetch,
|
|
1720
|
-
port: blsPort
|
|
1721
|
-
});
|
|
1722
|
-
const relayHost = config.ator?.enabled ? "127.0.0.1" : void 0;
|
|
1723
|
-
const wsRelay = new NostrRelayServer(
|
|
1724
|
-
{ port: relayPort, host: relayHost },
|
|
1725
|
-
eventStore
|
|
1726
|
-
);
|
|
1727
|
-
wsRelayRef.current = wsRelay;
|
|
1728
|
-
await wsRelay.start();
|
|
1729
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1730
|
-
let running = true;
|
|
1731
|
-
bootstrapService.setConnectorAdmin(adminClient);
|
|
1732
|
-
if (channelClient) {
|
|
1733
|
-
bootstrapService.setChannelClient(channelClient);
|
|
1734
|
-
}
|
|
1735
|
-
bootstrapService.setIlpClient(ilpClient);
|
|
1736
|
-
bootstrapService.on((event) => {
|
|
1737
|
-
switch (event.type) {
|
|
1738
|
-
case "bootstrap:peer-registered":
|
|
1739
|
-
peerCount++;
|
|
1740
|
-
break;
|
|
1741
|
-
case "bootstrap:channel-opened":
|
|
1742
|
-
channelCount++;
|
|
1743
|
-
break;
|
|
1744
|
-
case "bootstrap:ready":
|
|
1745
|
-
break;
|
|
1746
|
-
}
|
|
1747
|
-
});
|
|
1748
|
-
if (effectiveConnector.setPacketHandler) {
|
|
1749
|
-
effectiveConnector.setPacketHandler(async (request) => {
|
|
1750
|
-
const result = await handlePacket(request);
|
|
1751
|
-
if (result.accept && discoveryTrackerRef.current) {
|
|
1752
|
-
try {
|
|
1753
|
-
const toonBytes = Buffer.from(
|
|
1754
|
-
request.data,
|
|
1755
|
-
"base64"
|
|
1756
|
-
);
|
|
1757
|
-
const decoded = decodeEventFromToon2(toonBytes);
|
|
1758
|
-
if (decoded && decoded.kind === ILP_PEER_INFO_KIND) {
|
|
1759
|
-
discoveryTrackerRef.current.processEvent(decoded);
|
|
1760
|
-
}
|
|
1761
|
-
} catch {
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
return result;
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
if (autoCreatedConnector) {
|
|
1768
|
-
await autoCreatedConnector.start();
|
|
1769
|
-
}
|
|
1770
|
-
const discoveryTracker = createDiscoveryTracker({
|
|
1771
|
-
secretKey: identity.secretKey,
|
|
1772
|
-
settlementInfo
|
|
1773
|
-
});
|
|
1774
|
-
discoveryTracker.setConnectorAdmin(adminClient);
|
|
1775
|
-
if (channelClient) {
|
|
1776
|
-
discoveryTracker.setChannelClient(channelClient);
|
|
1777
|
-
}
|
|
1778
|
-
discoveryTrackerRef.current = discoveryTracker;
|
|
1779
|
-
let seedRelayDiscovery;
|
|
1780
|
-
if (discovery === "seed-list" && seedRelays.length > 0) {
|
|
1781
|
-
seedRelayDiscovery = new SeedRelayDiscovery({
|
|
1782
|
-
publicRelays: seedRelays
|
|
1783
|
-
});
|
|
1784
|
-
try {
|
|
1785
|
-
const seedResult = await seedRelayDiscovery.discover();
|
|
1786
|
-
const seedPeers = seedResult.discoveredPeers.filter((info) => info.pubkey).map((info) => ({
|
|
1787
|
-
pubkey: info.pubkey,
|
|
1788
|
-
relayUrl: seedResult.connectedUrls[0] ?? `ws://localhost:${relayPort}`,
|
|
1789
|
-
btpEndpoint: info.btpEndpoint
|
|
1790
|
-
}));
|
|
1791
|
-
const existingPubkeys = new Set(knownPeers.map((p) => p.pubkey));
|
|
1792
|
-
for (const seedPeer of seedPeers) {
|
|
1793
|
-
if (!existingPubkeys.has(seedPeer.pubkey)) {
|
|
1794
|
-
knownPeers.push(seedPeer);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
console.log(
|
|
1798
|
-
`[Town] Seed relay discovery: found ${seedPeers.length} peers from ${seedResult.connectedUrls.length} seed relay(s)`
|
|
1799
|
-
);
|
|
1800
|
-
} catch (seedError) {
|
|
1801
|
-
const msg = seedError instanceof Error ? seedError.message : "Unknown error";
|
|
1802
|
-
console.warn(`[Town] Seed relay discovery failed: ${msg}`);
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
let announcementHeartbeat;
|
|
1806
|
-
try {
|
|
1807
|
-
const results = await bootstrapService.bootstrap();
|
|
1808
|
-
const ownIlpInfo = {
|
|
1809
|
-
ilpAddress,
|
|
1810
|
-
btpEndpoint,
|
|
1811
|
-
assetCode,
|
|
1812
|
-
assetScale,
|
|
1813
|
-
// Advertise the publish price (per byte, in ILP base units) so clients can
|
|
1814
|
-
// compute the amount to attach before sending — derived from feePerEvent /
|
|
1815
|
-
// basePricePerByte. Previously omitted, leaving peers to assume free.
|
|
1816
|
-
feePerByte: String(basePricePerByte),
|
|
1817
|
-
// Public Nostr relay URL for FREE reads, so clients discover where to
|
|
1818
|
-
// subscribe (separate from btpEndpoint, which is the pay-to-write path).
|
|
1819
|
-
// Set when the operator exposes the relay publicly (HS .anyone or direct).
|
|
1820
|
-
...externalRelayUrl && { relayUrl: externalRelayUrl },
|
|
1821
|
-
...settlementInfo?.supportedChains && {
|
|
1822
|
-
supportedChains: settlementInfo.supportedChains
|
|
1823
|
-
},
|
|
1824
|
-
...settlementInfo?.settlementAddresses && {
|
|
1825
|
-
settlementAddresses: settlementInfo.settlementAddresses
|
|
1826
|
-
},
|
|
1827
|
-
...settlementInfo?.preferredTokens && {
|
|
1828
|
-
preferredTokens: settlementInfo.preferredTokens
|
|
1829
|
-
},
|
|
1830
|
-
...settlementInfo?.tokenNetworks && {
|
|
1831
|
-
tokenNetworks: settlementInfo.tokenNetworks
|
|
1832
|
-
}
|
|
1833
|
-
};
|
|
1834
|
-
const publishOwnAnnouncement = () => {
|
|
1835
|
-
try {
|
|
1836
|
-
const ilpInfoEvent = buildIlpPeerInfoEvent(
|
|
1837
|
-
ownIlpInfo,
|
|
1838
|
-
identity.secretKey,
|
|
1839
|
-
announcementTtlSeconds > 0 ? { ttlSeconds: announcementTtlSeconds } : {}
|
|
1840
|
-
);
|
|
1841
|
-
eventStore.store(ilpInfoEvent);
|
|
1842
|
-
const firstPeer = knownPeers[0];
|
|
1843
|
-
const genesisResult = results[0];
|
|
1844
|
-
if (firstPeer && genesisResult) {
|
|
1845
|
-
const genesisIlpAddress = genesisResult.peerInfo.ilpAddress;
|
|
1846
|
-
const toonBytes = encodeEventToToon3(ilpInfoEvent);
|
|
1847
|
-
const base64Toon = Buffer.from(toonBytes).toString("base64");
|
|
1848
|
-
const ilpAmount = String(BigInt(toonBytes.length) * basePricePerByte);
|
|
1849
|
-
ilpClient.sendIlpPacket({
|
|
1850
|
-
destination: genesisIlpAddress,
|
|
1851
|
-
amount: ilpAmount,
|
|
1852
|
-
data: base64Toon
|
|
1853
|
-
}).catch((err) => {
|
|
1854
|
-
const msg = err instanceof Error ? err.message : "Unknown";
|
|
1855
|
-
console.warn("[Town] Failed to publish via ILP:", msg);
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
} catch (error) {
|
|
1859
|
-
console.warn("[Town] Failed to publish ILP info:", error);
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
publishOwnAnnouncement();
|
|
1863
|
-
if (announcementTtlSeconds > 0) {
|
|
1864
|
-
const heartbeatMs = Math.max(
|
|
1865
|
-
1,
|
|
1866
|
-
Math.floor(announcementTtlSeconds * 1e3 / 2)
|
|
1867
|
-
);
|
|
1868
|
-
announcementHeartbeat = setInterval(publishOwnAnnouncement, heartbeatMs);
|
|
1869
|
-
announcementHeartbeat.unref?.();
|
|
1870
|
-
}
|
|
1871
|
-
try {
|
|
1872
|
-
const serviceDiscoveryContent = {
|
|
1873
|
-
serviceType: "relay",
|
|
1874
|
-
ilpAddress,
|
|
1875
|
-
pricing: {
|
|
1876
|
-
basePricePerByte: Number(basePricePerByte),
|
|
1877
|
-
currency: "USDC"
|
|
1878
|
-
},
|
|
1879
|
-
supportedKinds: [1, 10032, 10035, 10036],
|
|
1880
|
-
capabilities: x402Enabled ? ["relay", "x402"] : ["relay"],
|
|
1881
|
-
chain: chainConfig.name,
|
|
1882
|
-
version: VERSION2
|
|
1883
|
-
};
|
|
1884
|
-
if (x402Enabled) {
|
|
1885
|
-
serviceDiscoveryContent.x402 = {
|
|
1886
|
-
enabled: true,
|
|
1887
|
-
endpoint: "/publish"
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
if (config.skill) {
|
|
1891
|
-
serviceDiscoveryContent.skill = config.skill;
|
|
1892
|
-
}
|
|
1893
|
-
const serviceDiscoveryEvent = buildServiceDiscoveryEvent(
|
|
1894
|
-
serviceDiscoveryContent,
|
|
1895
|
-
identity.secretKey
|
|
1896
|
-
);
|
|
1897
|
-
eventStore.store(serviceDiscoveryEvent);
|
|
1898
|
-
const firstPeer = knownPeers[0];
|
|
1899
|
-
const genesisResult = results[0];
|
|
1900
|
-
if (firstPeer && genesisResult) {
|
|
1901
|
-
const genesisIlpAddress = genesisResult.peerInfo.ilpAddress;
|
|
1902
|
-
const sdToonBytes = encodeEventToToon3(serviceDiscoveryEvent);
|
|
1903
|
-
const sdBase64Toon = Buffer.from(sdToonBytes).toString("base64");
|
|
1904
|
-
const sdIlpAmount = String(
|
|
1905
|
-
BigInt(sdToonBytes.length) * basePricePerByte
|
|
1906
|
-
);
|
|
1907
|
-
ilpClient.sendIlpPacket({
|
|
1908
|
-
destination: genesisIlpAddress,
|
|
1909
|
-
amount: sdIlpAmount,
|
|
1910
|
-
data: sdBase64Toon
|
|
1911
|
-
}).catch((err) => {
|
|
1912
|
-
const msg = err instanceof Error ? err.message : "Unknown";
|
|
1913
|
-
console.warn(
|
|
1914
|
-
"[Town] Failed to publish service discovery via ILP:",
|
|
1915
|
-
msg
|
|
1916
|
-
);
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
} catch (error) {
|
|
1920
|
-
console.warn("[Town] Failed to publish service discovery:", error);
|
|
1921
|
-
}
|
|
1922
|
-
const bootstrapPeerPubkeys = results.map((r) => r.knownPeer.pubkey);
|
|
1923
|
-
discoveryTracker.addExcludedPubkeys(bootstrapPeerPubkeys);
|
|
1924
|
-
} catch (error) {
|
|
1925
|
-
console.error("[Town] Bootstrap failed:", error);
|
|
1926
|
-
}
|
|
1927
|
-
if (publishSeedEntryFlag && !externalRelayUrl) {
|
|
1928
|
-
console.warn(
|
|
1929
|
-
"[Town] publishSeedEntry is true but externalRelayUrl is not set -- skipping seed relay entry publication"
|
|
1930
|
-
);
|
|
1931
|
-
}
|
|
1932
|
-
if (publishSeedEntryFlag && externalRelayUrl && seedRelays.length > 0) {
|
|
1933
|
-
publishSeedRelayEntry({
|
|
1934
|
-
secretKey: identity.secretKey,
|
|
1935
|
-
relayUrl: externalRelayUrl,
|
|
1936
|
-
publicRelays: seedRelays
|
|
1937
|
-
}).then(({ publishedTo, eventId }) => {
|
|
1938
|
-
console.log(
|
|
1939
|
-
`[Town] Published seed relay entry to ${publishedTo} relay(s), eventId: ${eventId}`
|
|
1940
|
-
);
|
|
1941
|
-
}).catch((err) => {
|
|
1942
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
1943
|
-
console.warn(`[Town] Failed to publish seed relay entry: ${msg}`);
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
const socialDiscovery = new SocialPeerDiscovery(
|
|
1947
|
-
{ relayUrls },
|
|
1948
|
-
identity.secretKey
|
|
1949
|
-
);
|
|
1950
|
-
const socialSubscription = socialDiscovery.start();
|
|
1951
|
-
const activeSubscriptions = /* @__PURE__ */ new Set();
|
|
1952
|
-
const instance = {
|
|
1953
|
-
isRunning() {
|
|
1954
|
-
return running;
|
|
1955
|
-
},
|
|
1956
|
-
subscribe(subscribeRelayUrl, filter) {
|
|
1957
|
-
if (!running) {
|
|
1958
|
-
throw new Error("Cannot subscribe: town is not running");
|
|
1959
|
-
}
|
|
1960
|
-
return createSubscription(
|
|
1961
|
-
subscribeRelayUrl,
|
|
1962
|
-
filter,
|
|
1963
|
-
eventStore,
|
|
1964
|
-
activeSubscriptions
|
|
1965
|
-
);
|
|
1966
|
-
},
|
|
1967
|
-
async stop() {
|
|
1968
|
-
if (!running) return;
|
|
1969
|
-
running = false;
|
|
1970
|
-
if (announcementHeartbeat) {
|
|
1971
|
-
clearInterval(announcementHeartbeat);
|
|
1972
|
-
announcementHeartbeat = void 0;
|
|
1973
|
-
}
|
|
1974
|
-
for (const sub of activeSubscriptions) {
|
|
1975
|
-
sub.close();
|
|
1976
|
-
}
|
|
1977
|
-
activeSubscriptions.clear();
|
|
1978
|
-
if (socialSubscription) {
|
|
1979
|
-
socialSubscription.unsubscribe();
|
|
1980
|
-
}
|
|
1981
|
-
if (seedRelayDiscovery) {
|
|
1982
|
-
await seedRelayDiscovery.close();
|
|
1983
|
-
}
|
|
1984
|
-
await wsRelay.stop();
|
|
1985
|
-
blsServer.close();
|
|
1986
|
-
if (autoCreatedConnector) {
|
|
1987
|
-
await autoCreatedConnector.stop();
|
|
1988
|
-
}
|
|
1989
|
-
eventStore.close?.();
|
|
1990
|
-
},
|
|
1991
|
-
pubkey: identity.pubkey,
|
|
1992
|
-
evmAddress: identity.evmAddress,
|
|
1993
|
-
config: resolvedConfig,
|
|
1994
|
-
bootstrapResult: {
|
|
1995
|
-
peerCount,
|
|
1996
|
-
channelCount
|
|
1997
|
-
},
|
|
1998
|
-
discoveryMode: discovery
|
|
1999
|
-
};
|
|
2000
|
-
return instance;
|
|
2001
|
-
}
|
|
2002
|
-
var startTown = startRelay;
|
|
2003
|
-
|
|
2004
|
-
export {
|
|
2005
|
-
DEFAULT_RELAY_CONFIG,
|
|
2006
|
-
matchFilter,
|
|
2007
|
-
InMemoryEventStore,
|
|
2008
|
-
RelayError,
|
|
2009
|
-
SqliteEventStore,
|
|
2010
|
-
encodeEventToToon,
|
|
2011
|
-
ToonEncodeError,
|
|
2012
|
-
decodeEventFromToon,
|
|
2013
|
-
ToonDecodeError,
|
|
2014
|
-
ConnectionHandler,
|
|
2015
|
-
NostrRelayServer,
|
|
2016
|
-
RelaySubscriber,
|
|
2017
|
-
createEventStorageHandler,
|
|
2018
|
-
calculateX402Price,
|
|
2019
|
-
EIP_3009_TYPES,
|
|
2020
|
-
USDC_EIP712_DOMAIN,
|
|
2021
|
-
USDC_ABI,
|
|
2022
|
-
runPreflight,
|
|
2023
|
-
settleEip3009,
|
|
2024
|
-
createX402Handler,
|
|
2025
|
-
createHealthResponse,
|
|
2026
|
-
startRelay,
|
|
2027
|
-
startTown
|
|
2028
|
-
};
|
|
2029
|
-
//# sourceMappingURL=chunk-ZKWFGHZ7.js.map
|