@toon-protocol/core 1.1.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/README.md +25 -0
- package/dist/chunk-7AHDGE4H.js +189 -0
- package/dist/chunk-7AHDGE4H.js.map +1 -0
- package/dist/index-DSgMeweu.d.ts +104 -0
- package/dist/index.d.ts +2853 -0
- package/dist/index.js +3495 -0
- package/dist/index.js.map +1 -0
- package/dist/nip34/index.d.ts +350 -0
- package/dist/nip34/index.js +562 -0
- package/dist/nip34/index.js.map +1 -0
- package/dist/toon/index.d.ts +2 -0
- package/dist/toon/index.js +17 -0
- package/dist/toon/index.js.map +1 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3495 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidEventError,
|
|
3
|
+
PeerDiscoveryError,
|
|
4
|
+
ToonDecodeError,
|
|
5
|
+
ToonEncodeError,
|
|
6
|
+
ToonError,
|
|
7
|
+
decodeEventFromToon,
|
|
8
|
+
encodeEventToToon,
|
|
9
|
+
encodeEventToToonString,
|
|
10
|
+
shallowParseToon
|
|
11
|
+
} from "./chunk-7AHDGE4H.js";
|
|
12
|
+
|
|
13
|
+
// src/constants.ts
|
|
14
|
+
var ILP_PEER_INFO_KIND = 10032;
|
|
15
|
+
var SERVICE_DISCOVERY_KIND = 10035;
|
|
16
|
+
var SEED_RELAY_LIST_KIND = 10036;
|
|
17
|
+
var TEE_ATTESTATION_KIND = 10033;
|
|
18
|
+
var JOB_REQUEST_KIND_BASE = 5e3;
|
|
19
|
+
var JOB_RESULT_KIND_BASE = 6e3;
|
|
20
|
+
var JOB_FEEDBACK_KIND = 7e3;
|
|
21
|
+
var TEXT_GENERATION_KIND = 5100;
|
|
22
|
+
var IMAGE_GENERATION_KIND = 5200;
|
|
23
|
+
var TEXT_TO_SPEECH_KIND = 5300;
|
|
24
|
+
var TRANSLATION_KIND = 5302;
|
|
25
|
+
|
|
26
|
+
// src/events/parsers.ts
|
|
27
|
+
function validateChainId(chainId) {
|
|
28
|
+
if (!chainId) return false;
|
|
29
|
+
const segments = chainId.split(":");
|
|
30
|
+
if (segments.length < 2 || segments.length > 3) return false;
|
|
31
|
+
return segments.every((s) => s.length > 0);
|
|
32
|
+
}
|
|
33
|
+
function isObject(value) {
|
|
34
|
+
return typeof value === "object" && value !== null;
|
|
35
|
+
}
|
|
36
|
+
function parseIlpPeerInfo(event) {
|
|
37
|
+
if (event.kind !== ILP_PEER_INFO_KIND) {
|
|
38
|
+
throw new InvalidEventError(
|
|
39
|
+
`Expected event kind ${ILP_PEER_INFO_KIND}, got ${event.kind}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(event.content);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new InvalidEventError(
|
|
47
|
+
"Failed to parse event content as JSON",
|
|
48
|
+
err instanceof Error ? err : void 0
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!isObject(parsed)) {
|
|
52
|
+
throw new InvalidEventError("Event content must be a JSON object");
|
|
53
|
+
}
|
|
54
|
+
const {
|
|
55
|
+
ilpAddress,
|
|
56
|
+
btpEndpoint,
|
|
57
|
+
blsHttpEndpoint,
|
|
58
|
+
settlementEngine,
|
|
59
|
+
assetCode,
|
|
60
|
+
assetScale
|
|
61
|
+
} = parsed;
|
|
62
|
+
if (typeof ilpAddress !== "string" || ilpAddress.length === 0) {
|
|
63
|
+
throw new InvalidEventError(
|
|
64
|
+
"Missing or invalid required field: ilpAddress"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (typeof btpEndpoint !== "string" || btpEndpoint.length === 0) {
|
|
68
|
+
throw new InvalidEventError(
|
|
69
|
+
"Missing or invalid required field: btpEndpoint"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (typeof assetCode !== "string" || assetCode.length === 0) {
|
|
73
|
+
throw new InvalidEventError("Missing or invalid required field: assetCode");
|
|
74
|
+
}
|
|
75
|
+
if (typeof assetScale !== "number" || !Number.isInteger(assetScale)) {
|
|
76
|
+
throw new InvalidEventError(
|
|
77
|
+
"Missing or invalid required field: assetScale"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (settlementEngine !== void 0 && typeof settlementEngine !== "string") {
|
|
81
|
+
throw new InvalidEventError(
|
|
82
|
+
"Invalid optional field: settlementEngine must be a string"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const {
|
|
86
|
+
supportedChains,
|
|
87
|
+
settlementAddresses,
|
|
88
|
+
preferredTokens,
|
|
89
|
+
tokenNetworks
|
|
90
|
+
} = parsed;
|
|
91
|
+
if (supportedChains !== void 0) {
|
|
92
|
+
if (!Array.isArray(supportedChains)) {
|
|
93
|
+
throw new InvalidEventError("supportedChains must be an array");
|
|
94
|
+
}
|
|
95
|
+
if (supportedChains.length === 0) {
|
|
96
|
+
throw new InvalidEventError(
|
|
97
|
+
"supportedChains must be a non-empty array when provided"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
for (const chainId of supportedChains) {
|
|
101
|
+
if (typeof chainId !== "string" || !validateChainId(chainId)) {
|
|
102
|
+
throw new InvalidEventError(
|
|
103
|
+
`Invalid chain identifier: ${String(chainId)}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (settlementAddresses !== void 0) {
|
|
109
|
+
if (!isObject(settlementAddresses)) {
|
|
110
|
+
throw new InvalidEventError("settlementAddresses must be an object");
|
|
111
|
+
}
|
|
112
|
+
for (const [key, value] of Object.entries(settlementAddresses)) {
|
|
113
|
+
if (!validateChainId(key)) {
|
|
114
|
+
throw new InvalidEventError(
|
|
115
|
+
`Invalid chain identifier in settlementAddresses: ${key}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
119
|
+
throw new InvalidEventError(
|
|
120
|
+
"settlementAddresses values must be non-empty strings"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(supportedChains)) {
|
|
125
|
+
const chainSet = new Set(supportedChains);
|
|
126
|
+
for (const key of Object.keys(settlementAddresses)) {
|
|
127
|
+
if (!chainSet.has(key)) {
|
|
128
|
+
throw new InvalidEventError(
|
|
129
|
+
`settlementAddresses key '${key}' is not in supportedChains`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (preferredTokens !== void 0) {
|
|
136
|
+
if (!isObject(preferredTokens)) {
|
|
137
|
+
throw new InvalidEventError("preferredTokens must be an object");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (tokenNetworks !== void 0) {
|
|
141
|
+
if (!isObject(tokenNetworks)) {
|
|
142
|
+
throw new InvalidEventError("tokenNetworks must be an object");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ilpAddress,
|
|
147
|
+
btpEndpoint,
|
|
148
|
+
...blsHttpEndpoint !== void 0 && typeof blsHttpEndpoint === "string" && { blsHttpEndpoint },
|
|
149
|
+
assetCode,
|
|
150
|
+
assetScale,
|
|
151
|
+
...settlementEngine !== void 0 && { settlementEngine },
|
|
152
|
+
supportedChains: supportedChains !== void 0 ? supportedChains : [],
|
|
153
|
+
settlementAddresses: settlementAddresses !== void 0 ? settlementAddresses : {},
|
|
154
|
+
...preferredTokens !== void 0 && {
|
|
155
|
+
preferredTokens
|
|
156
|
+
},
|
|
157
|
+
...tokenNetworks !== void 0 && {
|
|
158
|
+
tokenNetworks
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/events/builders.ts
|
|
164
|
+
import { finalizeEvent } from "nostr-tools/pure";
|
|
165
|
+
function buildIlpPeerInfoEvent(info, secretKey) {
|
|
166
|
+
return finalizeEvent(
|
|
167
|
+
{
|
|
168
|
+
kind: ILP_PEER_INFO_KIND,
|
|
169
|
+
content: JSON.stringify(info),
|
|
170
|
+
tags: [],
|
|
171
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
172
|
+
},
|
|
173
|
+
secretKey
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/events/seed-relay.ts
|
|
178
|
+
import { finalizeEvent as finalizeEvent2 } from "nostr-tools/pure";
|
|
179
|
+
var PUBKEY_REGEX = /^[0-9a-f]{64}$/;
|
|
180
|
+
function isValidWsUrl(url) {
|
|
181
|
+
return typeof url === "string" && // nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket -- validation check, not a connection
|
|
182
|
+
(url.startsWith("ws://") || url.startsWith("wss://"));
|
|
183
|
+
}
|
|
184
|
+
function isValidPubkey(pubkey) {
|
|
185
|
+
return typeof pubkey === "string" && PUBKEY_REGEX.test(pubkey);
|
|
186
|
+
}
|
|
187
|
+
function buildSeedRelayListEvent(secretKey, entries) {
|
|
188
|
+
return finalizeEvent2(
|
|
189
|
+
{
|
|
190
|
+
kind: SEED_RELAY_LIST_KIND,
|
|
191
|
+
content: JSON.stringify(entries),
|
|
192
|
+
tags: [["d", "toon-seed-list"]],
|
|
193
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
194
|
+
},
|
|
195
|
+
secretKey
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function parseSeedRelayList(event) {
|
|
199
|
+
let parsed;
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(event.content);
|
|
202
|
+
} catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(parsed)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const results = [];
|
|
209
|
+
for (const item of parsed) {
|
|
210
|
+
if (typeof item !== "object" || item === null) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const record = item;
|
|
214
|
+
const url = record["url"];
|
|
215
|
+
const pubkey = record["pubkey"];
|
|
216
|
+
if (!isValidWsUrl(url)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!isValidPubkey(pubkey)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const entry = { url, pubkey };
|
|
223
|
+
const metadata = record["metadata"];
|
|
224
|
+
if (typeof metadata === "object" && metadata !== null) {
|
|
225
|
+
const meta = metadata;
|
|
226
|
+
const entryMeta = {};
|
|
227
|
+
if (typeof meta["region"] === "string") {
|
|
228
|
+
entryMeta.region = meta["region"];
|
|
229
|
+
}
|
|
230
|
+
if (typeof meta["version"] === "string") {
|
|
231
|
+
entryMeta.version = meta["version"];
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(meta["services"])) {
|
|
234
|
+
entryMeta.services = meta["services"].filter(
|
|
235
|
+
(s) => typeof s === "string"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (Object.keys(entryMeta).length > 0) {
|
|
239
|
+
entry.metadata = entryMeta;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
results.push(entry);
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/events/service-discovery.ts
|
|
248
|
+
import { finalizeEvent as finalizeEvent3 } from "nostr-tools/pure";
|
|
249
|
+
function buildServiceDiscoveryEvent(content, secretKey) {
|
|
250
|
+
return finalizeEvent3(
|
|
251
|
+
{
|
|
252
|
+
kind: SERVICE_DISCOVERY_KIND,
|
|
253
|
+
content: JSON.stringify(content),
|
|
254
|
+
tags: [["d", "toon-service-discovery"]],
|
|
255
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
256
|
+
},
|
|
257
|
+
secretKey
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
function parseServiceDiscovery(event) {
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(event.content);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const record = parsed;
|
|
271
|
+
const serviceType = record["serviceType"];
|
|
272
|
+
if (typeof serviceType !== "string") return null;
|
|
273
|
+
const ilpAddress = record["ilpAddress"];
|
|
274
|
+
if (typeof ilpAddress !== "string") return null;
|
|
275
|
+
const chain = record["chain"];
|
|
276
|
+
if (typeof chain !== "string") return null;
|
|
277
|
+
const version = record["version"];
|
|
278
|
+
if (typeof version !== "string") return null;
|
|
279
|
+
const pricing = record["pricing"];
|
|
280
|
+
if (typeof pricing !== "object" || pricing === null || Array.isArray(pricing))
|
|
281
|
+
return null;
|
|
282
|
+
const pricingRecord = pricing;
|
|
283
|
+
const basePricePerByte = pricingRecord["basePricePerByte"];
|
|
284
|
+
if (typeof basePricePerByte !== "number") return null;
|
|
285
|
+
if (!isFinite(basePricePerByte) || basePricePerByte < 0) return null;
|
|
286
|
+
const currency = pricingRecord["currency"];
|
|
287
|
+
if (typeof currency !== "string") return null;
|
|
288
|
+
const supportedKinds = record["supportedKinds"];
|
|
289
|
+
if (!Array.isArray(supportedKinds)) return null;
|
|
290
|
+
if (!supportedKinds.every(
|
|
291
|
+
(k) => typeof k === "number" && Number.isInteger(k) && k >= 0
|
|
292
|
+
)) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const capabilities = record["capabilities"];
|
|
296
|
+
if (!Array.isArray(capabilities)) return null;
|
|
297
|
+
if (!capabilities.every((c) => typeof c === "string")) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const result = {
|
|
301
|
+
serviceType,
|
|
302
|
+
ilpAddress,
|
|
303
|
+
pricing: { basePricePerByte, currency },
|
|
304
|
+
supportedKinds,
|
|
305
|
+
capabilities,
|
|
306
|
+
chain,
|
|
307
|
+
version
|
|
308
|
+
};
|
|
309
|
+
const x402 = record["x402"];
|
|
310
|
+
if (x402 !== void 0) {
|
|
311
|
+
if (typeof x402 !== "object" || x402 === null || Array.isArray(x402))
|
|
312
|
+
return null;
|
|
313
|
+
const x402Record = x402;
|
|
314
|
+
const enabled = x402Record["enabled"];
|
|
315
|
+
if (typeof enabled !== "boolean") return null;
|
|
316
|
+
const x402Result = { enabled };
|
|
317
|
+
const endpoint = x402Record["endpoint"];
|
|
318
|
+
if (endpoint !== void 0) {
|
|
319
|
+
if (typeof endpoint !== "string") return null;
|
|
320
|
+
x402Result.endpoint = endpoint;
|
|
321
|
+
}
|
|
322
|
+
result.x402 = x402Result;
|
|
323
|
+
}
|
|
324
|
+
const skill = record["skill"];
|
|
325
|
+
if (skill !== void 0) {
|
|
326
|
+
if (typeof skill !== "object" || skill === null || Array.isArray(skill))
|
|
327
|
+
return null;
|
|
328
|
+
const skillRecord = skill;
|
|
329
|
+
const skillName = skillRecord["name"];
|
|
330
|
+
if (typeof skillName !== "string") return null;
|
|
331
|
+
const skillVersion = skillRecord["version"];
|
|
332
|
+
if (typeof skillVersion !== "string") return null;
|
|
333
|
+
const kinds = skillRecord["kinds"];
|
|
334
|
+
if (!Array.isArray(kinds)) return null;
|
|
335
|
+
if (!kinds.every(
|
|
336
|
+
(k) => typeof k === "number" && Number.isInteger(k) && k >= 0
|
|
337
|
+
)) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const features = skillRecord["features"];
|
|
341
|
+
if (!Array.isArray(features)) return null;
|
|
342
|
+
if (!features.every((f) => typeof f === "string")) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const inputSchema = skillRecord["inputSchema"];
|
|
346
|
+
if (typeof inputSchema !== "object" || inputSchema === null || Array.isArray(inputSchema))
|
|
347
|
+
return null;
|
|
348
|
+
const skillPricing = skillRecord["pricing"];
|
|
349
|
+
if (typeof skillPricing !== "object" || skillPricing === null || Array.isArray(skillPricing))
|
|
350
|
+
return null;
|
|
351
|
+
const pricingEntries = Object.entries(
|
|
352
|
+
skillPricing
|
|
353
|
+
);
|
|
354
|
+
if (!pricingEntries.every(([, v]) => typeof v === "string")) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const skillResult = {
|
|
358
|
+
name: skillName,
|
|
359
|
+
version: skillVersion,
|
|
360
|
+
kinds,
|
|
361
|
+
features,
|
|
362
|
+
inputSchema,
|
|
363
|
+
pricing: skillPricing
|
|
364
|
+
};
|
|
365
|
+
const models = skillRecord["models"];
|
|
366
|
+
if (models !== void 0) {
|
|
367
|
+
if (!Array.isArray(models)) return null;
|
|
368
|
+
if (!models.every((m) => typeof m === "string")) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
skillResult.models = models;
|
|
372
|
+
}
|
|
373
|
+
const attestation = skillRecord["attestation"];
|
|
374
|
+
if (attestation !== void 0) {
|
|
375
|
+
if (typeof attestation !== "object" || attestation === null || Array.isArray(attestation))
|
|
376
|
+
return null;
|
|
377
|
+
skillResult.attestation = attestation;
|
|
378
|
+
}
|
|
379
|
+
result.skill = skillResult;
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/events/attestation.ts
|
|
385
|
+
import { finalizeEvent as finalizeEvent4 } from "nostr-tools/pure";
|
|
386
|
+
var PCR_REGEX = /^[0-9a-f]{96}$/;
|
|
387
|
+
var BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
388
|
+
function buildAttestationEvent(attestation, secretKey, options) {
|
|
389
|
+
return finalizeEvent4(
|
|
390
|
+
{
|
|
391
|
+
kind: TEE_ATTESTATION_KIND,
|
|
392
|
+
content: JSON.stringify(attestation),
|
|
393
|
+
tags: [
|
|
394
|
+
["relay", options.relay],
|
|
395
|
+
["chain", options.chain],
|
|
396
|
+
["expiry", String(options.expiry)]
|
|
397
|
+
],
|
|
398
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
399
|
+
},
|
|
400
|
+
secretKey
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
function parseAttestation(event, options) {
|
|
404
|
+
let parsed;
|
|
405
|
+
try {
|
|
406
|
+
parsed = JSON.parse(event.content);
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const record = parsed;
|
|
414
|
+
const enclave = record["enclave"];
|
|
415
|
+
if (typeof enclave !== "string") return null;
|
|
416
|
+
const pcr0 = record["pcr0"];
|
|
417
|
+
if (typeof pcr0 !== "string") return null;
|
|
418
|
+
const pcr1 = record["pcr1"];
|
|
419
|
+
if (typeof pcr1 !== "string") return null;
|
|
420
|
+
const pcr2 = record["pcr2"];
|
|
421
|
+
if (typeof pcr2 !== "string") return null;
|
|
422
|
+
const attestationDoc = record["attestationDoc"];
|
|
423
|
+
if (typeof attestationDoc !== "string") return null;
|
|
424
|
+
const version = record["version"];
|
|
425
|
+
if (typeof version !== "string") return null;
|
|
426
|
+
if (options?.verify) {
|
|
427
|
+
if (!PCR_REGEX.test(pcr0)) {
|
|
428
|
+
throw new Error("Invalid PCR0 format: expected 96-char lowercase hex");
|
|
429
|
+
}
|
|
430
|
+
if (!PCR_REGEX.test(pcr1)) {
|
|
431
|
+
throw new Error("Invalid PCR1 format: expected 96-char lowercase hex");
|
|
432
|
+
}
|
|
433
|
+
if (!PCR_REGEX.test(pcr2)) {
|
|
434
|
+
throw new Error("Invalid PCR2 format: expected 96-char lowercase hex");
|
|
435
|
+
}
|
|
436
|
+
if (attestationDoc.length === 0) {
|
|
437
|
+
throw new Error("Attestation document is empty");
|
|
438
|
+
}
|
|
439
|
+
if (!BASE64_REGEX.test(attestationDoc)) {
|
|
440
|
+
throw new Error("Attestation document is not valid base64");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const relayTag = event.tags.find((t) => t[0] === "relay");
|
|
444
|
+
const chainTag = event.tags.find((t) => t[0] === "chain");
|
|
445
|
+
const expiryTag = event.tags.find((t) => t[0] === "expiry");
|
|
446
|
+
if (!relayTag?.[1] || !chainTag?.[1] || !expiryTag?.[1]) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const expiry = parseInt(expiryTag[1], 10);
|
|
450
|
+
if (isNaN(expiry)) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
attestation: {
|
|
455
|
+
enclave,
|
|
456
|
+
pcr0,
|
|
457
|
+
pcr1,
|
|
458
|
+
pcr2,
|
|
459
|
+
attestationDoc,
|
|
460
|
+
version
|
|
461
|
+
},
|
|
462
|
+
relay: relayTag[1],
|
|
463
|
+
chain: chainTag[1],
|
|
464
|
+
expiry
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/events/dvm.ts
|
|
469
|
+
import { finalizeEvent as finalizeEvent5 } from "nostr-tools/pure";
|
|
470
|
+
var HEX_64_REGEX = /^[0-9a-f]{64}$/;
|
|
471
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
472
|
+
"processing",
|
|
473
|
+
"error",
|
|
474
|
+
"success",
|
|
475
|
+
"partial"
|
|
476
|
+
]);
|
|
477
|
+
function buildJobRequestEvent(params, secretKey) {
|
|
478
|
+
if (params.kind < 5e3 || params.kind > 5999) {
|
|
479
|
+
throw new ToonError(
|
|
480
|
+
`Job request kind must be in range 5000-5999, got ${params.kind}`,
|
|
481
|
+
"DVM_INVALID_KIND"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (params.input.data === void 0 || params.input.data === null) {
|
|
485
|
+
throw new ToonError(
|
|
486
|
+
"Job request input data is required",
|
|
487
|
+
"DVM_MISSING_INPUT"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (!params.input.type) {
|
|
491
|
+
throw new ToonError(
|
|
492
|
+
"Job request input type is required",
|
|
493
|
+
"DVM_MISSING_INPUT"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (typeof params.bid !== "string" || params.bid === "") {
|
|
497
|
+
throw new ToonError(
|
|
498
|
+
"Job request bid must be a non-empty string (USDC micro-units)",
|
|
499
|
+
"DVM_INVALID_BID"
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (typeof params.output !== "string" || params.output === "") {
|
|
503
|
+
throw new ToonError(
|
|
504
|
+
"Job request output MIME type must be a non-empty string",
|
|
505
|
+
"DVM_MISSING_OUTPUT"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const tags = [];
|
|
509
|
+
const iTag = ["i", params.input.data, params.input.type];
|
|
510
|
+
if (params.input.relay !== void 0) {
|
|
511
|
+
iTag.push(params.input.relay);
|
|
512
|
+
}
|
|
513
|
+
if (params.input.marker !== void 0) {
|
|
514
|
+
if (params.input.relay === void 0) {
|
|
515
|
+
iTag.push("");
|
|
516
|
+
}
|
|
517
|
+
iTag.push(params.input.marker);
|
|
518
|
+
}
|
|
519
|
+
tags.push(iTag);
|
|
520
|
+
tags.push(["bid", params.bid, "usdc"]);
|
|
521
|
+
tags.push(["output", params.output]);
|
|
522
|
+
if (params.targetProvider !== void 0) {
|
|
523
|
+
if (!HEX_64_REGEX.test(params.targetProvider)) {
|
|
524
|
+
throw new ToonError(
|
|
525
|
+
"Job request targetProvider must be a 64-character lowercase hex string",
|
|
526
|
+
"DVM_INVALID_PUBKEY"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
tags.push(["p", params.targetProvider]);
|
|
530
|
+
}
|
|
531
|
+
if (params.params !== void 0) {
|
|
532
|
+
for (const p of params.params) {
|
|
533
|
+
tags.push(["param", p.key, p.value]);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (params.relays !== void 0 && params.relays.length > 0) {
|
|
537
|
+
tags.push(["relays", ...params.relays]);
|
|
538
|
+
}
|
|
539
|
+
return finalizeEvent5(
|
|
540
|
+
{
|
|
541
|
+
kind: params.kind,
|
|
542
|
+
content: params.content ?? "",
|
|
543
|
+
tags,
|
|
544
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
545
|
+
},
|
|
546
|
+
secretKey
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
function buildJobResultEvent(params, secretKey) {
|
|
550
|
+
if (params.kind < 6e3 || params.kind > 6999) {
|
|
551
|
+
throw new ToonError(
|
|
552
|
+
`Job result kind must be in range 6000-6999, got ${params.kind}`,
|
|
553
|
+
"DVM_INVALID_KIND"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (!HEX_64_REGEX.test(params.requestEventId)) {
|
|
557
|
+
throw new ToonError(
|
|
558
|
+
"Job result requestEventId must be a 64-character lowercase hex string",
|
|
559
|
+
"DVM_INVALID_EVENT_ID"
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (!HEX_64_REGEX.test(params.customerPubkey)) {
|
|
563
|
+
throw new ToonError(
|
|
564
|
+
"Job result customerPubkey must be a 64-character lowercase hex string",
|
|
565
|
+
"DVM_INVALID_PUBKEY"
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (typeof params.amount !== "string" || params.amount === "") {
|
|
569
|
+
throw new ToonError(
|
|
570
|
+
"Job result amount must be a non-empty string (USDC micro-units)",
|
|
571
|
+
"DVM_INVALID_AMOUNT"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (typeof params.content !== "string") {
|
|
575
|
+
throw new ToonError(
|
|
576
|
+
"Job result content must be a string",
|
|
577
|
+
"DVM_MISSING_CONTENT"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
const tags = [
|
|
581
|
+
["e", params.requestEventId],
|
|
582
|
+
["p", params.customerPubkey],
|
|
583
|
+
["amount", params.amount, "usdc"]
|
|
584
|
+
];
|
|
585
|
+
return finalizeEvent5(
|
|
586
|
+
{
|
|
587
|
+
kind: params.kind,
|
|
588
|
+
content: params.content,
|
|
589
|
+
tags,
|
|
590
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
591
|
+
},
|
|
592
|
+
secretKey
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
function buildJobFeedbackEvent(params, secretKey) {
|
|
596
|
+
if (!HEX_64_REGEX.test(params.requestEventId)) {
|
|
597
|
+
throw new ToonError(
|
|
598
|
+
"Job feedback requestEventId must be a 64-character lowercase hex string",
|
|
599
|
+
"DVM_INVALID_EVENT_ID"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (!HEX_64_REGEX.test(params.customerPubkey)) {
|
|
603
|
+
throw new ToonError(
|
|
604
|
+
"Job feedback customerPubkey must be a 64-character lowercase hex string",
|
|
605
|
+
"DVM_INVALID_PUBKEY"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (!VALID_STATUSES.has(params.status)) {
|
|
609
|
+
throw new ToonError(
|
|
610
|
+
`Job feedback status must be one of: processing, error, success, partial. Got: ${String(params.status)}`,
|
|
611
|
+
"DVM_INVALID_STATUS"
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const tags = [
|
|
615
|
+
["e", params.requestEventId],
|
|
616
|
+
["p", params.customerPubkey],
|
|
617
|
+
["status", params.status]
|
|
618
|
+
];
|
|
619
|
+
return finalizeEvent5(
|
|
620
|
+
{
|
|
621
|
+
kind: JOB_FEEDBACK_KIND,
|
|
622
|
+
content: params.content ?? "",
|
|
623
|
+
tags,
|
|
624
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
625
|
+
},
|
|
626
|
+
secretKey
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
function parseJobRequest(event) {
|
|
630
|
+
if (event.kind < 5e3 || event.kind > 5999) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const iTag = event.tags.find((t) => t[0] === "i");
|
|
634
|
+
if (!iTag) return null;
|
|
635
|
+
const inputData = iTag[1];
|
|
636
|
+
const inputType = iTag[2];
|
|
637
|
+
if (inputData === void 0 || inputType === void 0) return null;
|
|
638
|
+
const bidTag = event.tags.find((t) => t[0] === "bid");
|
|
639
|
+
if (!bidTag) return null;
|
|
640
|
+
const bidAmount = bidTag[1];
|
|
641
|
+
if (bidAmount === void 0 || bidAmount === "") return null;
|
|
642
|
+
const outputTag = event.tags.find((t) => t[0] === "output");
|
|
643
|
+
if (!outputTag) return null;
|
|
644
|
+
const outputMime = outputTag[1];
|
|
645
|
+
if (outputMime === void 0 || outputMime === "") return null;
|
|
646
|
+
const input = {
|
|
647
|
+
data: inputData,
|
|
648
|
+
type: inputType
|
|
649
|
+
};
|
|
650
|
+
const inputRelay = iTag[3];
|
|
651
|
+
if (inputRelay !== void 0 && inputRelay !== "") {
|
|
652
|
+
input.relay = inputRelay;
|
|
653
|
+
}
|
|
654
|
+
const inputMarker = iTag[4];
|
|
655
|
+
if (inputMarker !== void 0 && inputMarker !== "") {
|
|
656
|
+
input.marker = inputMarker;
|
|
657
|
+
}
|
|
658
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
659
|
+
const targetProvider = pTag?.[1];
|
|
660
|
+
if (targetProvider !== void 0 && !HEX_64_REGEX.test(targetProvider))
|
|
661
|
+
return null;
|
|
662
|
+
const paramTags = event.tags.filter((t) => t[0] === "param");
|
|
663
|
+
const params = [];
|
|
664
|
+
for (const pt of paramTags) {
|
|
665
|
+
const key = pt[1];
|
|
666
|
+
const value = pt[2];
|
|
667
|
+
if (key !== void 0 && value !== void 0) {
|
|
668
|
+
params.push({ key, value });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const relaysTag = event.tags.find((t) => t[0] === "relays");
|
|
672
|
+
const relays = [];
|
|
673
|
+
if (relaysTag) {
|
|
674
|
+
for (let i = 1; i < relaysTag.length; i++) {
|
|
675
|
+
const url = relaysTag[i];
|
|
676
|
+
if (url !== void 0) {
|
|
677
|
+
relays.push(url);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const result = {
|
|
682
|
+
kind: event.kind,
|
|
683
|
+
input,
|
|
684
|
+
bid: bidAmount,
|
|
685
|
+
output: outputMime,
|
|
686
|
+
content: event.content,
|
|
687
|
+
params,
|
|
688
|
+
relays
|
|
689
|
+
};
|
|
690
|
+
if (targetProvider !== void 0) {
|
|
691
|
+
result.targetProvider = targetProvider;
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
function parseJobResult(event) {
|
|
696
|
+
if (event.kind < 6e3 || event.kind > 6999) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
700
|
+
if (!eTag) return null;
|
|
701
|
+
const requestEventId = eTag[1];
|
|
702
|
+
if (requestEventId === void 0 || !HEX_64_REGEX.test(requestEventId))
|
|
703
|
+
return null;
|
|
704
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
705
|
+
if (!pTag) return null;
|
|
706
|
+
const customerPubkey = pTag[1];
|
|
707
|
+
if (customerPubkey === void 0 || !HEX_64_REGEX.test(customerPubkey))
|
|
708
|
+
return null;
|
|
709
|
+
const amountTag = event.tags.find((t) => t[0] === "amount");
|
|
710
|
+
if (!amountTag) return null;
|
|
711
|
+
const amount = amountTag[1];
|
|
712
|
+
if (amount === void 0 || amount === "") return null;
|
|
713
|
+
return {
|
|
714
|
+
kind: event.kind,
|
|
715
|
+
requestEventId,
|
|
716
|
+
customerPubkey,
|
|
717
|
+
amount,
|
|
718
|
+
content: event.content
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function parseJobFeedback(event) {
|
|
722
|
+
if (event.kind !== JOB_FEEDBACK_KIND) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
726
|
+
if (!eTag) return null;
|
|
727
|
+
const requestEventId = eTag[1];
|
|
728
|
+
if (requestEventId === void 0 || !HEX_64_REGEX.test(requestEventId))
|
|
729
|
+
return null;
|
|
730
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
731
|
+
if (!pTag) return null;
|
|
732
|
+
const customerPubkey = pTag[1];
|
|
733
|
+
if (customerPubkey === void 0 || !HEX_64_REGEX.test(customerPubkey))
|
|
734
|
+
return null;
|
|
735
|
+
const statusTag = event.tags.find((t) => t[0] === "status");
|
|
736
|
+
if (!statusTag) return null;
|
|
737
|
+
const statusValue = statusTag[1];
|
|
738
|
+
if (statusValue === void 0) return null;
|
|
739
|
+
if (!VALID_STATUSES.has(statusValue)) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
requestEventId,
|
|
744
|
+
customerPubkey,
|
|
745
|
+
status: statusValue,
|
|
746
|
+
content: event.content
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/discovery/NostrPeerDiscovery.ts
|
|
751
|
+
import { SimplePool } from "nostr-tools/pool";
|
|
752
|
+
var PUBKEY_REGEX2 = /^[0-9a-f]{64}$/;
|
|
753
|
+
var NostrPeerDiscovery = class {
|
|
754
|
+
relayUrls;
|
|
755
|
+
pool;
|
|
756
|
+
/**
|
|
757
|
+
* Creates a new NostrPeerDiscovery instance.
|
|
758
|
+
*
|
|
759
|
+
* @param relayUrls - Array of relay WebSocket URLs to query
|
|
760
|
+
* @param pool - Optional SimplePool instance (creates new one if not provided)
|
|
761
|
+
*/
|
|
762
|
+
constructor(relayUrls, pool) {
|
|
763
|
+
this.relayUrls = relayUrls;
|
|
764
|
+
this.pool = pool ?? new SimplePool();
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Retrieves the list of pubkeys that a given pubkey follows.
|
|
768
|
+
*
|
|
769
|
+
* Queries NIP-02 kind:3 events from configured relays and returns
|
|
770
|
+
* the followed pubkeys from the most recent event.
|
|
771
|
+
*
|
|
772
|
+
* @param pubkey - The 64-character hex pubkey to get follows for
|
|
773
|
+
* @returns Array of followed pubkeys (deduplicated)
|
|
774
|
+
* @throws PeerDiscoveryError if pubkey format is invalid
|
|
775
|
+
*/
|
|
776
|
+
async getFollows(pubkey) {
|
|
777
|
+
if (!PUBKEY_REGEX2.test(pubkey)) {
|
|
778
|
+
throw new PeerDiscoveryError(
|
|
779
|
+
`Invalid pubkey format: must be 64-character lowercase hex string`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const filter = {
|
|
783
|
+
kinds: [3],
|
|
784
|
+
authors: [pubkey],
|
|
785
|
+
limit: 1
|
|
786
|
+
};
|
|
787
|
+
try {
|
|
788
|
+
const events = await this.pool.querySync(this.relayUrls, filter);
|
|
789
|
+
if (events.length === 0) {
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
792
|
+
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at);
|
|
793
|
+
const mostRecent = sortedEvents[0];
|
|
794
|
+
if (!mostRecent) {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
const pubkeys = mostRecent.tags.filter(
|
|
798
|
+
(tag) => tag[0] === "p" && typeof tag[1] === "string"
|
|
799
|
+
).map((tag) => tag[1]);
|
|
800
|
+
return [...new Set(pubkeys)];
|
|
801
|
+
} catch (error) {
|
|
802
|
+
throw new PeerDiscoveryError(
|
|
803
|
+
"Failed to query relays for follow list",
|
|
804
|
+
error instanceof Error ? error : void 0
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Discovers ILP peers by querying follow list and their ILP peer info events.
|
|
810
|
+
*
|
|
811
|
+
* For each followed pubkey, queries kind:10032 events to retrieve ILP connection info.
|
|
812
|
+
* Peers without kind:10032 events or with malformed events are silently excluded.
|
|
813
|
+
*
|
|
814
|
+
* @param pubkey - The 64-character hex pubkey to discover peers for
|
|
815
|
+
* @returns Map of pubkey → IlpPeerInfo for peers with valid ILP info
|
|
816
|
+
* @throws PeerDiscoveryError if pubkey format is invalid
|
|
817
|
+
*/
|
|
818
|
+
async discoverPeers(pubkey) {
|
|
819
|
+
if (!PUBKEY_REGEX2.test(pubkey)) {
|
|
820
|
+
throw new PeerDiscoveryError(
|
|
821
|
+
`Invalid pubkey format: must be 64-character lowercase hex string`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
const follows = await this.getFollows(pubkey);
|
|
825
|
+
if (follows.length === 0) {
|
|
826
|
+
return /* @__PURE__ */ new Map();
|
|
827
|
+
}
|
|
828
|
+
const filter = {
|
|
829
|
+
kinds: [ILP_PEER_INFO_KIND],
|
|
830
|
+
authors: follows
|
|
831
|
+
};
|
|
832
|
+
const events = await this.pool.querySync(this.relayUrls, filter);
|
|
833
|
+
const eventsByPubkey = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const event of events) {
|
|
835
|
+
const existing = eventsByPubkey.get(event.pubkey);
|
|
836
|
+
if (!existing || event.created_at > existing.created_at) {
|
|
837
|
+
eventsByPubkey.set(event.pubkey, event);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const result = /* @__PURE__ */ new Map();
|
|
841
|
+
for (const [peerPubkey, event] of eventsByPubkey) {
|
|
842
|
+
try {
|
|
843
|
+
const info = parseIlpPeerInfo(event);
|
|
844
|
+
result.set(peerPubkey, info);
|
|
845
|
+
} catch {
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Subscribes to ILP peer info updates from followed pubkeys.
|
|
852
|
+
*
|
|
853
|
+
* Sets up a real-time subscription for kind:10032 events from all pubkeys
|
|
854
|
+
* that the given pubkey follows. The callback is invoked whenever a new
|
|
855
|
+
* or updated ILP peer info event is received.
|
|
856
|
+
*
|
|
857
|
+
* @param pubkey - The 64-character hex pubkey whose follows to monitor
|
|
858
|
+
* @param callback - Function called with (peerPubkey, parsedInfo) for each update
|
|
859
|
+
* @returns Subscription object with unsubscribe() method
|
|
860
|
+
* @throws PeerDiscoveryError if pubkey format is invalid
|
|
861
|
+
*/
|
|
862
|
+
async subscribeToPeerUpdates(pubkey, callback) {
|
|
863
|
+
if (!PUBKEY_REGEX2.test(pubkey)) {
|
|
864
|
+
throw new PeerDiscoveryError(
|
|
865
|
+
`Invalid pubkey format: must be 64-character lowercase hex string`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
const follows = await this.getFollows(pubkey);
|
|
869
|
+
if (follows.length === 0) {
|
|
870
|
+
return {
|
|
871
|
+
unsubscribe: () => {
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const peerTimestamps = /* @__PURE__ */ new Map();
|
|
876
|
+
let isUnsubscribed = false;
|
|
877
|
+
const filter = {
|
|
878
|
+
kinds: [ILP_PEER_INFO_KIND],
|
|
879
|
+
authors: follows
|
|
880
|
+
};
|
|
881
|
+
const subCloser = this.pool.subscribeMany(this.relayUrls, filter, {
|
|
882
|
+
onevent: (event) => {
|
|
883
|
+
if (isUnsubscribed) return;
|
|
884
|
+
const lastSeen = peerTimestamps.get(event.pubkey) ?? 0;
|
|
885
|
+
if (event.created_at <= lastSeen) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
peerTimestamps.set(event.pubkey, event.created_at);
|
|
889
|
+
try {
|
|
890
|
+
const info = parseIlpPeerInfo(event);
|
|
891
|
+
callback(event.pubkey, info);
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
return {
|
|
897
|
+
unsubscribe: () => {
|
|
898
|
+
if (!isUnsubscribed) {
|
|
899
|
+
isUnsubscribed = true;
|
|
900
|
+
subCloser.close();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/discovery/genesis-peers.json
|
|
908
|
+
var genesis_peers_default = [];
|
|
909
|
+
|
|
910
|
+
// src/discovery/GenesisPeerLoader.ts
|
|
911
|
+
var PUBKEY_REGEX3 = /^[0-9a-f]{64}$/;
|
|
912
|
+
var ILP_ADDRESS_REGEX = /^g\.[a-zA-Z0-9.-]+$/;
|
|
913
|
+
function isValidPubkey2(pubkey) {
|
|
914
|
+
return PUBKEY_REGEX3.test(pubkey);
|
|
915
|
+
}
|
|
916
|
+
function isValidRelayUrl(url) {
|
|
917
|
+
return url.startsWith("wss://") || url.startsWith("ws://");
|
|
918
|
+
}
|
|
919
|
+
function isValidIlpAddress(address) {
|
|
920
|
+
return ILP_ADDRESS_REGEX.test(address);
|
|
921
|
+
}
|
|
922
|
+
function isValidBtpEndpoint(url) {
|
|
923
|
+
return url.startsWith("wss://") || url.startsWith("ws://");
|
|
924
|
+
}
|
|
925
|
+
function isValidGenesisPeer(entry) {
|
|
926
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
927
|
+
const obj = entry;
|
|
928
|
+
return typeof obj["pubkey"] === "string" && typeof obj["relayUrl"] === "string" && typeof obj["ilpAddress"] === "string" && typeof obj["btpEndpoint"] === "string" && isValidPubkey2(obj["pubkey"]) && isValidRelayUrl(obj["relayUrl"]) && isValidIlpAddress(obj["ilpAddress"]) && isValidBtpEndpoint(obj["btpEndpoint"]);
|
|
929
|
+
}
|
|
930
|
+
function deduplicateByPubkey(peers) {
|
|
931
|
+
const map = /* @__PURE__ */ new Map();
|
|
932
|
+
for (const peer of peers) {
|
|
933
|
+
map.set(peer.pubkey, peer);
|
|
934
|
+
}
|
|
935
|
+
return [...map.values()];
|
|
936
|
+
}
|
|
937
|
+
function loadGenesisPeers() {
|
|
938
|
+
const raw = genesis_peers_default;
|
|
939
|
+
const valid = [];
|
|
940
|
+
for (const entry of raw) {
|
|
941
|
+
if (isValidGenesisPeer(entry)) {
|
|
942
|
+
valid.push(entry);
|
|
943
|
+
} else {
|
|
944
|
+
console.warn("Skipping invalid genesis peer entry:", entry);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return deduplicateByPubkey(valid);
|
|
948
|
+
}
|
|
949
|
+
function loadAdditionalPeers(json) {
|
|
950
|
+
let parsed;
|
|
951
|
+
try {
|
|
952
|
+
parsed = JSON.parse(json);
|
|
953
|
+
} catch {
|
|
954
|
+
console.warn("Failed to parse additional peers JSON:", json);
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
if (!Array.isArray(parsed)) {
|
|
958
|
+
console.warn("Additional peers JSON is not an array");
|
|
959
|
+
return [];
|
|
960
|
+
}
|
|
961
|
+
const valid = [];
|
|
962
|
+
for (const entry of parsed) {
|
|
963
|
+
if (isValidGenesisPeer(entry)) {
|
|
964
|
+
valid.push(entry);
|
|
965
|
+
} else {
|
|
966
|
+
console.warn("Skipping invalid additional peer entry:", entry);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return valid;
|
|
970
|
+
}
|
|
971
|
+
function loadAllPeers(additionalPeersJson) {
|
|
972
|
+
const genesis = loadGenesisPeers();
|
|
973
|
+
if (!additionalPeersJson) {
|
|
974
|
+
return genesis;
|
|
975
|
+
}
|
|
976
|
+
const additional = loadAdditionalPeers(additionalPeersJson);
|
|
977
|
+
return deduplicateByPubkey([...genesis, ...additional]);
|
|
978
|
+
}
|
|
979
|
+
var GenesisPeerLoader = {
|
|
980
|
+
loadGenesisPeers,
|
|
981
|
+
loadAdditionalPeers,
|
|
982
|
+
loadAllPeers
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// src/discovery/ArDrivePeerRegistry.ts
|
|
986
|
+
var DEFAULT_GATEWAY_URL = "https://arweave.net/graphql";
|
|
987
|
+
var GRAPHQL_QUERY = `
|
|
988
|
+
query {
|
|
989
|
+
transactions(
|
|
990
|
+
tags: [
|
|
991
|
+
{ name: "App-Name", values: ["toon"] },
|
|
992
|
+
{ name: "type", values: ["ilp-peer-info"] }
|
|
993
|
+
],
|
|
994
|
+
first: 100
|
|
995
|
+
) {
|
|
996
|
+
edges {
|
|
997
|
+
node {
|
|
998
|
+
id
|
|
999
|
+
tags {
|
|
1000
|
+
name
|
|
1001
|
+
value
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
`;
|
|
1008
|
+
function isValidIlpPeerInfo(entry) {
|
|
1009
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
1010
|
+
const obj = entry;
|
|
1011
|
+
return typeof obj["ilpAddress"] === "string" && isValidIlpAddress(obj["ilpAddress"]) && typeof obj["btpEndpoint"] === "string" && isValidBtpEndpoint(obj["btpEndpoint"]) && typeof obj["assetCode"] === "string" && obj["assetCode"].length > 0 && typeof obj["assetScale"] === "number" && Number.isInteger(obj["assetScale"]) && obj["assetScale"] >= 0 && (obj["settlementEngine"] === void 0 || typeof obj["settlementEngine"] === "string");
|
|
1012
|
+
}
|
|
1013
|
+
function getPubkeyFromTags(tags) {
|
|
1014
|
+
const tag = tags.find((t) => t.name === "pubkey");
|
|
1015
|
+
return tag?.value;
|
|
1016
|
+
}
|
|
1017
|
+
async function fetchPeers(gatewayUrl = DEFAULT_GATEWAY_URL) {
|
|
1018
|
+
const result = /* @__PURE__ */ new Map();
|
|
1019
|
+
try {
|
|
1020
|
+
const response = await fetch(gatewayUrl, {
|
|
1021
|
+
method: "POST",
|
|
1022
|
+
headers: { "Content-Type": "application/json" },
|
|
1023
|
+
body: JSON.stringify({ query: GRAPHQL_QUERY })
|
|
1024
|
+
});
|
|
1025
|
+
const json = await response.json();
|
|
1026
|
+
const edges = json?.data?.transactions?.edges;
|
|
1027
|
+
if (!Array.isArray(edges)) {
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
const baseUrl = gatewayUrl.replace(/\/graphql$/, "");
|
|
1031
|
+
for (const edge of edges) {
|
|
1032
|
+
const txId = edge?.node?.id;
|
|
1033
|
+
const tags = edge?.node?.tags;
|
|
1034
|
+
if (!txId || !Array.isArray(tags)) continue;
|
|
1035
|
+
const pubkey = getPubkeyFromTags(tags);
|
|
1036
|
+
if (!pubkey || !isValidPubkey2(pubkey)) continue;
|
|
1037
|
+
if (result.has(pubkey)) continue;
|
|
1038
|
+
try {
|
|
1039
|
+
const dataResponse = await fetch(`${baseUrl}/${txId}`);
|
|
1040
|
+
const data = await dataResponse.json();
|
|
1041
|
+
if (isValidIlpPeerInfo(data)) {
|
|
1042
|
+
result.set(pubkey, data);
|
|
1043
|
+
} else {
|
|
1044
|
+
console.warn(
|
|
1045
|
+
`Skipping transaction ${txId}: invalid IlpPeerInfo data`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
console.warn(`Failed to fetch transaction data for ${txId}:`, err);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
console.warn("ArDrive peer registry unavailable:", err);
|
|
1054
|
+
}
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|
|
1057
|
+
async function publishPeerInfo(peerInfo, pubkey, turboClient) {
|
|
1058
|
+
if (!isValidPubkey2(pubkey)) {
|
|
1059
|
+
throw new PeerDiscoveryError(`Invalid pubkey: ${pubkey}`);
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const json = JSON.stringify(peerInfo);
|
|
1063
|
+
const result = await turboClient.uploadFile({
|
|
1064
|
+
fileStreamFactory: () => Buffer.from(json, "utf-8"),
|
|
1065
|
+
fileSizeFactory: () => Buffer.byteLength(json, "utf-8"),
|
|
1066
|
+
dataItemOpts: {
|
|
1067
|
+
tags: [
|
|
1068
|
+
{ name: "App-Name", value: "toon" },
|
|
1069
|
+
{ name: "type", value: "ilp-peer-info" },
|
|
1070
|
+
{ name: "pubkey", value: pubkey },
|
|
1071
|
+
{ name: "version", value: "1" },
|
|
1072
|
+
{ name: "Content-Type", value: "application/json" }
|
|
1073
|
+
]
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
return result.id;
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
throw new PeerDiscoveryError(
|
|
1079
|
+
"Failed to publish peer info to ArDrive",
|
|
1080
|
+
err instanceof Error ? err : void 0
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
var ArDrivePeerRegistry = {
|
|
1085
|
+
fetchPeers,
|
|
1086
|
+
publishPeerInfo
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
// src/discovery/SocialPeerDiscovery.ts
|
|
1090
|
+
import { SimplePool as SimplePool2 } from "nostr-tools/pool";
|
|
1091
|
+
import { getPublicKey } from "nostr-tools/pure";
|
|
1092
|
+
var SocialPeerDiscovery = class {
|
|
1093
|
+
config;
|
|
1094
|
+
pubkey;
|
|
1095
|
+
pool;
|
|
1096
|
+
listeners = [];
|
|
1097
|
+
previousFollows = /* @__PURE__ */ new Set();
|
|
1098
|
+
started = false;
|
|
1099
|
+
/**
|
|
1100
|
+
* Creates a new SocialPeerDiscovery instance.
|
|
1101
|
+
*
|
|
1102
|
+
* @param config - Discovery configuration
|
|
1103
|
+
* @param secretKey - Our Nostr secret key (used to derive pubkey)
|
|
1104
|
+
* @param pool - Optional SimplePool instance (creates new one if not provided)
|
|
1105
|
+
*/
|
|
1106
|
+
constructor(config, secretKey, pool) {
|
|
1107
|
+
this.config = config;
|
|
1108
|
+
this.pubkey = getPublicKey(secretKey);
|
|
1109
|
+
this.pool = pool ?? new SimplePool2();
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Register a listener for social discovery events.
|
|
1113
|
+
*/
|
|
1114
|
+
on(listener) {
|
|
1115
|
+
this.listeners.push(listener);
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Remove a previously registered listener.
|
|
1119
|
+
*/
|
|
1120
|
+
off(listener) {
|
|
1121
|
+
const idx = this.listeners.indexOf(listener);
|
|
1122
|
+
if (idx !== -1) {
|
|
1123
|
+
this.listeners.splice(idx, 1);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Start subscribing to kind:3 follow list events for the node's pubkey.
|
|
1128
|
+
*
|
|
1129
|
+
* @returns Subscription with unsubscribe() to stop discovery
|
|
1130
|
+
* @throws PeerDiscoveryError if already started
|
|
1131
|
+
*/
|
|
1132
|
+
start() {
|
|
1133
|
+
if (this.started) {
|
|
1134
|
+
throw new PeerDiscoveryError("SocialPeerDiscovery already started");
|
|
1135
|
+
}
|
|
1136
|
+
this.started = true;
|
|
1137
|
+
const subCloser = this.pool.subscribeMany(
|
|
1138
|
+
this.config.relayUrls,
|
|
1139
|
+
{ kinds: [3], authors: [this.pubkey] },
|
|
1140
|
+
{
|
|
1141
|
+
onevent: (event) => {
|
|
1142
|
+
const followedPubkeys = event.tags.filter(
|
|
1143
|
+
(tag) => tag[0] === "p" && typeof tag[1] === "string"
|
|
1144
|
+
).map((tag) => tag[1]);
|
|
1145
|
+
this.processFollowListUpdate(followedPubkeys);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
);
|
|
1149
|
+
return {
|
|
1150
|
+
unsubscribe: () => {
|
|
1151
|
+
subCloser.close();
|
|
1152
|
+
this.started = false;
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Process a follow list update by diffing against previous state.
|
|
1158
|
+
*/
|
|
1159
|
+
processFollowListUpdate(followedPubkeys) {
|
|
1160
|
+
const currentFollows = new Set(followedPubkeys);
|
|
1161
|
+
for (const pubkey of currentFollows) {
|
|
1162
|
+
if (!this.previousFollows.has(pubkey)) {
|
|
1163
|
+
this.emit({ type: "social:follow-discovered", pubkey });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (const pubkey of this.previousFollows) {
|
|
1167
|
+
if (!currentFollows.has(pubkey)) {
|
|
1168
|
+
this.emit({ type: "social:follow-removed", pubkey });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
this.previousFollows = currentFollows;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Emit an event to all registered listeners.
|
|
1175
|
+
*/
|
|
1176
|
+
emit(event) {
|
|
1177
|
+
for (const listener of this.listeners) {
|
|
1178
|
+
try {
|
|
1179
|
+
listener(event);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
console.warn(
|
|
1182
|
+
"[SocialDiscovery] Listener error:",
|
|
1183
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
// src/discovery/seed-relay-discovery.ts
|
|
1191
|
+
import WebSocket from "ws";
|
|
1192
|
+
import { getPublicKey as getPublicKey2, verifyEvent } from "nostr-tools/pure";
|
|
1193
|
+
var DEFAULT_PUBLISH_CONNECTION_TIMEOUT = 1e4;
|
|
1194
|
+
var DEFAULT_PUBLISH_OK_TIMEOUT = 5e3;
|
|
1195
|
+
function generateSubId() {
|
|
1196
|
+
return `ct-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1197
|
+
}
|
|
1198
|
+
function connectWebSocket(url, timeout) {
|
|
1199
|
+
return new Promise((resolve, reject) => {
|
|
1200
|
+
const ws = new WebSocket(url);
|
|
1201
|
+
const timer = setTimeout(() => {
|
|
1202
|
+
ws.close();
|
|
1203
|
+
reject(new Error(`Connection timeout after ${timeout}ms: ${url}`));
|
|
1204
|
+
}, timeout);
|
|
1205
|
+
ws.on("open", () => {
|
|
1206
|
+
clearTimeout(timer);
|
|
1207
|
+
resolve(ws);
|
|
1208
|
+
});
|
|
1209
|
+
ws.on("error", (err) => {
|
|
1210
|
+
clearTimeout(timer);
|
|
1211
|
+
ws.close();
|
|
1212
|
+
reject(new Error(`WebSocket error connecting to ${url}: ${err.message}`));
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function subscribeAndCollect(ws, filter, timeout) {
|
|
1217
|
+
return new Promise((resolve) => {
|
|
1218
|
+
const events = [];
|
|
1219
|
+
const subId = generateSubId();
|
|
1220
|
+
let settled = false;
|
|
1221
|
+
const cleanup = () => {
|
|
1222
|
+
if (settled) return;
|
|
1223
|
+
settled = true;
|
|
1224
|
+
ws.removeListener("message", messageHandler);
|
|
1225
|
+
};
|
|
1226
|
+
const messageHandler = (data) => {
|
|
1227
|
+
try {
|
|
1228
|
+
const msg = JSON.parse(String(data));
|
|
1229
|
+
if (!Array.isArray(msg)) return;
|
|
1230
|
+
const msgType = msg[0];
|
|
1231
|
+
if (msgType === "EVENT" && msg[1] === subId && msg[2]) {
|
|
1232
|
+
events.push(msg[2]);
|
|
1233
|
+
}
|
|
1234
|
+
if (msgType === "EOSE" && msg[1] === subId) {
|
|
1235
|
+
clearTimeout(timer);
|
|
1236
|
+
cleanup();
|
|
1237
|
+
try {
|
|
1238
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
resolve(events);
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
const timer = setTimeout(() => {
|
|
1247
|
+
cleanup();
|
|
1248
|
+
try {
|
|
1249
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
resolve(events);
|
|
1253
|
+
}, timeout);
|
|
1254
|
+
ws.on("message", messageHandler);
|
|
1255
|
+
ws.send(JSON.stringify(["REQ", subId, filter]));
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
var SeedRelayDiscovery = class {
|
|
1259
|
+
config;
|
|
1260
|
+
connectionTimeout;
|
|
1261
|
+
queryTimeout;
|
|
1262
|
+
openSockets = [];
|
|
1263
|
+
constructor(config) {
|
|
1264
|
+
this.config = config;
|
|
1265
|
+
this.connectionTimeout = config.connectionTimeout ?? 1e4;
|
|
1266
|
+
this.queryTimeout = config.queryTimeout ?? 5e3;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Discover peers via seed relay list.
|
|
1270
|
+
*
|
|
1271
|
+
* 1. Query publicRelays for kind:10036 events
|
|
1272
|
+
* 2. Parse seed relay entries
|
|
1273
|
+
* 3. Connect to seed relays sequentially (fallback on failure)
|
|
1274
|
+
* 4. Subscribe to kind:10032 on connected seed relay
|
|
1275
|
+
* 5. Return discovered peers
|
|
1276
|
+
*
|
|
1277
|
+
* @throws PeerDiscoveryError if all seed relays are exhausted
|
|
1278
|
+
*/
|
|
1279
|
+
async discover() {
|
|
1280
|
+
const seedRelayEntries = await this.querySeedRelayLists();
|
|
1281
|
+
if (seedRelayEntries.length === 0) {
|
|
1282
|
+
throw new PeerDiscoveryError(
|
|
1283
|
+
`All seed relays exhausted -- unable to bootstrap. Tried 0 seed relays from 0 kind:10036 events. Queried ${this.config.publicRelays.length} public relay(s).`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
const uniqueEntries = this.deduplicateByUrl(seedRelayEntries);
|
|
1287
|
+
let connectedSocket;
|
|
1288
|
+
let connectedUrl = "";
|
|
1289
|
+
let attemptedSeeds = 0;
|
|
1290
|
+
for (const entry of uniqueEntries) {
|
|
1291
|
+
attemptedSeeds++;
|
|
1292
|
+
try {
|
|
1293
|
+
const ws = await connectWebSocket(entry.url, this.connectionTimeout);
|
|
1294
|
+
connectedSocket = ws;
|
|
1295
|
+
connectedUrl = entry.url;
|
|
1296
|
+
this.openSockets.push(ws);
|
|
1297
|
+
break;
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
1300
|
+
console.warn(
|
|
1301
|
+
`[SeedRelayDiscovery] Failed to connect to seed relay ${entry.url}: ${msg}`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (!connectedSocket) {
|
|
1306
|
+
throw new PeerDiscoveryError(
|
|
1307
|
+
`All seed relays exhausted -- unable to bootstrap. Tried ${attemptedSeeds} seed relays from ${seedRelayEntries.length} kind:10036 events.`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const peerEvents = await subscribeAndCollect(
|
|
1311
|
+
connectedSocket,
|
|
1312
|
+
{ kinds: [ILP_PEER_INFO_KIND] },
|
|
1313
|
+
this.queryTimeout
|
|
1314
|
+
);
|
|
1315
|
+
const discoveredPeers = [];
|
|
1316
|
+
for (const event of peerEvents) {
|
|
1317
|
+
try {
|
|
1318
|
+
if (!verifyEvent(event)) {
|
|
1319
|
+
console.warn(
|
|
1320
|
+
`[SeedRelayDiscovery] Skipping kind:10032 event with invalid signature: ${event.id}`
|
|
1321
|
+
);
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const info = parseIlpPeerInfo(event);
|
|
1325
|
+
info.pubkey = event.pubkey;
|
|
1326
|
+
discoveredPeers.push(info);
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
seedRelaysConnected: 1,
|
|
1332
|
+
attemptedSeeds,
|
|
1333
|
+
connectedUrls: [connectedUrl],
|
|
1334
|
+
discoveredPeers
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Stop discovery and close all open WebSocket connections.
|
|
1339
|
+
*/
|
|
1340
|
+
async close() {
|
|
1341
|
+
for (const ws of this.openSockets) {
|
|
1342
|
+
try {
|
|
1343
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
1344
|
+
ws.close();
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
this.openSockets.length = 0;
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Query public relays for kind:10036 events and parse seed relay entries.
|
|
1353
|
+
*/
|
|
1354
|
+
async querySeedRelayLists() {
|
|
1355
|
+
const allEntries = [];
|
|
1356
|
+
for (const relayUrl of this.config.publicRelays) {
|
|
1357
|
+
try {
|
|
1358
|
+
const ws = await connectWebSocket(relayUrl, this.connectionTimeout);
|
|
1359
|
+
this.openSockets.push(ws);
|
|
1360
|
+
const events = await subscribeAndCollect(
|
|
1361
|
+
ws,
|
|
1362
|
+
{ kinds: [SEED_RELAY_LIST_KIND] },
|
|
1363
|
+
this.queryTimeout
|
|
1364
|
+
);
|
|
1365
|
+
for (const event of events) {
|
|
1366
|
+
if (!verifyEvent(event)) {
|
|
1367
|
+
console.warn(
|
|
1368
|
+
`[SeedRelayDiscovery] Skipping kind:10036 event with invalid signature: ${event.id}`
|
|
1369
|
+
);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const entries = parseSeedRelayList(event);
|
|
1373
|
+
allEntries.push(...entries);
|
|
1374
|
+
}
|
|
1375
|
+
ws.close();
|
|
1376
|
+
const idx = this.openSockets.indexOf(ws);
|
|
1377
|
+
if (idx !== -1) {
|
|
1378
|
+
this.openSockets.splice(idx, 1);
|
|
1379
|
+
}
|
|
1380
|
+
} catch (err) {
|
|
1381
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
1382
|
+
console.warn(
|
|
1383
|
+
`[SeedRelayDiscovery] Failed to query public relay ${relayUrl}: ${msg}`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return allEntries;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Deduplicate seed relay entries by URL, keeping the first occurrence.
|
|
1391
|
+
*/
|
|
1392
|
+
deduplicateByUrl(entries) {
|
|
1393
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1394
|
+
const unique = [];
|
|
1395
|
+
for (const entry of entries) {
|
|
1396
|
+
if (!seen.has(entry.url)) {
|
|
1397
|
+
seen.add(entry.url);
|
|
1398
|
+
unique.push(entry);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return unique;
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
async function publishSeedRelayEntry(config) {
|
|
1405
|
+
const pubkey = getPublicKey2(config.secretKey);
|
|
1406
|
+
const entry = {
|
|
1407
|
+
url: config.relayUrl,
|
|
1408
|
+
pubkey,
|
|
1409
|
+
...config.metadata && { metadata: config.metadata }
|
|
1410
|
+
};
|
|
1411
|
+
const event = buildSeedRelayListEvent(config.secretKey, [entry]);
|
|
1412
|
+
let publishedTo = 0;
|
|
1413
|
+
for (const relayUrl of config.publicRelays) {
|
|
1414
|
+
try {
|
|
1415
|
+
const ws = await connectWebSocket(
|
|
1416
|
+
relayUrl,
|
|
1417
|
+
DEFAULT_PUBLISH_CONNECTION_TIMEOUT
|
|
1418
|
+
);
|
|
1419
|
+
const published = await new Promise((resolve) => {
|
|
1420
|
+
let settled = false;
|
|
1421
|
+
const cleanup = () => {
|
|
1422
|
+
if (settled) return;
|
|
1423
|
+
settled = true;
|
|
1424
|
+
ws.removeListener("message", messageHandler);
|
|
1425
|
+
};
|
|
1426
|
+
const timer = setTimeout(() => {
|
|
1427
|
+
cleanup();
|
|
1428
|
+
resolve(false);
|
|
1429
|
+
}, DEFAULT_PUBLISH_OK_TIMEOUT);
|
|
1430
|
+
const messageHandler = (data) => {
|
|
1431
|
+
try {
|
|
1432
|
+
const msg = JSON.parse(String(data));
|
|
1433
|
+
if (Array.isArray(msg) && msg[0] === "OK" && msg[1] === event.id) {
|
|
1434
|
+
clearTimeout(timer);
|
|
1435
|
+
cleanup();
|
|
1436
|
+
resolve(msg[2] === true);
|
|
1437
|
+
}
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
ws.on("message", messageHandler);
|
|
1442
|
+
ws.send(JSON.stringify(["EVENT", event]));
|
|
1443
|
+
});
|
|
1444
|
+
if (published) {
|
|
1445
|
+
publishedTo++;
|
|
1446
|
+
}
|
|
1447
|
+
ws.close();
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
1450
|
+
console.warn(
|
|
1451
|
+
`[SeedRelayDiscovery] Failed to publish to ${relayUrl}: ${msg}`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return { publishedTo, eventId: event.id };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/settlement/settlement.ts
|
|
1459
|
+
function negotiateSettlementChain(requesterChains, responderChains, requesterPreferredTokens, responderPreferredTokens) {
|
|
1460
|
+
const responderSet = new Set(responderChains);
|
|
1461
|
+
const intersection = requesterChains.filter(
|
|
1462
|
+
(chain) => responderSet.has(chain)
|
|
1463
|
+
);
|
|
1464
|
+
if (intersection.length === 0) {
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
if (requesterPreferredTokens) {
|
|
1468
|
+
const requesterMatch = intersection.find(
|
|
1469
|
+
(chain) => requesterPreferredTokens[chain] !== void 0
|
|
1470
|
+
);
|
|
1471
|
+
if (requesterMatch) {
|
|
1472
|
+
return requesterMatch;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (responderPreferredTokens) {
|
|
1476
|
+
const responderMatch = intersection.find(
|
|
1477
|
+
(chain) => responderPreferredTokens[chain] !== void 0
|
|
1478
|
+
);
|
|
1479
|
+
if (responderMatch) {
|
|
1480
|
+
return responderMatch;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return intersection[0] ?? null;
|
|
1484
|
+
}
|
|
1485
|
+
function resolveTokenForChain(chain, requesterPreferredTokens, responderPreferredTokens) {
|
|
1486
|
+
if (requesterPreferredTokens?.[chain] !== void 0) {
|
|
1487
|
+
return requesterPreferredTokens[chain];
|
|
1488
|
+
}
|
|
1489
|
+
if (responderPreferredTokens?.[chain] !== void 0) {
|
|
1490
|
+
return responderPreferredTokens[chain];
|
|
1491
|
+
}
|
|
1492
|
+
return void 0;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// src/bootstrap/BootstrapService.ts
|
|
1496
|
+
import { SimplePool as SimplePool3 } from "nostr-tools/pool";
|
|
1497
|
+
import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
|
|
1498
|
+
import WebSocket2 from "ws";
|
|
1499
|
+
var BootstrapError = class extends ToonError {
|
|
1500
|
+
constructor(message, cause) {
|
|
1501
|
+
super(message, "BOOTSTRAP_FAILED", cause);
|
|
1502
|
+
this.name = "BootstrapError";
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
var BootstrapService = class {
|
|
1506
|
+
config;
|
|
1507
|
+
secretKey;
|
|
1508
|
+
pubkey;
|
|
1509
|
+
ownIlpInfo;
|
|
1510
|
+
pool;
|
|
1511
|
+
connectorAdmin;
|
|
1512
|
+
channelClient;
|
|
1513
|
+
claimSigner;
|
|
1514
|
+
// ILP-first flow additions
|
|
1515
|
+
ilpClient;
|
|
1516
|
+
settlementInfo;
|
|
1517
|
+
ownIlpAddress;
|
|
1518
|
+
toonEncoder;
|
|
1519
|
+
toonDecoder;
|
|
1520
|
+
basePricePerByte;
|
|
1521
|
+
// Event emitter
|
|
1522
|
+
listeners = [];
|
|
1523
|
+
phase = "discovering";
|
|
1524
|
+
/**
|
|
1525
|
+
* Creates a new BootstrapService instance.
|
|
1526
|
+
*
|
|
1527
|
+
* @param config - Bootstrap configuration with known peers and optional ILP-first settings
|
|
1528
|
+
* @param secretKey - Our Nostr secret key for signing events
|
|
1529
|
+
* @param ownIlpInfo - Our ILP peer info to publish
|
|
1530
|
+
* @param pool - Optional SimplePool instance (creates new one if not provided)
|
|
1531
|
+
*/
|
|
1532
|
+
constructor(config, secretKey, ownIlpInfo, pool) {
|
|
1533
|
+
this.config = {
|
|
1534
|
+
knownPeers: config.knownPeers,
|
|
1535
|
+
queryTimeout: config.queryTimeout ?? 5e3,
|
|
1536
|
+
ardriveEnabled: config.ardriveEnabled ?? true,
|
|
1537
|
+
defaultRelayUrl: config.defaultRelayUrl ?? ""
|
|
1538
|
+
};
|
|
1539
|
+
this.secretKey = secretKey;
|
|
1540
|
+
this.pubkey = getPublicKey3(secretKey);
|
|
1541
|
+
this.ownIlpInfo = ownIlpInfo;
|
|
1542
|
+
this.pool = pool ?? new SimplePool3();
|
|
1543
|
+
this.settlementInfo = config.settlementInfo;
|
|
1544
|
+
this.ownIlpAddress = config.ownIlpAddress;
|
|
1545
|
+
this.toonEncoder = config.toonEncoder;
|
|
1546
|
+
this.toonDecoder = config.toonDecoder;
|
|
1547
|
+
this.basePricePerByte = config.basePricePerByte ?? 10n;
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Set the ILP client for sending packets via the connector.
|
|
1551
|
+
* Kept separate from constructor for backward compatibility with existing code
|
|
1552
|
+
* that creates the client after construction.
|
|
1553
|
+
*/
|
|
1554
|
+
setIlpClient(client) {
|
|
1555
|
+
this.ilpClient = client;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* @deprecated Use setIlpClient instead
|
|
1559
|
+
*/
|
|
1560
|
+
setAgentRuntimeClient(client) {
|
|
1561
|
+
this.setIlpClient(client);
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Set the connector admin client for adding peers/routes.
|
|
1565
|
+
*/
|
|
1566
|
+
setConnectorAdmin(admin) {
|
|
1567
|
+
this.connectorAdmin = admin;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Set the channel client for opening payment channels.
|
|
1571
|
+
*/
|
|
1572
|
+
setChannelClient(client) {
|
|
1573
|
+
this.channelClient = client;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Set the claim signer for creating signed balance proofs.
|
|
1577
|
+
* Used by clients to sign payment channel claims for ILP packets.
|
|
1578
|
+
*/
|
|
1579
|
+
setClaimSigner(signer) {
|
|
1580
|
+
this.claimSigner = signer;
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Get the current bootstrap phase.
|
|
1584
|
+
*/
|
|
1585
|
+
getPhase() {
|
|
1586
|
+
return this.phase;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Register an event listener.
|
|
1590
|
+
*/
|
|
1591
|
+
on(listener) {
|
|
1592
|
+
this.listeners.push(listener);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Unregister an event listener.
|
|
1596
|
+
*/
|
|
1597
|
+
off(listener) {
|
|
1598
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Emit a bootstrap event to all listeners.
|
|
1602
|
+
*/
|
|
1603
|
+
emit(event) {
|
|
1604
|
+
for (const listener of this.listeners) {
|
|
1605
|
+
try {
|
|
1606
|
+
listener(event);
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Transition to a new phase, emitting phase change event.
|
|
1613
|
+
*/
|
|
1614
|
+
setPhase(newPhase) {
|
|
1615
|
+
const previousPhase = this.phase;
|
|
1616
|
+
this.phase = newPhase;
|
|
1617
|
+
this.emit({ type: "bootstrap:phase", phase: newPhase, previousPhase });
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Load peers from genesis config, ArDrive, and optional env var JSON.
|
|
1621
|
+
* Merges all sources, deduplicating by pubkey (ArDrive overrides genesis for matching pubkeys).
|
|
1622
|
+
*/
|
|
1623
|
+
async loadPeers(additionalPeersJson) {
|
|
1624
|
+
const genesisPeers = GenesisPeerLoader.loadAllPeers(additionalPeersJson);
|
|
1625
|
+
const ardrivePeers = [];
|
|
1626
|
+
if (this.config.ardriveEnabled) {
|
|
1627
|
+
try {
|
|
1628
|
+
const ardriveMap = await ArDrivePeerRegistry.fetchPeers();
|
|
1629
|
+
for (const [pubkey, info] of ardriveMap) {
|
|
1630
|
+
if (!this.config.defaultRelayUrl) continue;
|
|
1631
|
+
ardrivePeers.push({
|
|
1632
|
+
pubkey,
|
|
1633
|
+
relayUrl: this.config.defaultRelayUrl,
|
|
1634
|
+
ilpAddress: info.ilpAddress,
|
|
1635
|
+
btpEndpoint: info.btpEndpoint
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
console.warn(
|
|
1640
|
+
"[Bootstrap] ArDrive peer fetch failed, using genesis peers only:",
|
|
1641
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1646
|
+
for (const peer of genesisPeers) {
|
|
1647
|
+
merged.set(peer.pubkey, peer);
|
|
1648
|
+
}
|
|
1649
|
+
for (const peer of ardrivePeers) {
|
|
1650
|
+
merged.set(peer.pubkey, peer);
|
|
1651
|
+
}
|
|
1652
|
+
return [...merged.values()];
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Bootstrap with all known peers.
|
|
1656
|
+
*
|
|
1657
|
+
* Loads peers from genesis config, ArDrive, and optional env var JSON,
|
|
1658
|
+
* then attempts to bootstrap with each peer in order.
|
|
1659
|
+
* Returns results for successfully bootstrapped peers.
|
|
1660
|
+
* Continues to next peer on failure.
|
|
1661
|
+
*
|
|
1662
|
+
* @param additionalPeersJson - Optional JSON string of additional peers to merge
|
|
1663
|
+
* @returns Array of successful bootstrap results
|
|
1664
|
+
*/
|
|
1665
|
+
async bootstrap(additionalPeersJson) {
|
|
1666
|
+
const results = [];
|
|
1667
|
+
try {
|
|
1668
|
+
this.setPhase("discovering");
|
|
1669
|
+
const allPeers = await this.loadPeers(additionalPeersJson);
|
|
1670
|
+
const knownPeersMap = /* @__PURE__ */ new Map();
|
|
1671
|
+
for (const peer of this.config.knownPeers) {
|
|
1672
|
+
knownPeersMap.set(peer.pubkey, peer);
|
|
1673
|
+
}
|
|
1674
|
+
for (const peer of allPeers) {
|
|
1675
|
+
if (!knownPeersMap.has(peer.pubkey)) {
|
|
1676
|
+
knownPeersMap.set(peer.pubkey, {
|
|
1677
|
+
pubkey: peer.pubkey,
|
|
1678
|
+
relayUrl: peer.relayUrl,
|
|
1679
|
+
btpEndpoint: peer.btpEndpoint
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
this.setPhase("registering");
|
|
1684
|
+
for (const knownPeer of knownPeersMap.values()) {
|
|
1685
|
+
try {
|
|
1686
|
+
const result = await this.bootstrapWithPeer(knownPeer);
|
|
1687
|
+
results.push(result);
|
|
1688
|
+
console.log(
|
|
1689
|
+
`[Bootstrap] Successfully bootstrapped with ${knownPeer.pubkey.slice(0, 16)}...`
|
|
1690
|
+
);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
console.warn(
|
|
1693
|
+
`[Bootstrap] Failed to bootstrap with ${knownPeer.pubkey.slice(0, 16)}...:`,
|
|
1694
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (this.ilpClient && results.length > 0) {
|
|
1699
|
+
this.setPhase("announcing");
|
|
1700
|
+
for (const result of results) {
|
|
1701
|
+
try {
|
|
1702
|
+
await this.announceViaIlp(result);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
1705
|
+
this.emit({
|
|
1706
|
+
type: "bootstrap:announce-failed",
|
|
1707
|
+
peerId: result.registeredPeerId,
|
|
1708
|
+
reason
|
|
1709
|
+
});
|
|
1710
|
+
console.warn(
|
|
1711
|
+
`[Bootstrap] Announce failed for ${result.registeredPeerId}:`,
|
|
1712
|
+
reason
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const channelCount = results.filter((r) => r.channelId).length;
|
|
1718
|
+
this.setPhase("ready");
|
|
1719
|
+
this.emit({
|
|
1720
|
+
type: "bootstrap:ready",
|
|
1721
|
+
peerCount: results.length,
|
|
1722
|
+
channelCount
|
|
1723
|
+
});
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
this.setPhase("failed");
|
|
1726
|
+
console.error(
|
|
1727
|
+
"[Bootstrap] Bootstrap failed:",
|
|
1728
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
return results;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Bootstrap with a single known peer.
|
|
1735
|
+
*
|
|
1736
|
+
* @param knownPeer - The known peer to bootstrap with
|
|
1737
|
+
* @returns Bootstrap result with peer info and registered peer ID
|
|
1738
|
+
* @throws BootstrapError if pubkey is invalid or peer info query fails
|
|
1739
|
+
*/
|
|
1740
|
+
async bootstrapWithPeer(knownPeer) {
|
|
1741
|
+
const PUBKEY_REGEX4 = /^[0-9a-f]{64}$/;
|
|
1742
|
+
if (!PUBKEY_REGEX4.test(knownPeer.pubkey)) {
|
|
1743
|
+
throw new BootstrapError(
|
|
1744
|
+
`Invalid pubkey format for known peer: ${knownPeer.pubkey}`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
console.log(`[Bootstrap] Querying ${knownPeer.relayUrl} for peer info...`);
|
|
1748
|
+
const peerInfo = await this.queryPeerInfo(knownPeer);
|
|
1749
|
+
const registeredPeerId = `nostr-${knownPeer.pubkey.slice(0, 16)}`;
|
|
1750
|
+
if (this.connectorAdmin) {
|
|
1751
|
+
try {
|
|
1752
|
+
console.log(`[Bootstrap] Adding peer to connector routing table...`);
|
|
1753
|
+
await this.addPeerToConnector(knownPeer, peerInfo);
|
|
1754
|
+
this.emit({
|
|
1755
|
+
type: "bootstrap:peer-registered",
|
|
1756
|
+
peerId: registeredPeerId,
|
|
1757
|
+
peerPubkey: knownPeer.pubkey,
|
|
1758
|
+
ilpAddress: peerInfo.ilpAddress
|
|
1759
|
+
});
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
console.warn(
|
|
1762
|
+
`[Bootstrap] Failed to register peer ${registeredPeerId} with connector:`,
|
|
1763
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const result = {
|
|
1768
|
+
knownPeer,
|
|
1769
|
+
peerInfo,
|
|
1770
|
+
registeredPeerId
|
|
1771
|
+
};
|
|
1772
|
+
if (this.channelClient && this.settlementInfo?.supportedChains?.length && peerInfo.supportedChains?.length && peerInfo.settlementAddresses) {
|
|
1773
|
+
try {
|
|
1774
|
+
const negotiatedChain = negotiateSettlementChain(
|
|
1775
|
+
this.settlementInfo.supportedChains,
|
|
1776
|
+
peerInfo.supportedChains,
|
|
1777
|
+
this.settlementInfo.preferredTokens,
|
|
1778
|
+
peerInfo.preferredTokens
|
|
1779
|
+
);
|
|
1780
|
+
if (negotiatedChain) {
|
|
1781
|
+
const peerAddress = peerInfo.settlementAddresses[negotiatedChain];
|
|
1782
|
+
const tokenAddress = resolveTokenForChain(
|
|
1783
|
+
negotiatedChain,
|
|
1784
|
+
this.settlementInfo.preferredTokens,
|
|
1785
|
+
peerInfo.preferredTokens
|
|
1786
|
+
);
|
|
1787
|
+
const tokenNetwork = peerInfo.tokenNetworks?.[negotiatedChain];
|
|
1788
|
+
if (peerAddress) {
|
|
1789
|
+
console.log(
|
|
1790
|
+
`[Bootstrap] Opening channel on ${negotiatedChain} with ${registeredPeerId}...`
|
|
1791
|
+
);
|
|
1792
|
+
const channelResult = await this.channelClient.openChannel({
|
|
1793
|
+
peerId: registeredPeerId,
|
|
1794
|
+
chain: negotiatedChain,
|
|
1795
|
+
token: tokenAddress,
|
|
1796
|
+
tokenNetwork,
|
|
1797
|
+
peerAddress,
|
|
1798
|
+
initialDeposit: "100000",
|
|
1799
|
+
settlementTimeout: 86400
|
|
1800
|
+
});
|
|
1801
|
+
result.channelId = channelResult.channelId;
|
|
1802
|
+
result.negotiatedChain = negotiatedChain;
|
|
1803
|
+
result.settlementAddress = peerAddress;
|
|
1804
|
+
console.log(
|
|
1805
|
+
`[Bootstrap] Opened channel ${channelResult.channelId} with ${registeredPeerId}`
|
|
1806
|
+
);
|
|
1807
|
+
this.emit({
|
|
1808
|
+
type: "bootstrap:channel-opened",
|
|
1809
|
+
peerId: registeredPeerId,
|
|
1810
|
+
channelId: channelResult.channelId,
|
|
1811
|
+
negotiatedChain
|
|
1812
|
+
});
|
|
1813
|
+
if (this.connectorAdmin) {
|
|
1814
|
+
await this.connectorAdmin.addPeer({
|
|
1815
|
+
id: registeredPeerId,
|
|
1816
|
+
url: peerInfo.btpEndpoint,
|
|
1817
|
+
authToken: "",
|
|
1818
|
+
routes: [{ prefix: peerInfo.ilpAddress }],
|
|
1819
|
+
settlement: {
|
|
1820
|
+
preference: negotiatedChain,
|
|
1821
|
+
...peerAddress && { evmAddress: peerAddress },
|
|
1822
|
+
...tokenAddress && { tokenAddress },
|
|
1823
|
+
...tokenNetwork && { tokenNetworkAddress: tokenNetwork },
|
|
1824
|
+
...channelResult.channelId && {
|
|
1825
|
+
channelId: channelResult.channelId
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
1834
|
+
console.warn(
|
|
1835
|
+
`[Bootstrap] Settlement failed for ${registeredPeerId}:`,
|
|
1836
|
+
reason
|
|
1837
|
+
);
|
|
1838
|
+
this.emit({
|
|
1839
|
+
type: "bootstrap:settlement-failed",
|
|
1840
|
+
peerId: registeredPeerId,
|
|
1841
|
+
reason
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (!this.ilpClient) {
|
|
1846
|
+
try {
|
|
1847
|
+
console.log(
|
|
1848
|
+
`[Bootstrap] Publishing our ILP info to ${knownPeer.relayUrl}...`
|
|
1849
|
+
);
|
|
1850
|
+
await this.publishOurInfo(knownPeer.relayUrl);
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
console.warn(
|
|
1853
|
+
`[Bootstrap] Failed to publish ILP info to ${knownPeer.relayUrl}:`,
|
|
1854
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return result;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Announce own kind:10032 as paid ILP PREPARE (Phase 2).
|
|
1862
|
+
*/
|
|
1863
|
+
async announceViaIlp(result) {
|
|
1864
|
+
if (!this.ilpClient || !this.toonEncoder) {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const ilpInfoEvent = buildIlpPeerInfoEvent(this.ownIlpInfo, this.secretKey);
|
|
1868
|
+
const toonBytes = this.toonEncoder(ilpInfoEvent);
|
|
1869
|
+
const base64Toon = Buffer.from(toonBytes).toString("base64");
|
|
1870
|
+
const amount = String(BigInt(toonBytes.length) * this.basePricePerByte);
|
|
1871
|
+
const ilpResult = await this.ilpClient.sendIlpPacket({
|
|
1872
|
+
destination: result.peerInfo.ilpAddress,
|
|
1873
|
+
amount,
|
|
1874
|
+
data: base64Toon
|
|
1875
|
+
});
|
|
1876
|
+
if (ilpResult.accepted) {
|
|
1877
|
+
console.log(
|
|
1878
|
+
`[Bootstrap] Announced to ${result.registeredPeerId} via ILP (fulfillment: ${ilpResult.fulfillment}, eventId: ${ilpInfoEvent.id})`
|
|
1879
|
+
);
|
|
1880
|
+
this.emit({
|
|
1881
|
+
type: "bootstrap:announced",
|
|
1882
|
+
peerId: result.registeredPeerId,
|
|
1883
|
+
eventId: ilpInfoEvent.id,
|
|
1884
|
+
amount
|
|
1885
|
+
});
|
|
1886
|
+
} else {
|
|
1887
|
+
const reason = `${ilpResult.code} ${ilpResult.message}`;
|
|
1888
|
+
this.emit({
|
|
1889
|
+
type: "bootstrap:announce-failed",
|
|
1890
|
+
peerId: result.registeredPeerId,
|
|
1891
|
+
reason
|
|
1892
|
+
});
|
|
1893
|
+
console.warn(
|
|
1894
|
+
`[Bootstrap] Announce rejected by ${result.registeredPeerId}: ${reason}`
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Query a peer's relay for their kind:10032 ILP Peer Info event.
|
|
1900
|
+
* Uses direct WebSocket connection for reliable container-to-container communication.
|
|
1901
|
+
*/
|
|
1902
|
+
async queryPeerInfo(knownPeer) {
|
|
1903
|
+
const filter = {
|
|
1904
|
+
kinds: [ILP_PEER_INFO_KIND],
|
|
1905
|
+
authors: [knownPeer.pubkey],
|
|
1906
|
+
limit: 1
|
|
1907
|
+
};
|
|
1908
|
+
console.log(`[Bootstrap] Query filter:`, JSON.stringify(filter));
|
|
1909
|
+
console.log(`[Bootstrap] Connecting to ${knownPeer.relayUrl}...`);
|
|
1910
|
+
return new Promise((resolve, reject) => {
|
|
1911
|
+
const events = [];
|
|
1912
|
+
const timeout = this.config.queryTimeout ?? 5e3;
|
|
1913
|
+
const ws = new WebSocket2(knownPeer.relayUrl);
|
|
1914
|
+
const subId = `bootstrap-${Date.now()}`;
|
|
1915
|
+
let resolved = false;
|
|
1916
|
+
const cleanup = () => {
|
|
1917
|
+
if (!resolved) {
|
|
1918
|
+
resolved = true;
|
|
1919
|
+
try {
|
|
1920
|
+
ws.close();
|
|
1921
|
+
} catch {
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
ws.on("open", () => {
|
|
1926
|
+
console.log(
|
|
1927
|
+
`[Bootstrap] Connected to ${knownPeer.relayUrl}, sending REQ`
|
|
1928
|
+
);
|
|
1929
|
+
ws.send(JSON.stringify(["REQ", subId, filter]));
|
|
1930
|
+
});
|
|
1931
|
+
ws.on("message", (data) => {
|
|
1932
|
+
let msg;
|
|
1933
|
+
try {
|
|
1934
|
+
msg = JSON.parse(data.toString());
|
|
1935
|
+
} catch {
|
|
1936
|
+
console.warn("[Bootstrap] Received malformed JSON from relay");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
console.log(`[Bootstrap] Received message type: ${msg[0]}`);
|
|
1940
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
1941
|
+
let event;
|
|
1942
|
+
const rawEvent = msg[2];
|
|
1943
|
+
if (typeof rawEvent === "string") {
|
|
1944
|
+
try {
|
|
1945
|
+
const lines = rawEvent.trim().split("\n");
|
|
1946
|
+
const parsed = {};
|
|
1947
|
+
for (const line of lines) {
|
|
1948
|
+
const colonIndex = line.indexOf(":");
|
|
1949
|
+
if (colonIndex > 0) {
|
|
1950
|
+
const key = line.substring(0, colonIndex).trim();
|
|
1951
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
1952
|
+
if (key.startsWith("tags[")) {
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1956
|
+
parsed[key] = JSON.parse(value);
|
|
1957
|
+
} else if (!isNaN(Number(value))) {
|
|
1958
|
+
parsed[key] = Number(value);
|
|
1959
|
+
} else {
|
|
1960
|
+
parsed[key] = value;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
event = parsed;
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
console.warn(`[Bootstrap] Failed to parse TOON event:`, error);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
} else if (typeof rawEvent === "object" && rawEvent !== null) {
|
|
1970
|
+
event = rawEvent;
|
|
1971
|
+
}
|
|
1972
|
+
if (event && typeof event["id"] === "string") {
|
|
1973
|
+
console.log(
|
|
1974
|
+
`[Bootstrap] Received event: ${event["id"].slice(0, 16)}...`
|
|
1975
|
+
);
|
|
1976
|
+
events.push(event);
|
|
1977
|
+
} else {
|
|
1978
|
+
console.warn(
|
|
1979
|
+
`[Bootstrap] Received EVENT message with invalid event data:`,
|
|
1980
|
+
msg
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
} else if (msg[0] === "EOSE" && msg[1] === subId) {
|
|
1984
|
+
console.log(
|
|
1985
|
+
`[Bootstrap] EOSE received, found ${events.length} events`
|
|
1986
|
+
);
|
|
1987
|
+
cleanup();
|
|
1988
|
+
if (events.length === 0) {
|
|
1989
|
+
reject(
|
|
1990
|
+
new BootstrapError(
|
|
1991
|
+
`No kind:${ILP_PEER_INFO_KIND} event found for peer ${knownPeer.pubkey.slice(0, 16)}...`
|
|
1992
|
+
)
|
|
1993
|
+
);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const sortedEvents = events.sort(
|
|
1997
|
+
(a, b) => b.created_at - a.created_at
|
|
1998
|
+
);
|
|
1999
|
+
const mostRecent = sortedEvents[0];
|
|
2000
|
+
try {
|
|
2001
|
+
const peerInfo = parseIlpPeerInfo(
|
|
2002
|
+
mostRecent
|
|
2003
|
+
);
|
|
2004
|
+
resolve(peerInfo);
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
reject(
|
|
2007
|
+
new BootstrapError(
|
|
2008
|
+
`Failed to parse peer info`,
|
|
2009
|
+
error instanceof Error ? error : void 0
|
|
2010
|
+
)
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
} else if (msg[0] === "NOTICE") {
|
|
2014
|
+
console.log(`[Bootstrap] Notice from relay: ${msg[1]}`);
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
ws.on("error", (error) => {
|
|
2018
|
+
console.error(`[Bootstrap] WebSocket error:`, error.message);
|
|
2019
|
+
cleanup();
|
|
2020
|
+
reject(
|
|
2021
|
+
new BootstrapError(
|
|
2022
|
+
`Failed to connect to ${knownPeer.relayUrl}: ${error.message}`,
|
|
2023
|
+
error
|
|
2024
|
+
)
|
|
2025
|
+
);
|
|
2026
|
+
});
|
|
2027
|
+
ws.on("close", () => {
|
|
2028
|
+
console.log(`[Bootstrap] Connection closed`);
|
|
2029
|
+
if (!resolved) {
|
|
2030
|
+
cleanup();
|
|
2031
|
+
reject(
|
|
2032
|
+
new BootstrapError(
|
|
2033
|
+
`Connection closed before receiving events from ${knownPeer.relayUrl}`
|
|
2034
|
+
)
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
setTimeout(() => {
|
|
2039
|
+
if (resolved) return;
|
|
2040
|
+
console.log(`[Bootstrap] Query timeout after ${timeout}ms`);
|
|
2041
|
+
cleanup();
|
|
2042
|
+
if (events.length > 0) {
|
|
2043
|
+
const sortedEvents = events.sort(
|
|
2044
|
+
(a, b) => b.created_at - a.created_at
|
|
2045
|
+
);
|
|
2046
|
+
const mostRecent = sortedEvents[0];
|
|
2047
|
+
try {
|
|
2048
|
+
const peerInfo = parseIlpPeerInfo(
|
|
2049
|
+
mostRecent
|
|
2050
|
+
);
|
|
2051
|
+
resolve(peerInfo);
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
reject(
|
|
2054
|
+
new BootstrapError(
|
|
2055
|
+
`Failed to parse peer info`,
|
|
2056
|
+
error instanceof Error ? error : void 0
|
|
2057
|
+
)
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
} else {
|
|
2061
|
+
reject(
|
|
2062
|
+
new BootstrapError(
|
|
2063
|
+
`Query timeout: No events received from ${knownPeer.relayUrl} after ${timeout}ms`
|
|
2064
|
+
)
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
}, timeout);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Add a peer to the connector via Admin API.
|
|
2072
|
+
*/
|
|
2073
|
+
async addPeerToConnector(knownPeer, peerInfo) {
|
|
2074
|
+
if (!this.connectorAdmin) {
|
|
2075
|
+
throw new BootstrapError("Connector admin client not set");
|
|
2076
|
+
}
|
|
2077
|
+
const peerId = `nostr-${knownPeer.pubkey.slice(0, 16)}`;
|
|
2078
|
+
await this.connectorAdmin.addPeer({
|
|
2079
|
+
id: peerId,
|
|
2080
|
+
url: peerInfo.btpEndpoint,
|
|
2081
|
+
authToken: "",
|
|
2082
|
+
// BTP doesn't need auth
|
|
2083
|
+
routes: [{ prefix: peerInfo.ilpAddress }]
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Publish our own kind:10032 ILP Peer Info to a relay.
|
|
2088
|
+
*/
|
|
2089
|
+
async publishOurInfo(relayUrl) {
|
|
2090
|
+
const event = buildIlpPeerInfoEvent(this.ownIlpInfo, this.secretKey);
|
|
2091
|
+
try {
|
|
2092
|
+
await this.pool.publish([relayUrl], event);
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
throw new BootstrapError(
|
|
2095
|
+
`Failed to publish ILP info to ${relayUrl}`,
|
|
2096
|
+
error instanceof Error ? error : void 0
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Query a peer's relay for other peers' kind:10032 events.
|
|
2102
|
+
*
|
|
2103
|
+
* Used after bootstrapping to discover additional peers
|
|
2104
|
+
* that have also published to the bootstrap node's relay.
|
|
2105
|
+
*
|
|
2106
|
+
* @param relayUrl - The relay URL to query
|
|
2107
|
+
* @param excludePubkeys - Pubkeys to exclude from results (e.g., our own, known peers)
|
|
2108
|
+
* @returns Map of pubkey to IlpPeerInfo for discovered peers
|
|
2109
|
+
*/
|
|
2110
|
+
async discoverPeersViaRelay(relayUrl, excludePubkeys = []) {
|
|
2111
|
+
const excludeSet = /* @__PURE__ */ new Set([this.pubkey, ...excludePubkeys]);
|
|
2112
|
+
const filter = {
|
|
2113
|
+
kinds: [ILP_PEER_INFO_KIND]
|
|
2114
|
+
};
|
|
2115
|
+
try {
|
|
2116
|
+
const events = await this.pool.querySync([relayUrl], filter);
|
|
2117
|
+
const eventsByPubkey = /* @__PURE__ */ new Map();
|
|
2118
|
+
for (const event of events) {
|
|
2119
|
+
if (excludeSet.has(event.pubkey)) continue;
|
|
2120
|
+
const existing = eventsByPubkey.get(event.pubkey);
|
|
2121
|
+
if (!existing || event.created_at > existing.created_at) {
|
|
2122
|
+
eventsByPubkey.set(event.pubkey, event);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
const result = /* @__PURE__ */ new Map();
|
|
2126
|
+
for (const [pubkey, event] of eventsByPubkey) {
|
|
2127
|
+
try {
|
|
2128
|
+
const info = parseIlpPeerInfo(event);
|
|
2129
|
+
result.set(pubkey, info);
|
|
2130
|
+
} catch {
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return result;
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
throw new BootstrapError(
|
|
2136
|
+
`Failed to discover peers from ${relayUrl}`,
|
|
2137
|
+
error instanceof Error ? error : void 0
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Get our pubkey.
|
|
2143
|
+
*/
|
|
2144
|
+
getPubkey() {
|
|
2145
|
+
return this.pubkey;
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Publish our ILP info to a specific relay.
|
|
2149
|
+
*
|
|
2150
|
+
* @param relayUrl - The relay URL to publish to (defaults to 'ws://localhost:7100')
|
|
2151
|
+
*/
|
|
2152
|
+
async publishToRelay(relayUrl = "ws://localhost:7100") {
|
|
2153
|
+
await this.publishOurInfo(relayUrl);
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
// src/bootstrap/discovery-tracker.ts
|
|
2158
|
+
import { getPublicKey as getPublicKey4 } from "nostr-tools/pure";
|
|
2159
|
+
function createDiscoveryTracker(config) {
|
|
2160
|
+
const pubkey = getPublicKey4(config.secretKey);
|
|
2161
|
+
const excludedPubkeys = /* @__PURE__ */ new Set([pubkey]);
|
|
2162
|
+
let connectorAdmin;
|
|
2163
|
+
let channelClient;
|
|
2164
|
+
let listeners = [];
|
|
2165
|
+
const discoveredPeers = /* @__PURE__ */ new Map();
|
|
2166
|
+
const peeredPubkeys = /* @__PURE__ */ new Set();
|
|
2167
|
+
const peerTimestamps = /* @__PURE__ */ new Map();
|
|
2168
|
+
function emit(event) {
|
|
2169
|
+
for (const listener of listeners) {
|
|
2170
|
+
try {
|
|
2171
|
+
listener(event);
|
|
2172
|
+
} catch {
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
function handleDeregistration(peerPubkey, peerId) {
|
|
2177
|
+
discoveredPeers.delete(peerPubkey);
|
|
2178
|
+
if (peeredPubkeys.has(peerPubkey)) {
|
|
2179
|
+
peeredPubkeys.delete(peerPubkey);
|
|
2180
|
+
if (connectorAdmin?.removePeer) {
|
|
2181
|
+
connectorAdmin.removePeer(peerId).then(() => {
|
|
2182
|
+
emit({
|
|
2183
|
+
type: "bootstrap:peer-deregistered",
|
|
2184
|
+
peerId,
|
|
2185
|
+
peerPubkey,
|
|
2186
|
+
reason: "empty-content"
|
|
2187
|
+
});
|
|
2188
|
+
}).catch((error) => {
|
|
2189
|
+
console.warn(
|
|
2190
|
+
`[DiscoveryTracker] Failed to deregister ${peerId}:`,
|
|
2191
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
2192
|
+
);
|
|
2193
|
+
});
|
|
2194
|
+
} else {
|
|
2195
|
+
emit({
|
|
2196
|
+
type: "bootstrap:peer-deregistered",
|
|
2197
|
+
peerId,
|
|
2198
|
+
peerPubkey,
|
|
2199
|
+
reason: "empty-content"
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function processDiscovery(event) {
|
|
2205
|
+
const peerId = `nostr-${event.pubkey.slice(0, 16)}`;
|
|
2206
|
+
let peerInfo;
|
|
2207
|
+
try {
|
|
2208
|
+
peerInfo = parseIlpPeerInfo(event);
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
if (!peerInfo || !peerInfo.ilpAddress || !event.content || event.content.trim() === "") {
|
|
2212
|
+
handleDeregistration(event.pubkey, peerId);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
discoveredPeers.set(event.pubkey, {
|
|
2216
|
+
pubkey: event.pubkey,
|
|
2217
|
+
peerId,
|
|
2218
|
+
peerInfo,
|
|
2219
|
+
discoveredAt: event.created_at
|
|
2220
|
+
});
|
|
2221
|
+
emit({
|
|
2222
|
+
type: "bootstrap:peer-discovered",
|
|
2223
|
+
peerPubkey: event.pubkey,
|
|
2224
|
+
ilpAddress: peerInfo.ilpAddress
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
const tracker = {
|
|
2228
|
+
processEvent(event) {
|
|
2229
|
+
if (event.kind !== ILP_PEER_INFO_KIND) return;
|
|
2230
|
+
if (excludedPubkeys.has(event.pubkey)) return;
|
|
2231
|
+
const lastSeen = peerTimestamps.get(event.pubkey) ?? 0;
|
|
2232
|
+
if (event.created_at <= lastSeen) return;
|
|
2233
|
+
peerTimestamps.set(event.pubkey, event.created_at);
|
|
2234
|
+
processDiscovery(event);
|
|
2235
|
+
},
|
|
2236
|
+
async peerWith(targetPubkey) {
|
|
2237
|
+
if (peeredPubkeys.has(targetPubkey)) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const discovered = discoveredPeers.get(targetPubkey);
|
|
2241
|
+
if (!discovered) {
|
|
2242
|
+
throw new BootstrapError(
|
|
2243
|
+
`Peer ${targetPubkey.slice(0, 16)}... not discovered yet`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
if (!connectorAdmin) {
|
|
2247
|
+
throw new BootstrapError(
|
|
2248
|
+
"connectorAdmin must be set before calling peerWith()"
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
const { peerId, peerInfo } = discovered;
|
|
2252
|
+
const admin = connectorAdmin;
|
|
2253
|
+
peeredPubkeys.add(targetPubkey);
|
|
2254
|
+
try {
|
|
2255
|
+
await admin.addPeer({
|
|
2256
|
+
id: peerId,
|
|
2257
|
+
url: peerInfo.btpEndpoint,
|
|
2258
|
+
authToken: "",
|
|
2259
|
+
routes: [{ prefix: peerInfo.ilpAddress }]
|
|
2260
|
+
});
|
|
2261
|
+
emit({
|
|
2262
|
+
type: "bootstrap:peer-registered",
|
|
2263
|
+
peerId,
|
|
2264
|
+
peerPubkey: targetPubkey,
|
|
2265
|
+
ilpAddress: peerInfo.ilpAddress
|
|
2266
|
+
});
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
peeredPubkeys.delete(targetPubkey);
|
|
2269
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2270
|
+
console.warn(
|
|
2271
|
+
`[DiscoveryTracker] Failed to register ${peerId}:`,
|
|
2272
|
+
reason
|
|
2273
|
+
);
|
|
2274
|
+
emit({
|
|
2275
|
+
type: "bootstrap:settlement-failed",
|
|
2276
|
+
peerId,
|
|
2277
|
+
reason: `Registration failed: ${reason}`
|
|
2278
|
+
});
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (channelClient && config.settlementInfo?.supportedChains?.length && peerInfo.supportedChains?.length && peerInfo.settlementAddresses) {
|
|
2282
|
+
try {
|
|
2283
|
+
const negotiatedChain = negotiateSettlementChain(
|
|
2284
|
+
config.settlementInfo.supportedChains,
|
|
2285
|
+
peerInfo.supportedChains,
|
|
2286
|
+
config.settlementInfo.preferredTokens,
|
|
2287
|
+
peerInfo.preferredTokens
|
|
2288
|
+
);
|
|
2289
|
+
if (negotiatedChain) {
|
|
2290
|
+
const peerAddress = peerInfo.settlementAddresses[negotiatedChain];
|
|
2291
|
+
const tokenAddress = resolveTokenForChain(
|
|
2292
|
+
negotiatedChain,
|
|
2293
|
+
config.settlementInfo.preferredTokens,
|
|
2294
|
+
peerInfo.preferredTokens
|
|
2295
|
+
);
|
|
2296
|
+
const tokenNetwork = peerInfo.tokenNetworks?.[negotiatedChain];
|
|
2297
|
+
if (peerAddress) {
|
|
2298
|
+
const channelResult = await channelClient.openChannel({
|
|
2299
|
+
peerId,
|
|
2300
|
+
chain: negotiatedChain,
|
|
2301
|
+
token: tokenAddress,
|
|
2302
|
+
tokenNetwork,
|
|
2303
|
+
peerAddress,
|
|
2304
|
+
initialDeposit: "100000",
|
|
2305
|
+
settlementTimeout: 86400
|
|
2306
|
+
});
|
|
2307
|
+
await admin.addPeer({
|
|
2308
|
+
id: peerId,
|
|
2309
|
+
url: peerInfo.btpEndpoint,
|
|
2310
|
+
authToken: "",
|
|
2311
|
+
routes: [{ prefix: peerInfo.ilpAddress }],
|
|
2312
|
+
settlement: {
|
|
2313
|
+
preference: negotiatedChain,
|
|
2314
|
+
...peerAddress && { evmAddress: peerAddress },
|
|
2315
|
+
...tokenAddress && { tokenAddress },
|
|
2316
|
+
...tokenNetwork && {
|
|
2317
|
+
tokenNetworkAddress: tokenNetwork
|
|
2318
|
+
},
|
|
2319
|
+
...channelResult.channelId && {
|
|
2320
|
+
channelId: channelResult.channelId
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
emit({
|
|
2325
|
+
type: "bootstrap:channel-opened",
|
|
2326
|
+
peerId,
|
|
2327
|
+
channelId: channelResult.channelId,
|
|
2328
|
+
negotiatedChain
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2334
|
+
console.warn(
|
|
2335
|
+
`[DiscoveryTracker] Settlement failed for ${peerId}:`,
|
|
2336
|
+
reason
|
|
2337
|
+
);
|
|
2338
|
+
emit({
|
|
2339
|
+
type: "bootstrap:settlement-failed",
|
|
2340
|
+
peerId,
|
|
2341
|
+
reason
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
},
|
|
2346
|
+
getDiscoveredPeers() {
|
|
2347
|
+
return [...discoveredPeers.values()].filter(
|
|
2348
|
+
(p) => !peeredPubkeys.has(p.pubkey)
|
|
2349
|
+
);
|
|
2350
|
+
},
|
|
2351
|
+
isPeered(targetPubkey) {
|
|
2352
|
+
return peeredPubkeys.has(targetPubkey);
|
|
2353
|
+
},
|
|
2354
|
+
getPeerCount() {
|
|
2355
|
+
return peeredPubkeys.size;
|
|
2356
|
+
},
|
|
2357
|
+
getDiscoveredCount() {
|
|
2358
|
+
return discoveredPeers.size;
|
|
2359
|
+
},
|
|
2360
|
+
on(listener) {
|
|
2361
|
+
listeners.push(listener);
|
|
2362
|
+
},
|
|
2363
|
+
off(listener) {
|
|
2364
|
+
listeners = listeners.filter((l) => l !== listener);
|
|
2365
|
+
},
|
|
2366
|
+
setConnectorAdmin(admin) {
|
|
2367
|
+
connectorAdmin = admin;
|
|
2368
|
+
},
|
|
2369
|
+
setChannelClient(client) {
|
|
2370
|
+
channelClient = client;
|
|
2371
|
+
},
|
|
2372
|
+
addExcludedPubkeys(pubkeys) {
|
|
2373
|
+
for (const pk of pubkeys) {
|
|
2374
|
+
excludedPubkeys.add(pk);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
return tracker;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// src/bootstrap/ilp-client.ts
|
|
2382
|
+
function createHttpIlpClient(baseUrl) {
|
|
2383
|
+
try {
|
|
2384
|
+
new URL(baseUrl);
|
|
2385
|
+
} catch {
|
|
2386
|
+
throw new BootstrapError(`Invalid connector base URL: ${baseUrl}`);
|
|
2387
|
+
}
|
|
2388
|
+
const normalizedUrl = baseUrl.replace(/\/+$/, "");
|
|
2389
|
+
return {
|
|
2390
|
+
async sendIlpPacket(params) {
|
|
2391
|
+
let response;
|
|
2392
|
+
try {
|
|
2393
|
+
response = await fetch(`${normalizedUrl}/admin/ilp/send`, {
|
|
2394
|
+
method: "POST",
|
|
2395
|
+
headers: { "Content-Type": "application/json" },
|
|
2396
|
+
body: JSON.stringify({
|
|
2397
|
+
destination: params.destination,
|
|
2398
|
+
amount: params.amount,
|
|
2399
|
+
data: params.data,
|
|
2400
|
+
...params.timeout !== void 0 && { timeout: params.timeout }
|
|
2401
|
+
})
|
|
2402
|
+
});
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
throw new BootstrapError(
|
|
2405
|
+
`Network error sending ILP packet: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2406
|
+
error instanceof Error ? error : void 0
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (!response.ok) {
|
|
2410
|
+
const text = await response.text().catch(() => "Unknown error");
|
|
2411
|
+
throw new BootstrapError(
|
|
2412
|
+
`Connector error (${response.status}): ${text}`
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
const data = await response.json();
|
|
2416
|
+
return {
|
|
2417
|
+
accepted: data["accepted"] ?? data["fulfilled"] ?? false,
|
|
2418
|
+
fulfillment: data["fulfillment"],
|
|
2419
|
+
data: data["data"],
|
|
2420
|
+
code: data["code"],
|
|
2421
|
+
message: data["message"]
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
var createHttpRuntimeClient = createHttpIlpClient;
|
|
2427
|
+
var createAgentRuntimeClient = createHttpIlpClient;
|
|
2428
|
+
|
|
2429
|
+
// src/bootstrap/direct-ilp-client.ts
|
|
2430
|
+
import { createHash } from "crypto";
|
|
2431
|
+
function createDirectIlpClient(connector, config) {
|
|
2432
|
+
return {
|
|
2433
|
+
async sendIlpPacket(params) {
|
|
2434
|
+
try {
|
|
2435
|
+
const amount = BigInt(params.amount);
|
|
2436
|
+
const data = Uint8Array.from(Buffer.from(params.data, "base64"));
|
|
2437
|
+
let executionCondition;
|
|
2438
|
+
if (config?.toonDecoder && data.length > 0) {
|
|
2439
|
+
const decoded = config.toonDecoder(data);
|
|
2440
|
+
const fulfillment = createHash("sha256").update(decoded.id).digest();
|
|
2441
|
+
executionCondition = createHash("sha256").update(fulfillment).digest();
|
|
2442
|
+
}
|
|
2443
|
+
const result = await connector.sendPacket({
|
|
2444
|
+
destination: params.destination,
|
|
2445
|
+
amount,
|
|
2446
|
+
data,
|
|
2447
|
+
executionCondition,
|
|
2448
|
+
expiresAt: new Date(Date.now() + 3e4)
|
|
2449
|
+
});
|
|
2450
|
+
if (result.type === "fulfill" || result.type === 13) {
|
|
2451
|
+
return {
|
|
2452
|
+
accepted: true,
|
|
2453
|
+
fulfillment: Buffer.from(result.fulfillment).toString("base64"),
|
|
2454
|
+
data: result.data ? Buffer.from(result.data).toString("base64") : void 0
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
return {
|
|
2458
|
+
accepted: false,
|
|
2459
|
+
code: result.code,
|
|
2460
|
+
message: result.message,
|
|
2461
|
+
data: result.data ? Buffer.from(result.data).toString("base64") : void 0
|
|
2462
|
+
};
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
if (error instanceof BootstrapError) {
|
|
2465
|
+
throw error;
|
|
2466
|
+
}
|
|
2467
|
+
throw new BootstrapError(
|
|
2468
|
+
`Direct ILP packet send failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2469
|
+
error instanceof Error ? error : void 0
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
var createDirectRuntimeClient = createDirectIlpClient;
|
|
2476
|
+
|
|
2477
|
+
// src/bootstrap/direct-connector-admin.ts
|
|
2478
|
+
function createDirectConnectorAdmin(connector) {
|
|
2479
|
+
return {
|
|
2480
|
+
async addPeer(config) {
|
|
2481
|
+
try {
|
|
2482
|
+
await connector.registerPeer({
|
|
2483
|
+
id: config.id,
|
|
2484
|
+
url: config.url,
|
|
2485
|
+
authToken: config.authToken,
|
|
2486
|
+
routes: config.routes,
|
|
2487
|
+
settlement: config.settlement
|
|
2488
|
+
});
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
if (error instanceof BootstrapError) {
|
|
2491
|
+
throw error;
|
|
2492
|
+
}
|
|
2493
|
+
throw new BootstrapError(
|
|
2494
|
+
`Failed to register peer ${config.id}`,
|
|
2495
|
+
error instanceof Error ? error : void 0
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
async removePeer(peerId) {
|
|
2500
|
+
try {
|
|
2501
|
+
await connector.removePeer(peerId);
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
if (error instanceof BootstrapError) {
|
|
2504
|
+
throw error;
|
|
2505
|
+
}
|
|
2506
|
+
throw new BootstrapError(
|
|
2507
|
+
`Failed to remove peer ${peerId}`,
|
|
2508
|
+
error instanceof Error ? error : void 0
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/bootstrap/direct-channel-client.ts
|
|
2516
|
+
function createDirectChannelClient(connector) {
|
|
2517
|
+
return {
|
|
2518
|
+
async openChannel(params) {
|
|
2519
|
+
try {
|
|
2520
|
+
return await connector.openChannel(params);
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
if (error instanceof BootstrapError) {
|
|
2523
|
+
throw error;
|
|
2524
|
+
}
|
|
2525
|
+
throw new BootstrapError(
|
|
2526
|
+
`Failed to open channel for peer ${params.peerId}`,
|
|
2527
|
+
error instanceof Error ? error : void 0
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
},
|
|
2531
|
+
async getChannelState(channelId) {
|
|
2532
|
+
try {
|
|
2533
|
+
return await connector.getChannelState(channelId);
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
if (error instanceof BootstrapError) {
|
|
2536
|
+
throw error;
|
|
2537
|
+
}
|
|
2538
|
+
throw new BootstrapError(
|
|
2539
|
+
`Failed to get channel state for ${channelId}`,
|
|
2540
|
+
error instanceof Error ? error : void 0
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/bootstrap/http-connector-admin.ts
|
|
2548
|
+
function createHttpConnectorAdmin(adminUrl, btpSecret) {
|
|
2549
|
+
const baseUrl = adminUrl.replace(/\/$/, "");
|
|
2550
|
+
return {
|
|
2551
|
+
async addPeer(config) {
|
|
2552
|
+
try {
|
|
2553
|
+
const payload = {
|
|
2554
|
+
id: config.id,
|
|
2555
|
+
url: config.url,
|
|
2556
|
+
authToken: config.authToken || JSON.stringify({
|
|
2557
|
+
peerId: config.id,
|
|
2558
|
+
secret: btpSecret
|
|
2559
|
+
}),
|
|
2560
|
+
routes: config.routes,
|
|
2561
|
+
settlement: config.settlement
|
|
2562
|
+
};
|
|
2563
|
+
const response = await fetch(`${baseUrl}/admin/peers`, {
|
|
2564
|
+
method: "POST",
|
|
2565
|
+
headers: { "Content-Type": "application/json" },
|
|
2566
|
+
body: JSON.stringify(payload)
|
|
2567
|
+
});
|
|
2568
|
+
if (!response.ok) {
|
|
2569
|
+
const text = await response.text();
|
|
2570
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
2571
|
+
}
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
if (error instanceof BootstrapError) {
|
|
2574
|
+
throw error;
|
|
2575
|
+
}
|
|
2576
|
+
throw new BootstrapError(
|
|
2577
|
+
`Failed to register peer ${config.id} via HTTP`,
|
|
2578
|
+
error instanceof Error ? error : void 0
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
},
|
|
2582
|
+
async removePeer(peerId) {
|
|
2583
|
+
try {
|
|
2584
|
+
const response = await fetch(`${baseUrl}/admin/peers/${peerId}`, {
|
|
2585
|
+
method: "DELETE"
|
|
2586
|
+
});
|
|
2587
|
+
if (!response.ok) {
|
|
2588
|
+
const text = await response.text();
|
|
2589
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
2590
|
+
}
|
|
2591
|
+
} catch (error) {
|
|
2592
|
+
if (error instanceof BootstrapError) {
|
|
2593
|
+
throw error;
|
|
2594
|
+
}
|
|
2595
|
+
throw new BootstrapError(
|
|
2596
|
+
`Failed to remove peer ${peerId} via HTTP`,
|
|
2597
|
+
error instanceof Error ? error : void 0
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// src/bootstrap/http-ilp-client.ts
|
|
2605
|
+
function createHttpIlpClient2(connectorUrl) {
|
|
2606
|
+
const baseUrl = connectorUrl.replace(/\/$/, "");
|
|
2607
|
+
return {
|
|
2608
|
+
async sendIlpPacket(params) {
|
|
2609
|
+
try {
|
|
2610
|
+
const response = await fetch(`${baseUrl}/send-packet`, {
|
|
2611
|
+
method: "POST",
|
|
2612
|
+
headers: { "Content-Type": "application/json" },
|
|
2613
|
+
body: JSON.stringify({
|
|
2614
|
+
destination: params.destination,
|
|
2615
|
+
amount: params.amount,
|
|
2616
|
+
data: params.data
|
|
2617
|
+
}),
|
|
2618
|
+
signal: params.timeout ? AbortSignal.timeout(params.timeout) : void 0
|
|
2619
|
+
});
|
|
2620
|
+
if (!response.ok) {
|
|
2621
|
+
const text = await response.text();
|
|
2622
|
+
return {
|
|
2623
|
+
accepted: false,
|
|
2624
|
+
code: "T00",
|
|
2625
|
+
message: `HTTP ${response.status}: ${text}`
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
const result = await response.json();
|
|
2629
|
+
if (result["accept"] || result["accepted"]) {
|
|
2630
|
+
return {
|
|
2631
|
+
accepted: true,
|
|
2632
|
+
fulfillment: result["fulfillment"],
|
|
2633
|
+
data: result["data"]
|
|
2634
|
+
};
|
|
2635
|
+
} else {
|
|
2636
|
+
return {
|
|
2637
|
+
accepted: false,
|
|
2638
|
+
code: result["code"] || "T00",
|
|
2639
|
+
message: result["message"] || "Unknown error"
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
if (error instanceof BootstrapError) {
|
|
2644
|
+
throw error;
|
|
2645
|
+
}
|
|
2646
|
+
return {
|
|
2647
|
+
accepted: false,
|
|
2648
|
+
code: "T00",
|
|
2649
|
+
message: `HTTP request failed: ${error instanceof Error ? error.message : "Unknown"}`
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// src/bootstrap/http-channel-client.ts
|
|
2657
|
+
function createHttpChannelClient(adminUrl) {
|
|
2658
|
+
const baseUrl = adminUrl.replace(/\/$/, "");
|
|
2659
|
+
return {
|
|
2660
|
+
async openChannel(params) {
|
|
2661
|
+
try {
|
|
2662
|
+
const response = await fetch(`${baseUrl}/admin/channels`, {
|
|
2663
|
+
method: "POST",
|
|
2664
|
+
headers: { "Content-Type": "application/json" },
|
|
2665
|
+
body: JSON.stringify({
|
|
2666
|
+
peerId: params.peerId,
|
|
2667
|
+
chain: params.chain,
|
|
2668
|
+
token: params.token,
|
|
2669
|
+
tokenNetwork: params.tokenNetwork,
|
|
2670
|
+
peerAddress: params.peerAddress,
|
|
2671
|
+
initialDeposit: params.initialDeposit,
|
|
2672
|
+
settlementTimeout: params.settlementTimeout
|
|
2673
|
+
})
|
|
2674
|
+
});
|
|
2675
|
+
if (!response.ok) {
|
|
2676
|
+
const text = await response.text();
|
|
2677
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
2678
|
+
}
|
|
2679
|
+
const result = await response.json();
|
|
2680
|
+
return {
|
|
2681
|
+
channelId: result["channelId"],
|
|
2682
|
+
status: result["status"]
|
|
2683
|
+
};
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
if (error instanceof BootstrapError) {
|
|
2686
|
+
throw error;
|
|
2687
|
+
}
|
|
2688
|
+
throw new BootstrapError(
|
|
2689
|
+
`Failed to open channel for peer ${params.peerId} via HTTP`,
|
|
2690
|
+
error instanceof Error ? error : void 0
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
},
|
|
2694
|
+
async getChannelState(channelId) {
|
|
2695
|
+
try {
|
|
2696
|
+
const response = await fetch(`${baseUrl}/admin/channels/${channelId}`);
|
|
2697
|
+
if (!response.ok) {
|
|
2698
|
+
const text = await response.text();
|
|
2699
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
2700
|
+
}
|
|
2701
|
+
const result = await response.json();
|
|
2702
|
+
return {
|
|
2703
|
+
channelId: result["channelId"],
|
|
2704
|
+
status: result["status"],
|
|
2705
|
+
chain: result["chain"]
|
|
2706
|
+
};
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
if (error instanceof BootstrapError) {
|
|
2709
|
+
throw error;
|
|
2710
|
+
}
|
|
2711
|
+
throw new BootstrapError(
|
|
2712
|
+
`Failed to get channel state for ${channelId} via HTTP`,
|
|
2713
|
+
error instanceof Error ? error : void 0
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// src/bootstrap/AttestationVerifier.ts
|
|
2721
|
+
var AttestationState = /* @__PURE__ */ ((AttestationState2) => {
|
|
2722
|
+
AttestationState2["VALID"] = "valid";
|
|
2723
|
+
AttestationState2["STALE"] = "stale";
|
|
2724
|
+
AttestationState2["UNATTESTED"] = "unattested";
|
|
2725
|
+
return AttestationState2;
|
|
2726
|
+
})(AttestationState || {});
|
|
2727
|
+
var DEFAULT_VALIDITY_SECONDS = 300;
|
|
2728
|
+
var DEFAULT_GRACE_SECONDS = 30;
|
|
2729
|
+
var AttestationVerifier = class {
|
|
2730
|
+
knownGoodPcrs;
|
|
2731
|
+
validitySeconds;
|
|
2732
|
+
graceSeconds;
|
|
2733
|
+
constructor(config) {
|
|
2734
|
+
this.knownGoodPcrs = new Map(config.knownGoodPcrs);
|
|
2735
|
+
const validity = config.validitySeconds ?? DEFAULT_VALIDITY_SECONDS;
|
|
2736
|
+
const grace = config.graceSeconds ?? DEFAULT_GRACE_SECONDS;
|
|
2737
|
+
if (!Number.isFinite(validity) || validity < 0) {
|
|
2738
|
+
throw new Error(
|
|
2739
|
+
`validitySeconds must be a non-negative finite number, got ${String(validity)}`
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
if (!Number.isFinite(grace) || grace < 0) {
|
|
2743
|
+
throw new Error(
|
|
2744
|
+
`graceSeconds must be a non-negative finite number, got ${String(grace)}`
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
this.validitySeconds = validity;
|
|
2748
|
+
this.graceSeconds = grace;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Verifies a TEE attestation's PCR values against the known-good registry.
|
|
2752
|
+
*
|
|
2753
|
+
* All three PCR values (pcr0, pcr1, pcr2) must be present and truthy in
|
|
2754
|
+
* the registry for verification to pass.
|
|
2755
|
+
*
|
|
2756
|
+
* @param attestation - The TEE attestation to verify.
|
|
2757
|
+
* @returns Verification result with `valid: true` or `valid: false` with reason.
|
|
2758
|
+
*/
|
|
2759
|
+
verify(attestation) {
|
|
2760
|
+
const pcr0Valid = this.knownGoodPcrs.get(attestation.pcr0) === true;
|
|
2761
|
+
const pcr1Valid = this.knownGoodPcrs.get(attestation.pcr1) === true;
|
|
2762
|
+
const pcr2Valid = this.knownGoodPcrs.get(attestation.pcr2) === true;
|
|
2763
|
+
if (pcr0Valid && pcr1Valid && pcr2Valid) {
|
|
2764
|
+
return { valid: true };
|
|
2765
|
+
}
|
|
2766
|
+
return { valid: false, reason: "PCR mismatch" };
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Computes the attestation lifecycle state based on timing.
|
|
2770
|
+
*
|
|
2771
|
+
* Boundary behavior:
|
|
2772
|
+
* - At exactly `attestedAt + validitySeconds`: VALID (inclusive <=)
|
|
2773
|
+
* - At exactly `attestedAt + validitySeconds + graceSeconds`: STALE (inclusive <=)
|
|
2774
|
+
* - After grace expires: UNATTESTED
|
|
2775
|
+
*
|
|
2776
|
+
* @param _attestation - The TEE attestation (unused, reserved for future per-attestation logic).
|
|
2777
|
+
* @param attestedAt - Unix timestamp when the attestation was created.
|
|
2778
|
+
* @param now - Optional current unix timestamp (defaults to real clock).
|
|
2779
|
+
* @returns The current attestation state.
|
|
2780
|
+
*/
|
|
2781
|
+
getAttestationState(_attestation, attestedAt, now) {
|
|
2782
|
+
if (!Number.isFinite(attestedAt)) {
|
|
2783
|
+
return "unattested" /* UNATTESTED */;
|
|
2784
|
+
}
|
|
2785
|
+
const currentTime = now ?? Math.floor(Date.now() / 1e3);
|
|
2786
|
+
const validityEnd = attestedAt + this.validitySeconds;
|
|
2787
|
+
const graceEnd = validityEnd + this.graceSeconds;
|
|
2788
|
+
if (currentTime <= validityEnd) {
|
|
2789
|
+
return "valid" /* VALID */;
|
|
2790
|
+
}
|
|
2791
|
+
if (currentTime <= graceEnd) {
|
|
2792
|
+
return "stale" /* STALE */;
|
|
2793
|
+
}
|
|
2794
|
+
return "unattested" /* UNATTESTED */;
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Ranks peers by attestation status: attested peers first, then non-attested.
|
|
2798
|
+
*
|
|
2799
|
+
* Preserves relative order within each group (stable sort via filter).
|
|
2800
|
+
* Does NOT mutate the input array -- returns a new sorted array.
|
|
2801
|
+
*
|
|
2802
|
+
* Attestation is a preference, not a requirement. Non-attested peers
|
|
2803
|
+
* remain in the result and are connectable.
|
|
2804
|
+
*
|
|
2805
|
+
* @param peers - Array of peer descriptors to rank.
|
|
2806
|
+
* @returns New array with attested peers first, preserving relative order.
|
|
2807
|
+
*/
|
|
2808
|
+
rankPeers(peers) {
|
|
2809
|
+
const attested = peers.filter((p) => p.attested);
|
|
2810
|
+
const nonAttested = peers.filter((p) => !p.attested);
|
|
2811
|
+
return [...attested, ...nonAttested];
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
|
|
2815
|
+
// src/bootstrap/AttestationBootstrap.ts
|
|
2816
|
+
var AttestationBootstrap = class {
|
|
2817
|
+
config;
|
|
2818
|
+
listeners = [];
|
|
2819
|
+
constructor(config) {
|
|
2820
|
+
this.config = config;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Register an event listener.
|
|
2824
|
+
*/
|
|
2825
|
+
on(listener) {
|
|
2826
|
+
this.listeners.push(listener);
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Unregister an event listener.
|
|
2830
|
+
*/
|
|
2831
|
+
off(listener) {
|
|
2832
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Emit an attestation bootstrap event to all listeners.
|
|
2836
|
+
*/
|
|
2837
|
+
emit(event) {
|
|
2838
|
+
const snapshot = [...this.listeners];
|
|
2839
|
+
for (const listener of snapshot) {
|
|
2840
|
+
try {
|
|
2841
|
+
listener(event);
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Execute the attestation-first bootstrap flow.
|
|
2848
|
+
*
|
|
2849
|
+
* Iterates seed relays in order:
|
|
2850
|
+
* 1. Emit seed-connected, query attestation
|
|
2851
|
+
* 2. If null or verify fails or error: emit verification-failed, try next
|
|
2852
|
+
* 3. If valid: emit verified, subscribe peers, emit peers-discovered
|
|
2853
|
+
* 4. Return attested result with discovered peers
|
|
2854
|
+
* 5. If ALL fail: log warning, emit degraded, return degraded result
|
|
2855
|
+
*/
|
|
2856
|
+
async bootstrap() {
|
|
2857
|
+
const { seedRelays, verifier, queryAttestation, subscribePeers } = this.config;
|
|
2858
|
+
for (const relayUrl of seedRelays) {
|
|
2859
|
+
this.emit({ type: "attestation:seed-connected", relayUrl });
|
|
2860
|
+
try {
|
|
2861
|
+
const attestationEvent = await queryAttestation(relayUrl);
|
|
2862
|
+
if (attestationEvent === null) {
|
|
2863
|
+
this.emit({
|
|
2864
|
+
type: "attestation:verification-failed",
|
|
2865
|
+
relayUrl,
|
|
2866
|
+
reason: "No attestation event found"
|
|
2867
|
+
});
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
const raw = await Promise.resolve(verifier.verify(attestationEvent));
|
|
2871
|
+
const isValid = typeof raw === "boolean" ? raw : typeof raw === "object" && raw !== null && "valid" in raw ? raw.valid : false;
|
|
2872
|
+
if (!isValid) {
|
|
2873
|
+
this.emit({
|
|
2874
|
+
type: "attestation:verification-failed",
|
|
2875
|
+
relayUrl,
|
|
2876
|
+
reason: "Attestation verification failed"
|
|
2877
|
+
});
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
this.emit({
|
|
2881
|
+
type: "attestation:verified",
|
|
2882
|
+
relayUrl,
|
|
2883
|
+
pubkey: attestationEvent.pubkey
|
|
2884
|
+
});
|
|
2885
|
+
const peers = await subscribePeers(relayUrl);
|
|
2886
|
+
this.emit({
|
|
2887
|
+
type: "attestation:peers-discovered",
|
|
2888
|
+
relayUrl,
|
|
2889
|
+
peerCount: peers.length
|
|
2890
|
+
});
|
|
2891
|
+
return {
|
|
2892
|
+
mode: "attested",
|
|
2893
|
+
attestedSeedRelay: relayUrl,
|
|
2894
|
+
discoveredPeers: peers
|
|
2895
|
+
};
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2898
|
+
this.emit({
|
|
2899
|
+
type: "attestation:verification-failed",
|
|
2900
|
+
relayUrl,
|
|
2901
|
+
reason
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
console.warn(
|
|
2906
|
+
`No attested seed relays found. Tried ${String(seedRelays.length)} relays. Starting in degraded mode.`
|
|
2907
|
+
);
|
|
2908
|
+
this.emit({
|
|
2909
|
+
type: "attestation:degraded",
|
|
2910
|
+
triedCount: seedRelays.length
|
|
2911
|
+
});
|
|
2912
|
+
return {
|
|
2913
|
+
mode: "degraded",
|
|
2914
|
+
attestedSeedRelay: void 0,
|
|
2915
|
+
discoveredPeers: []
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
|
|
2920
|
+
// src/compose.ts
|
|
2921
|
+
function createToonNode(config) {
|
|
2922
|
+
const directIlpClient = createDirectIlpClient(config.connector, {
|
|
2923
|
+
toonDecoder: config.toonDecoder
|
|
2924
|
+
});
|
|
2925
|
+
const directAdminClient = createDirectConnectorAdmin(config.connector);
|
|
2926
|
+
const channelClient = config.connector.openChannel && config.connector.getChannelState ? createDirectChannelClient(
|
|
2927
|
+
config.connector
|
|
2928
|
+
) : null;
|
|
2929
|
+
const bootstrapService = new BootstrapService(
|
|
2930
|
+
{
|
|
2931
|
+
knownPeers: config.knownPeers ?? [],
|
|
2932
|
+
ardriveEnabled: config.ardriveEnabled ?? true,
|
|
2933
|
+
defaultRelayUrl: config.defaultRelayUrl ?? "",
|
|
2934
|
+
queryTimeout: config.queryTimeout ?? 5e3,
|
|
2935
|
+
settlementInfo: config.settlementInfo,
|
|
2936
|
+
ownIlpAddress: config.ilpInfo.ilpAddress,
|
|
2937
|
+
toonEncoder: config.toonEncoder,
|
|
2938
|
+
toonDecoder: config.toonDecoder,
|
|
2939
|
+
basePricePerByte: config.basePricePerByte ?? 10n
|
|
2940
|
+
},
|
|
2941
|
+
config.secretKey,
|
|
2942
|
+
config.ilpInfo
|
|
2943
|
+
);
|
|
2944
|
+
bootstrapService.setIlpClient(directIlpClient);
|
|
2945
|
+
bootstrapService.setConnectorAdmin(directAdminClient);
|
|
2946
|
+
if (channelClient) {
|
|
2947
|
+
bootstrapService.setChannelClient(channelClient);
|
|
2948
|
+
}
|
|
2949
|
+
const discoveryTracker = createDiscoveryTracker({
|
|
2950
|
+
secretKey: config.secretKey,
|
|
2951
|
+
settlementInfo: config.settlementInfo
|
|
2952
|
+
});
|
|
2953
|
+
discoveryTracker.setConnectorAdmin(directAdminClient);
|
|
2954
|
+
if (channelClient) {
|
|
2955
|
+
discoveryTracker.setChannelClient(channelClient);
|
|
2956
|
+
}
|
|
2957
|
+
let started = false;
|
|
2958
|
+
return {
|
|
2959
|
+
bootstrapService,
|
|
2960
|
+
discoveryTracker,
|
|
2961
|
+
channelClient,
|
|
2962
|
+
ilpClient: directIlpClient,
|
|
2963
|
+
peerWith(pubkey) {
|
|
2964
|
+
return discoveryTracker.peerWith(pubkey);
|
|
2965
|
+
},
|
|
2966
|
+
async start() {
|
|
2967
|
+
if (started) {
|
|
2968
|
+
throw new BootstrapError("ToonNode already started");
|
|
2969
|
+
}
|
|
2970
|
+
try {
|
|
2971
|
+
if (config.connector.setPacketHandler) {
|
|
2972
|
+
config.connector.setPacketHandler(config.handlePacket);
|
|
2973
|
+
}
|
|
2974
|
+
const results = await bootstrapService.bootstrap(
|
|
2975
|
+
config.additionalPeersJson
|
|
2976
|
+
);
|
|
2977
|
+
const bootstrapPeerPubkeys = results.map((r) => r.knownPeer.pubkey);
|
|
2978
|
+
discoveryTracker.addExcludedPubkeys(bootstrapPeerPubkeys);
|
|
2979
|
+
started = true;
|
|
2980
|
+
const channelCount = results.filter((r) => r.channelId).length;
|
|
2981
|
+
return {
|
|
2982
|
+
bootstrapResults: results,
|
|
2983
|
+
peerCount: results.length,
|
|
2984
|
+
channelCount
|
|
2985
|
+
};
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
throw new BootstrapError(
|
|
2988
|
+
`Failed to start ToonNode: ${error instanceof Error ? error.message : String(error)}`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
},
|
|
2992
|
+
async stop() {
|
|
2993
|
+
if (!started) {
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
started = false;
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/chain/usdc.ts
|
|
3002
|
+
var MOCK_USDC_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
|
|
3003
|
+
var USDC_DECIMALS = 6;
|
|
3004
|
+
var USDC_SYMBOL = "USDC";
|
|
3005
|
+
var USDC_NAME = "USD Coin";
|
|
3006
|
+
var MOCK_USDC_CONFIG = {
|
|
3007
|
+
address: MOCK_USDC_ADDRESS,
|
|
3008
|
+
decimals: USDC_DECIMALS,
|
|
3009
|
+
symbol: USDC_SYMBOL,
|
|
3010
|
+
name: USDC_NAME
|
|
3011
|
+
};
|
|
3012
|
+
|
|
3013
|
+
// src/chain/chain-config.ts
|
|
3014
|
+
var CHAIN_PRESETS = {
|
|
3015
|
+
anvil: {
|
|
3016
|
+
name: "anvil",
|
|
3017
|
+
chainId: 31337,
|
|
3018
|
+
rpcUrl: "http://localhost:8545",
|
|
3019
|
+
usdcAddress: MOCK_USDC_ADDRESS,
|
|
3020
|
+
tokenNetworkAddress: "0xCafac3dD18aC6c6e92c921884f9E4176737C052c"
|
|
3021
|
+
},
|
|
3022
|
+
"arbitrum-sepolia": {
|
|
3023
|
+
name: "arbitrum-sepolia",
|
|
3024
|
+
chainId: 421614,
|
|
3025
|
+
rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc",
|
|
3026
|
+
usdcAddress: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
|
|
3027
|
+
tokenNetworkAddress: ""
|
|
3028
|
+
},
|
|
3029
|
+
"arbitrum-one": {
|
|
3030
|
+
name: "arbitrum-one",
|
|
3031
|
+
chainId: 42161,
|
|
3032
|
+
rpcUrl: "https://arb1.arbitrum.io/rpc",
|
|
3033
|
+
usdcAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
3034
|
+
tokenNetworkAddress: ""
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
function resolveChainConfig(chain) {
|
|
3038
|
+
const envChain = process.env["TOON_CHAIN"];
|
|
3039
|
+
const name = envChain || chain || "anvil";
|
|
3040
|
+
const preset = CHAIN_PRESETS[name];
|
|
3041
|
+
if (!preset) {
|
|
3042
|
+
throw new ToonError(
|
|
3043
|
+
`Unknown chain "${name}". Valid chains: anvil, arbitrum-sepolia, arbitrum-one`,
|
|
3044
|
+
"INVALID_CHAIN"
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
const resolved = { ...preset };
|
|
3048
|
+
const envRpcUrl = process.env["TOON_RPC_URL"];
|
|
3049
|
+
if (envRpcUrl) {
|
|
3050
|
+
resolved.rpcUrl = envRpcUrl;
|
|
3051
|
+
}
|
|
3052
|
+
const envTokenNetwork = process.env["TOON_TOKEN_NETWORK"];
|
|
3053
|
+
if (envTokenNetwork) {
|
|
3054
|
+
resolved.tokenNetworkAddress = envTokenNetwork;
|
|
3055
|
+
}
|
|
3056
|
+
return resolved;
|
|
3057
|
+
}
|
|
3058
|
+
function buildEip712Domain(config) {
|
|
3059
|
+
return {
|
|
3060
|
+
name: "TokenNetwork",
|
|
3061
|
+
version: "1",
|
|
3062
|
+
chainId: config.chainId,
|
|
3063
|
+
verifyingContract: config.tokenNetworkAddress
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// src/x402/build-ilp-prepare.ts
|
|
3068
|
+
function buildIlpPrepare(params) {
|
|
3069
|
+
return {
|
|
3070
|
+
destination: params.destination,
|
|
3071
|
+
amount: String(params.amount),
|
|
3072
|
+
data: Buffer.from(params.data).toString("base64")
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/identity/kms-identity.ts
|
|
3077
|
+
import { validateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
|
|
3078
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
3079
|
+
import { HDKey } from "@scure/bip32";
|
|
3080
|
+
import { getPublicKey as getPublicKey5 } from "nostr-tools/pure";
|
|
3081
|
+
var KmsIdentityError = class extends ToonError {
|
|
3082
|
+
constructor(message, cause) {
|
|
3083
|
+
super(message, "KMS_IDENTITY_ERROR", cause);
|
|
3084
|
+
this.name = "KmsIdentityError";
|
|
3085
|
+
}
|
|
3086
|
+
};
|
|
3087
|
+
var MAX_BIP32_INDEX = 2147483647;
|
|
3088
|
+
function deriveFromKmsSeed(seed, options) {
|
|
3089
|
+
if (!(seed instanceof Uint8Array)) {
|
|
3090
|
+
throw new KmsIdentityError(
|
|
3091
|
+
`KMS seed unavailable: expected Uint8Array, got ${seed === null ? "null" : typeof seed}`
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
if (seed.length !== 32) {
|
|
3095
|
+
throw new KmsIdentityError(
|
|
3096
|
+
`KMS seed invalid: expected 32 bytes, got ${seed.length} bytes`
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
const accountIndex = options?.accountIndex ?? 0;
|
|
3100
|
+
if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
|
|
3101
|
+
throw new KmsIdentityError(
|
|
3102
|
+
`Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
const path2 = `m/44'/1237'/0'/0/${accountIndex}`;
|
|
3106
|
+
let derivationSeed;
|
|
3107
|
+
let masterKey;
|
|
3108
|
+
let childKey;
|
|
3109
|
+
try {
|
|
3110
|
+
if (options?.mnemonic !== void 0) {
|
|
3111
|
+
if (!validateMnemonic(options.mnemonic, wordlist)) {
|
|
3112
|
+
throw new KmsIdentityError(
|
|
3113
|
+
"Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase"
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
derivationSeed = mnemonicToSeedSync(options.mnemonic);
|
|
3117
|
+
} else {
|
|
3118
|
+
derivationSeed = seed;
|
|
3119
|
+
}
|
|
3120
|
+
masterKey = HDKey.fromMasterSeed(derivationSeed);
|
|
3121
|
+
childKey = masterKey.derive(path2);
|
|
3122
|
+
if (!childKey.privateKey) {
|
|
3123
|
+
throw new KmsIdentityError(
|
|
3124
|
+
`Failed to derive private key at path ${path2}`
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
const secretKey = childKey.privateKey;
|
|
3128
|
+
const pubkey = getPublicKey5(secretKey);
|
|
3129
|
+
return { secretKey: new Uint8Array(secretKey), pubkey };
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
if (error instanceof KmsIdentityError) {
|
|
3132
|
+
throw error;
|
|
3133
|
+
}
|
|
3134
|
+
throw new KmsIdentityError(
|
|
3135
|
+
`KMS key derivation failed at path ${path2}: ${error instanceof Error ? error.message : String(error)}`,
|
|
3136
|
+
error instanceof Error ? error : void 0
|
|
3137
|
+
);
|
|
3138
|
+
} finally {
|
|
3139
|
+
masterKey?.wipePrivateData();
|
|
3140
|
+
childKey?.wipePrivateData();
|
|
3141
|
+
if (options?.mnemonic !== void 0 && derivationSeed) {
|
|
3142
|
+
derivationSeed.fill(0);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
// src/build/nix-builder.ts
|
|
3148
|
+
import { execFile as execFileCb } from "child_process";
|
|
3149
|
+
import { readFile, mkdtemp, cp, writeFile, rm } from "fs/promises";
|
|
3150
|
+
import { tmpdir } from "os";
|
|
3151
|
+
import path from "path";
|
|
3152
|
+
import { createHash as createHash2 } from "crypto";
|
|
3153
|
+
function execFileAsync(cmd, args, options) {
|
|
3154
|
+
return new Promise((resolve, reject) => {
|
|
3155
|
+
execFileCb(cmd, args, options, (error, stdout, stderr) => {
|
|
3156
|
+
if (error) {
|
|
3157
|
+
reject(error);
|
|
3158
|
+
} else {
|
|
3159
|
+
resolve({ stdout: stdout.toString(), stderr: stderr.toString() });
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
var NixBuilder = class {
|
|
3165
|
+
config;
|
|
3166
|
+
constructor(config) {
|
|
3167
|
+
this.config = config;
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Execute a Nix build and return the result.
|
|
3171
|
+
*
|
|
3172
|
+
* Shells out to `nix build .#docker-image` in the project root directory.
|
|
3173
|
+
* If sourceOverride is configured, creates a temporary modified source tree.
|
|
3174
|
+
*
|
|
3175
|
+
* @throws Error if Nix is not installed or the build fails
|
|
3176
|
+
*/
|
|
3177
|
+
async build() {
|
|
3178
|
+
let buildDir = this.config.projectRoot;
|
|
3179
|
+
let tempDir;
|
|
3180
|
+
try {
|
|
3181
|
+
if (this.config.sourceOverride) {
|
|
3182
|
+
tempDir = await mkdtemp(path.join(tmpdir(), "toon-nix-"));
|
|
3183
|
+
await cp(this.config.projectRoot, tempDir, { recursive: true });
|
|
3184
|
+
for (const [relativePath, content] of Object.entries(
|
|
3185
|
+
this.config.sourceOverride
|
|
3186
|
+
)) {
|
|
3187
|
+
const fullPath = path.resolve(tempDir, relativePath);
|
|
3188
|
+
if (!fullPath.startsWith(tempDir + path.sep) && fullPath !== tempDir) {
|
|
3189
|
+
throw new Error(
|
|
3190
|
+
`sourceOverride path traversal detected: "${relativePath}" resolves outside temp directory`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
await writeFile(fullPath, content, "utf-8");
|
|
3194
|
+
}
|
|
3195
|
+
buildDir = tempDir;
|
|
3196
|
+
}
|
|
3197
|
+
const { stdout } = await execFileAsync(
|
|
3198
|
+
"nix",
|
|
3199
|
+
["build", ".#docker-image", "--print-out-paths"],
|
|
3200
|
+
{
|
|
3201
|
+
cwd: buildDir,
|
|
3202
|
+
timeout: 6e5
|
|
3203
|
+
// 10 minutes -- Nix builds can be slow on first run
|
|
3204
|
+
}
|
|
3205
|
+
);
|
|
3206
|
+
const imagePath = stdout.trim();
|
|
3207
|
+
if (!imagePath.startsWith("/nix/store/")) {
|
|
3208
|
+
throw new Error(
|
|
3209
|
+
`Unexpected Nix build output path: ${imagePath} (expected /nix/store/...)`
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
const imageData = await readFile(imagePath);
|
|
3213
|
+
const sha256 = createHash2("sha256").update(imageData).digest("hex");
|
|
3214
|
+
const imageHash = `sha256:${sha256}`;
|
|
3215
|
+
const pcr0 = createHash2("sha384").update(imageData).digest("hex");
|
|
3216
|
+
const KERNEL_REGION_SIZE = 1024 * 1024;
|
|
3217
|
+
const kernelRegion = imageData.subarray(
|
|
3218
|
+
0,
|
|
3219
|
+
Math.min(KERNEL_REGION_SIZE, imageData.length)
|
|
3220
|
+
);
|
|
3221
|
+
const pcr1 = createHash2("sha384").update(kernelRegion).digest("hex");
|
|
3222
|
+
const appRegion = imageData.subarray(
|
|
3223
|
+
Math.min(KERNEL_REGION_SIZE, imageData.length)
|
|
3224
|
+
);
|
|
3225
|
+
const pcr2 = appRegion.length > 0 ? createHash2("sha384").update(appRegion).digest("hex") : createHash2("sha384").update("pcr2:").update(imageData).digest("hex");
|
|
3226
|
+
return {
|
|
3227
|
+
imageHash,
|
|
3228
|
+
pcr0,
|
|
3229
|
+
pcr1,
|
|
3230
|
+
pcr2,
|
|
3231
|
+
imagePath,
|
|
3232
|
+
buildTimestamp: Math.floor(Date.now() / 1e3)
|
|
3233
|
+
};
|
|
3234
|
+
} finally {
|
|
3235
|
+
if (tempDir) {
|
|
3236
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
|
|
3243
|
+
// src/build/pcr-validator.ts
|
|
3244
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3245
|
+
var PcrReproducibilityError = class extends ToonError {
|
|
3246
|
+
constructor(buildA, buildB) {
|
|
3247
|
+
super(
|
|
3248
|
+
`PCR reproducibility check failed: pcr0 buildA=${buildA.pcr0} buildB=${buildB.pcr0}, pcr1 buildA=${buildA.pcr1} buildB=${buildB.pcr1}, pcr2 buildA=${buildA.pcr2} buildB=${buildB.pcr2}`,
|
|
3249
|
+
"PCR_REPRODUCIBILITY_ERROR"
|
|
3250
|
+
);
|
|
3251
|
+
this.name = "PcrReproducibilityError";
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
async function readDockerfileNix(filePath) {
|
|
3255
|
+
return readFile2(filePath, "utf-8");
|
|
3256
|
+
}
|
|
3257
|
+
function analyzeDockerfileForNonDeterminism(content, forbiddenPatterns) {
|
|
3258
|
+
const lines = content.split("\n");
|
|
3259
|
+
const violations = [];
|
|
3260
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3261
|
+
const line = lines[i];
|
|
3262
|
+
if (line === void 0) continue;
|
|
3263
|
+
const trimmed = line.trim();
|
|
3264
|
+
if (trimmed.startsWith("#")) continue;
|
|
3265
|
+
for (const fp of forbiddenPatterns) {
|
|
3266
|
+
fp.pattern.lastIndex = 0;
|
|
3267
|
+
const match = fp.pattern.exec(line);
|
|
3268
|
+
if (match) {
|
|
3269
|
+
violations.push({
|
|
3270
|
+
line: i + 1,
|
|
3271
|
+
// 1-indexed line numbers
|
|
3272
|
+
patternName: fp.name,
|
|
3273
|
+
matchedText: match[0]
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
return {
|
|
3279
|
+
deterministic: violations.length === 0,
|
|
3280
|
+
violations,
|
|
3281
|
+
scannedLines: lines.length
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
async function verifyPcrReproducibility(buildA, buildB, options) {
|
|
3285
|
+
const pcr0A = buildA.pcr0.toLowerCase();
|
|
3286
|
+
const pcr0B = buildB.pcr0.toLowerCase();
|
|
3287
|
+
const pcr1A = buildA.pcr1.toLowerCase();
|
|
3288
|
+
const pcr1B = buildB.pcr1.toLowerCase();
|
|
3289
|
+
const pcr2A = buildA.pcr2.toLowerCase();
|
|
3290
|
+
const pcr2B = buildB.pcr2.toLowerCase();
|
|
3291
|
+
const hashA = buildA.imageHash.toLowerCase();
|
|
3292
|
+
const hashB = buildB.imageHash.toLowerCase();
|
|
3293
|
+
const pcr0Match = pcr0A === pcr0B;
|
|
3294
|
+
const pcr1Match = pcr1A === pcr1B;
|
|
3295
|
+
const pcr2Match = pcr2A === pcr2B;
|
|
3296
|
+
const imageHashMatch = hashA === hashB;
|
|
3297
|
+
const reproducible = pcr0Match && pcr1Match && pcr2Match && imageHashMatch;
|
|
3298
|
+
const status = reproducible ? "PASS" : "FAIL";
|
|
3299
|
+
const summaryLines = [
|
|
3300
|
+
`PCR reproducibility: ${status}`,
|
|
3301
|
+
` PCR0: ${pcr0Match ? "match" : "MISMATCH"} (${pcr0A})`,
|
|
3302
|
+
` PCR1: ${pcr1Match ? "match" : "MISMATCH"} (${pcr1A})`,
|
|
3303
|
+
` PCR2: ${pcr2Match ? "match" : "MISMATCH"} (${pcr2A})`,
|
|
3304
|
+
` Image hash: ${imageHashMatch ? "match" : "MISMATCH"} (${hashA})`
|
|
3305
|
+
];
|
|
3306
|
+
if (!reproducible) {
|
|
3307
|
+
if (!pcr0Match) {
|
|
3308
|
+
summaryLines.push(` PCR0 buildB: ${pcr0B}`);
|
|
3309
|
+
}
|
|
3310
|
+
if (!pcr1Match) {
|
|
3311
|
+
summaryLines.push(` PCR1 buildB: ${pcr1B}`);
|
|
3312
|
+
}
|
|
3313
|
+
if (!pcr2Match) {
|
|
3314
|
+
summaryLines.push(` PCR2 buildB: ${pcr2B}`);
|
|
3315
|
+
}
|
|
3316
|
+
if (!imageHashMatch) {
|
|
3317
|
+
summaryLines.push(` Image hash buildB: ${hashB}`);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
const result = {
|
|
3321
|
+
reproducible,
|
|
3322
|
+
pcr0Match,
|
|
3323
|
+
pcr1Match,
|
|
3324
|
+
pcr2Match,
|
|
3325
|
+
imageHashMatch,
|
|
3326
|
+
details: {
|
|
3327
|
+
buildA: {
|
|
3328
|
+
pcr0: buildA.pcr0,
|
|
3329
|
+
pcr1: buildA.pcr1,
|
|
3330
|
+
pcr2: buildA.pcr2,
|
|
3331
|
+
imageHash: buildA.imageHash
|
|
3332
|
+
},
|
|
3333
|
+
buildB: {
|
|
3334
|
+
pcr0: buildB.pcr0,
|
|
3335
|
+
pcr1: buildB.pcr1,
|
|
3336
|
+
pcr2: buildB.pcr2,
|
|
3337
|
+
imageHash: buildB.imageHash
|
|
3338
|
+
}
|
|
3339
|
+
},
|
|
3340
|
+
summary: summaryLines.join("\n")
|
|
3341
|
+
};
|
|
3342
|
+
if (!reproducible && options?.throwOnMismatch) {
|
|
3343
|
+
throw new PcrReproducibilityError(buildA, buildB);
|
|
3344
|
+
}
|
|
3345
|
+
return result;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/logger.ts
|
|
3349
|
+
var LOG_LEVEL_PRIORITY = {
|
|
3350
|
+
debug: 0,
|
|
3351
|
+
info: 1,
|
|
3352
|
+
warn: 2,
|
|
3353
|
+
error: 3
|
|
3354
|
+
};
|
|
3355
|
+
function createLogger(config) {
|
|
3356
|
+
const minLevel = config.level ?? "info";
|
|
3357
|
+
const jsonOutput = config.json ?? true;
|
|
3358
|
+
const baseContext = config.context ?? {};
|
|
3359
|
+
const component = config.component;
|
|
3360
|
+
function shouldLog(level) {
|
|
3361
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[minLevel];
|
|
3362
|
+
}
|
|
3363
|
+
function formatEntry(level, msg, fields) {
|
|
3364
|
+
return {
|
|
3365
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3366
|
+
level,
|
|
3367
|
+
component,
|
|
3368
|
+
msg,
|
|
3369
|
+
...baseContext,
|
|
3370
|
+
...fields
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
function emit(level, msg, fields) {
|
|
3374
|
+
if (!shouldLog(level)) return;
|
|
3375
|
+
const entry = formatEntry(level, msg, fields);
|
|
3376
|
+
if (jsonOutput) {
|
|
3377
|
+
const output = JSON.stringify(entry);
|
|
3378
|
+
if (level === "error") {
|
|
3379
|
+
console.error(output);
|
|
3380
|
+
} else if (level === "warn") {
|
|
3381
|
+
console.warn(output);
|
|
3382
|
+
} else {
|
|
3383
|
+
console.log(output);
|
|
3384
|
+
}
|
|
3385
|
+
} else {
|
|
3386
|
+
const contextStr = Object.keys({ ...baseContext, ...fields }).length > 0 ? " " + JSON.stringify({ ...baseContext, ...fields }) : "";
|
|
3387
|
+
const prefix = `[${level.toUpperCase()}] [${component}]`;
|
|
3388
|
+
const output = `${prefix} ${msg}${contextStr}`;
|
|
3389
|
+
if (level === "error") {
|
|
3390
|
+
console.error(output);
|
|
3391
|
+
} else if (level === "warn") {
|
|
3392
|
+
console.warn(output);
|
|
3393
|
+
} else {
|
|
3394
|
+
console.log(output);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
const logger = {
|
|
3399
|
+
debug: (msg, fields) => emit("debug", msg, fields),
|
|
3400
|
+
info: (msg, fields) => emit("info", msg, fields),
|
|
3401
|
+
warn: (msg, fields) => emit("warn", msg, fields),
|
|
3402
|
+
error: (msg, fields) => emit("error", msg, fields),
|
|
3403
|
+
child: (context) => createLogger({
|
|
3404
|
+
component,
|
|
3405
|
+
level: minLevel,
|
|
3406
|
+
json: jsonOutput,
|
|
3407
|
+
context: { ...baseContext, ...context }
|
|
3408
|
+
})
|
|
3409
|
+
};
|
|
3410
|
+
return logger;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// src/index.ts
|
|
3414
|
+
var VERSION = "0.1.0";
|
|
3415
|
+
export {
|
|
3416
|
+
ArDrivePeerRegistry,
|
|
3417
|
+
AttestationBootstrap,
|
|
3418
|
+
AttestationState,
|
|
3419
|
+
AttestationVerifier,
|
|
3420
|
+
BootstrapError,
|
|
3421
|
+
BootstrapService,
|
|
3422
|
+
CHAIN_PRESETS,
|
|
3423
|
+
GenesisPeerLoader,
|
|
3424
|
+
ILP_PEER_INFO_KIND,
|
|
3425
|
+
IMAGE_GENERATION_KIND,
|
|
3426
|
+
InvalidEventError,
|
|
3427
|
+
JOB_FEEDBACK_KIND,
|
|
3428
|
+
JOB_REQUEST_KIND_BASE,
|
|
3429
|
+
JOB_RESULT_KIND_BASE,
|
|
3430
|
+
KmsIdentityError,
|
|
3431
|
+
MOCK_USDC_ADDRESS,
|
|
3432
|
+
MOCK_USDC_CONFIG,
|
|
3433
|
+
NixBuilder,
|
|
3434
|
+
NostrPeerDiscovery,
|
|
3435
|
+
PcrReproducibilityError,
|
|
3436
|
+
PeerDiscoveryError,
|
|
3437
|
+
SEED_RELAY_LIST_KIND,
|
|
3438
|
+
SERVICE_DISCOVERY_KIND,
|
|
3439
|
+
SeedRelayDiscovery,
|
|
3440
|
+
SocialPeerDiscovery,
|
|
3441
|
+
TEE_ATTESTATION_KIND,
|
|
3442
|
+
TEXT_GENERATION_KIND,
|
|
3443
|
+
TEXT_TO_SPEECH_KIND,
|
|
3444
|
+
TRANSLATION_KIND,
|
|
3445
|
+
ToonDecodeError,
|
|
3446
|
+
ToonEncodeError,
|
|
3447
|
+
ToonError,
|
|
3448
|
+
USDC_DECIMALS,
|
|
3449
|
+
USDC_NAME,
|
|
3450
|
+
USDC_SYMBOL,
|
|
3451
|
+
VERSION,
|
|
3452
|
+
analyzeDockerfileForNonDeterminism,
|
|
3453
|
+
buildAttestationEvent,
|
|
3454
|
+
buildEip712Domain,
|
|
3455
|
+
buildIlpPeerInfoEvent,
|
|
3456
|
+
buildIlpPrepare,
|
|
3457
|
+
buildJobFeedbackEvent,
|
|
3458
|
+
buildJobRequestEvent,
|
|
3459
|
+
buildJobResultEvent,
|
|
3460
|
+
buildSeedRelayListEvent,
|
|
3461
|
+
buildServiceDiscoveryEvent,
|
|
3462
|
+
createAgentRuntimeClient,
|
|
3463
|
+
createDirectChannelClient,
|
|
3464
|
+
createDirectConnectorAdmin,
|
|
3465
|
+
createDirectIlpClient,
|
|
3466
|
+
createDirectRuntimeClient,
|
|
3467
|
+
createDiscoveryTracker,
|
|
3468
|
+
createHttpChannelClient,
|
|
3469
|
+
createHttpConnectorAdmin,
|
|
3470
|
+
createHttpIlpClient,
|
|
3471
|
+
createHttpRuntimeClient,
|
|
3472
|
+
createHttpIlpClient2 as createHttpRuntimeClientV2,
|
|
3473
|
+
createLogger,
|
|
3474
|
+
createToonNode,
|
|
3475
|
+
decodeEventFromToon,
|
|
3476
|
+
deriveFromKmsSeed,
|
|
3477
|
+
encodeEventToToon,
|
|
3478
|
+
encodeEventToToonString,
|
|
3479
|
+
negotiateSettlementChain,
|
|
3480
|
+
parseAttestation,
|
|
3481
|
+
parseIlpPeerInfo,
|
|
3482
|
+
parseJobFeedback,
|
|
3483
|
+
parseJobRequest,
|
|
3484
|
+
parseJobResult,
|
|
3485
|
+
parseSeedRelayList,
|
|
3486
|
+
parseServiceDiscovery,
|
|
3487
|
+
publishSeedRelayEntry,
|
|
3488
|
+
readDockerfileNix,
|
|
3489
|
+
resolveChainConfig,
|
|
3490
|
+
resolveTokenForChain,
|
|
3491
|
+
shallowParseToon,
|
|
3492
|
+
validateChainId,
|
|
3493
|
+
verifyPcrReproducibility
|
|
3494
|
+
};
|
|
3495
|
+
//# sourceMappingURL=index.js.map
|