@toon-protocol/sdk 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,945 @@
1
+ // src/identity.ts
2
+ import {
3
+ generateMnemonic as _generateMnemonic,
4
+ validateMnemonic,
5
+ mnemonicToSeedSync
6
+ } from "@scure/bip39";
7
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
8
+ import { HDKey } from "@scure/bip32";
9
+ import { getPublicKey } from "nostr-tools/pure";
10
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
11
+ import { keccak_256 } from "@noble/hashes/sha3.js";
12
+ import { bytesToHex } from "@noble/hashes/utils.js";
13
+
14
+ // src/errors.ts
15
+ import { ToonError } from "@toon-protocol/core";
16
+ var IdentityError = class extends ToonError {
17
+ constructor(message, cause) {
18
+ super(message, "IDENTITY_ERROR", cause);
19
+ this.name = "IdentityError";
20
+ }
21
+ };
22
+ var NodeError = class extends ToonError {
23
+ constructor(message, cause) {
24
+ super(message, "NODE_ERROR", cause);
25
+ this.name = "NodeError";
26
+ }
27
+ };
28
+ var HandlerError = class extends ToonError {
29
+ constructor(message, cause) {
30
+ super(message, "HANDLER_ERROR", cause);
31
+ this.name = "HandlerError";
32
+ }
33
+ };
34
+ var VerificationError = class extends ToonError {
35
+ constructor(message, cause) {
36
+ super(message, "VERIFICATION_ERROR", cause);
37
+ this.name = "VerificationError";
38
+ }
39
+ };
40
+ var PricingError = class extends ToonError {
41
+ constructor(message, cause) {
42
+ super(message, "PRICING_ERROR", cause);
43
+ this.name = "PricingError";
44
+ }
45
+ };
46
+
47
+ // src/identity.ts
48
+ function generateMnemonic() {
49
+ return _generateMnemonic(wordlist, 128);
50
+ }
51
+ function fromMnemonic(mnemonic, options) {
52
+ if (!validateMnemonic(mnemonic, wordlist)) {
53
+ throw new IdentityError(
54
+ `Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase`
55
+ );
56
+ }
57
+ const accountIndex = options?.accountIndex ?? 0;
58
+ if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
59
+ throw new IdentityError(
60
+ `Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
61
+ );
62
+ }
63
+ const path = `m/44'/1237'/0'/0/${accountIndex}`;
64
+ let seed;
65
+ try {
66
+ seed = mnemonicToSeedSync(mnemonic);
67
+ const hdKey = HDKey.fromMasterSeed(seed).derive(path);
68
+ if (!hdKey.privateKey) {
69
+ throw new IdentityError(`Failed to derive private key at path ${path}`);
70
+ }
71
+ const secretKey = hdKey.privateKey;
72
+ return deriveIdentity(secretKey);
73
+ } catch (error) {
74
+ if (error instanceof IdentityError) {
75
+ throw error;
76
+ }
77
+ throw new IdentityError(
78
+ `Key derivation failed at path ${path}: ${error instanceof Error ? error.message : String(error)}`,
79
+ error instanceof Error ? error : void 0
80
+ );
81
+ } finally {
82
+ if (seed) {
83
+ seed.fill(0);
84
+ }
85
+ }
86
+ }
87
+ function fromSecretKey(secretKey) {
88
+ if (!(secretKey instanceof Uint8Array)) {
89
+ throw new IdentityError(
90
+ `Invalid secret key: expected Uint8Array, got ${secretKey === null ? "null" : typeof secretKey}`
91
+ );
92
+ }
93
+ if (secretKey.length !== 32) {
94
+ throw new IdentityError(
95
+ `Invalid secret key: expected 32 bytes, got ${secretKey.length} bytes`
96
+ );
97
+ }
98
+ try {
99
+ return deriveIdentity(secretKey);
100
+ } catch (error) {
101
+ if (error instanceof IdentityError) {
102
+ throw error;
103
+ }
104
+ throw new IdentityError(
105
+ `Invalid secret key: ${error instanceof Error ? error.message : String(error)}`,
106
+ error instanceof Error ? error : void 0
107
+ );
108
+ }
109
+ }
110
+ var MAX_BIP32_INDEX = 2147483647;
111
+ function deriveIdentity(secretKey) {
112
+ const pubkey = getPublicKey(secretKey);
113
+ const evmAddress = computeEvmAddress(secretKey);
114
+ return { secretKey: new Uint8Array(secretKey), pubkey, evmAddress };
115
+ }
116
+ function computeEvmAddress(secretKey) {
117
+ const uncompressedPubkey = secp256k1.getPublicKey(secretKey, false);
118
+ const pubkeyWithoutPrefix = uncompressedPubkey.slice(1);
119
+ const hash = keccak_256(pubkeyWithoutPrefix);
120
+ const addressBytes = hash.slice(-20);
121
+ const addressHex = bytesToHex(addressBytes);
122
+ return toChecksumAddress(addressHex);
123
+ }
124
+ function toChecksumAddress(addressHex) {
125
+ const lower = addressHex.toLowerCase();
126
+ const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));
127
+ let checksummed = "0x";
128
+ for (let i = 0; i < 40; i++) {
129
+ const char = lower[i];
130
+ const hashChar = hash[i];
131
+ if (char === void 0 || hashChar === void 0) {
132
+ throw new IdentityError(
133
+ `Unexpected undefined at index ${i} during checksum computation`
134
+ );
135
+ }
136
+ const hashNibble = parseInt(hashChar, 16);
137
+ checksummed += hashNibble >= 8 ? char.toUpperCase() : char;
138
+ }
139
+ return checksummed;
140
+ }
141
+
142
+ // src/handler-context.ts
143
+ function createHandlerContext(options) {
144
+ let cachedEvent;
145
+ return {
146
+ get toon() {
147
+ return options.toon;
148
+ },
149
+ get kind() {
150
+ return options.meta.kind;
151
+ },
152
+ get pubkey() {
153
+ return options.meta.pubkey;
154
+ },
155
+ get amount() {
156
+ return options.amount;
157
+ },
158
+ get destination() {
159
+ return options.destination;
160
+ },
161
+ decode() {
162
+ if (!cachedEvent) {
163
+ cachedEvent = options.toonDecoder(options.toon);
164
+ }
165
+ return cachedEvent;
166
+ },
167
+ accept(metadata) {
168
+ return {
169
+ accept: true,
170
+ fulfillment: "default-fulfillment",
171
+ ...metadata ? { metadata } : {}
172
+ };
173
+ },
174
+ reject(code, message) {
175
+ return {
176
+ accept: false,
177
+ code,
178
+ message
179
+ };
180
+ }
181
+ };
182
+ }
183
+
184
+ // src/handler-registry.ts
185
+ import { JOB_REQUEST_KIND_BASE } from "@toon-protocol/core";
186
+ var HandlerRegistry = class {
187
+ handlers = /* @__PURE__ */ new Map();
188
+ defaultHandler;
189
+ /**
190
+ * Register a handler for a specific event kind.
191
+ * Replaces any existing handler for that kind.
192
+ */
193
+ on(kind, handler) {
194
+ this.handlers.set(kind, handler);
195
+ return this;
196
+ }
197
+ /**
198
+ * Register a default handler for unrecognized kinds.
199
+ */
200
+ onDefault(handler) {
201
+ this.defaultHandler = handler;
202
+ return this;
203
+ }
204
+ /**
205
+ * Returns all registered kind numbers, sorted ascending.
206
+ */
207
+ getRegisteredKinds() {
208
+ return [...this.handlers.keys()].sort((a, b) => a - b);
209
+ }
210
+ /**
211
+ * Returns registered kinds in the DVM request range (5000-5999), sorted ascending.
212
+ * Uses JOB_REQUEST_KIND_BASE (5000) as the range start.
213
+ */
214
+ getDvmKinds() {
215
+ const dvmRangeStart = JOB_REQUEST_KIND_BASE;
216
+ const dvmRangeEnd = JOB_REQUEST_KIND_BASE + 999;
217
+ return this.getRegisteredKinds().filter(
218
+ (k) => k >= dvmRangeStart && k <= dvmRangeEnd
219
+ );
220
+ }
221
+ /**
222
+ * Dispatch a context to the appropriate handler based on kind.
223
+ */
224
+ async dispatch(ctx) {
225
+ const handler = this.handlers.get(ctx.kind);
226
+ if (handler) {
227
+ return handler(ctx);
228
+ }
229
+ if (this.defaultHandler) {
230
+ return this.defaultHandler(ctx);
231
+ }
232
+ return {
233
+ accept: false,
234
+ code: "F00",
235
+ message: `No handler registered for kind ${ctx.kind}`
236
+ };
237
+ }
238
+ };
239
+
240
+ // src/pricing-validator.ts
241
+ function createPricingValidator(config) {
242
+ const basePricePerByte = config.basePricePerByte ?? 10n;
243
+ return {
244
+ validate(meta, amount) {
245
+ if (meta.pubkey === config.ownPubkey) {
246
+ return { accepted: true };
247
+ }
248
+ const kindOverride = config.kindPricing && Object.hasOwn(config.kindPricing, meta.kind) ? config.kindPricing[meta.kind] : void 0;
249
+ const pricePerByte = kindOverride ?? basePricePerByte;
250
+ const requiredAmount = BigInt(meta.rawBytes.length) * pricePerByte;
251
+ if (amount < requiredAmount) {
252
+ return {
253
+ accepted: false,
254
+ rejection: {
255
+ accept: false,
256
+ code: "F04",
257
+ message: "Insufficient payment",
258
+ metadata: {
259
+ required: requiredAmount.toString(),
260
+ received: amount.toString()
261
+ }
262
+ }
263
+ };
264
+ }
265
+ return { accepted: true };
266
+ }
267
+ };
268
+ }
269
+
270
+ // src/verification-pipeline.ts
271
+ import { schnorr } from "@noble/curves/secp256k1.js";
272
+ import { hexToBytes } from "@noble/hashes/utils.js";
273
+ function createVerificationPipeline(config) {
274
+ return {
275
+ async verify(meta, _toonData) {
276
+ if (config.devMode) {
277
+ return { verified: true };
278
+ }
279
+ try {
280
+ const sigBytes = hexToBytes(meta.sig);
281
+ const msgBytes = hexToBytes(meta.id);
282
+ const pubkeyBytes = hexToBytes(meta.pubkey);
283
+ const valid = schnorr.verify(sigBytes, msgBytes, pubkeyBytes);
284
+ if (!valid) {
285
+ return {
286
+ verified: false,
287
+ rejection: {
288
+ accept: false,
289
+ code: "F06",
290
+ message: "Invalid Schnorr signature"
291
+ }
292
+ };
293
+ }
294
+ return { verified: true };
295
+ } catch {
296
+ return {
297
+ verified: false,
298
+ rejection: {
299
+ accept: false,
300
+ code: "F06",
301
+ message: "Signature verification failed"
302
+ }
303
+ };
304
+ }
305
+ }
306
+ };
307
+ }
308
+
309
+ // src/payment-handler-bridge.ts
310
+ function createPaymentHandlerBridge(config) {
311
+ const { registry } = config;
312
+ return {
313
+ async handlePayment(request) {
314
+ let amount;
315
+ try {
316
+ amount = BigInt(request.amount);
317
+ } catch {
318
+ return {
319
+ accept: false,
320
+ code: "T00",
321
+ message: "Invalid payment amount"
322
+ };
323
+ }
324
+ const ctx = createHandlerContext({
325
+ toon: request.data,
326
+ meta: {
327
+ kind: 0,
328
+ pubkey: "",
329
+ id: "",
330
+ sig: "",
331
+ rawBytes: new Uint8Array(0)
332
+ },
333
+ amount,
334
+ destination: request.destination,
335
+ toonDecoder: () => {
336
+ throw new Error("decode not available in bridge");
337
+ }
338
+ });
339
+ if (request.isTransit) {
340
+ void registry.dispatch(ctx).catch((err) => {
341
+ const errMsg = err instanceof Error ? err.message : "Unknown error";
342
+ console.error("Transit handler error:", errMsg);
343
+ });
344
+ return { accept: true };
345
+ }
346
+ try {
347
+ return await registry.dispatch(ctx);
348
+ } catch (err) {
349
+ const errMsg = err instanceof Error ? err.message : "Unknown error";
350
+ console.error("Handler error:", errMsg);
351
+ return {
352
+ accept: false,
353
+ code: "T00",
354
+ message: "Internal error"
355
+ };
356
+ }
357
+ }
358
+ };
359
+ }
360
+
361
+ // src/event-storage-handler.ts
362
+ function createEventStorageHandler(_config) {
363
+ throw new Error(
364
+ "createEventStorageHandler is not yet implemented in @toon-protocol/sdk. Use @toon-protocol/town for the relay implementation."
365
+ );
366
+ }
367
+
368
+ // src/create-node.ts
369
+ import { createServer } from "http";
370
+ import {
371
+ createToonNode,
372
+ BootstrapService,
373
+ createDiscoveryTracker,
374
+ createHttpIlpClient,
375
+ createHttpConnectorAdmin,
376
+ createHttpChannelClient,
377
+ resolveChainConfig,
378
+ buildIlpPrepare,
379
+ buildJobFeedbackEvent,
380
+ buildJobResultEvent,
381
+ parseJobResult
382
+ } from "@toon-protocol/core";
383
+ import {
384
+ shallowParseToon,
385
+ decodeEventFromToon,
386
+ encodeEventToToon
387
+ } from "@toon-protocol/core/toon";
388
+
389
+ // src/skill-descriptor.ts
390
+ function buildSkillDescriptor(registry, config = {}) {
391
+ const dvmKinds = registry.getDvmKinds();
392
+ if (dvmKinds.length === 0) {
393
+ return void 0;
394
+ }
395
+ const basePricePerByte = config.basePricePerByte ?? 10n;
396
+ const kindPricing = config.kindPricing ?? {};
397
+ const pricing = {};
398
+ for (const kind of dvmKinds) {
399
+ if (Object.hasOwn(kindPricing, kind)) {
400
+ pricing[String(kind)] = String(kindPricing[kind]);
401
+ } else {
402
+ pricing[String(kind)] = String(basePricePerByte);
403
+ }
404
+ }
405
+ const descriptor = {
406
+ name: config.name ?? "toon-dvm",
407
+ version: config.version ?? "1.0",
408
+ kinds: dvmKinds,
409
+ features: config.features ?? [],
410
+ inputSchema: config.inputSchema ?? { type: "object", properties: {} },
411
+ pricing
412
+ };
413
+ if (config.models !== void 0) {
414
+ descriptor.models = config.models;
415
+ }
416
+ return descriptor;
417
+ }
418
+
419
+ // src/create-node.ts
420
+ var MAX_PAYLOAD_BASE64_LENGTH = 1048576;
421
+ function createNode(config) {
422
+ const hasConnector = config.connector !== void 0;
423
+ const hasConnectorUrl = config.connectorUrl !== void 0;
424
+ if (hasConnector && hasConnectorUrl) {
425
+ throw new NodeError(
426
+ "NodeConfig: provide either connector or connectorUrl, not both"
427
+ );
428
+ }
429
+ if (!hasConnector && !hasConnectorUrl) {
430
+ throw new NodeError(
431
+ "NodeConfig: one of connector or connectorUrl is required"
432
+ );
433
+ }
434
+ if (hasConnectorUrl && config.handlerPort === void 0) {
435
+ throw new NodeError(
436
+ "NodeConfig: handlerPort is required when using connectorUrl (standalone mode)"
437
+ );
438
+ }
439
+ const embeddedMode = hasConnector;
440
+ const chainConfig = resolveChainConfig(config.chain);
441
+ let effectiveSettlementInfo = config.settlementInfo;
442
+ if (!effectiveSettlementInfo) {
443
+ const chainKey = `evm:base:${chainConfig.chainId}`;
444
+ const supportedChains = [chainKey];
445
+ const preferredTokens = {
446
+ [chainKey]: chainConfig.usdcAddress
447
+ };
448
+ const tokenNetworks = chainConfig.tokenNetworkAddress ? { [chainKey]: chainConfig.tokenNetworkAddress } : void 0;
449
+ effectiveSettlementInfo = {
450
+ supportedChains,
451
+ preferredTokens,
452
+ ...tokenNetworks && { tokenNetworks }
453
+ };
454
+ }
455
+ let identity;
456
+ try {
457
+ identity = fromSecretKey(config.secretKey);
458
+ } catch (error) {
459
+ throw new NodeError(
460
+ `Invalid secretKey: ${error instanceof Error ? error.message : String(error)}`,
461
+ error instanceof Error ? error : void 0
462
+ );
463
+ }
464
+ const { pubkey, evmAddress } = identity;
465
+ const registry = new HandlerRegistry();
466
+ if (config.handlers) {
467
+ for (const [kind, handler] of Object.entries(config.handlers)) {
468
+ const kindNum = Number(kind);
469
+ if (!Number.isInteger(kindNum) || kindNum < 0) {
470
+ throw new NodeError(
471
+ `Invalid event kind in handlers config: expected a non-negative integer, got '${kind}'`
472
+ );
473
+ }
474
+ registry.on(kindNum, handler);
475
+ }
476
+ }
477
+ if (config.defaultHandler) {
478
+ registry.onDefault(config.defaultHandler);
479
+ }
480
+ const verifier = createVerificationPipeline({
481
+ devMode: config.devMode ?? false
482
+ });
483
+ const pricer = createPricingValidator({
484
+ basePricePerByte: config.basePricePerByte ?? 10n,
485
+ ownPubkey: pubkey,
486
+ kindPricing: config.kindPricing
487
+ });
488
+ const encoder = config.toonEncoder ?? encodeEventToToon;
489
+ const decoder = config.toonDecoder ?? decodeEventFromToon;
490
+ const contextDecoder = (toon) => {
491
+ const bytes = Buffer.from(toon, "base64");
492
+ return decoder(bytes);
493
+ };
494
+ const trackerRef = {};
495
+ const pipelinedHandler = async (request) => {
496
+ if (request.data.length > MAX_PAYLOAD_BASE64_LENGTH) {
497
+ return {
498
+ accept: false,
499
+ code: "F06",
500
+ message: `Payload too large: ${request.data.length} bytes exceeds maximum ${MAX_PAYLOAD_BASE64_LENGTH}`
501
+ };
502
+ }
503
+ const toonBytes = Buffer.from(request.data, "base64");
504
+ let meta;
505
+ try {
506
+ meta = shallowParseToon(toonBytes);
507
+ } catch {
508
+ return {
509
+ accept: false,
510
+ code: "F06",
511
+ message: "Invalid TOON payload: failed to parse routing metadata"
512
+ };
513
+ }
514
+ if (config.devMode ?? false) {
515
+ const toonPreview = request.data.length > 80 ? request.data.substring(0, 80) + "..." : request.data;
516
+ const sanitize = (s) => s.replace(/[\x00-\x1f\x7f]/g, "");
517
+ console.log(
518
+ "[toon:dev]",
519
+ `kind=${meta.kind}`,
520
+ `pubkey=${meta.pubkey.substring(0, 16)}...`,
521
+ `amount=${sanitize(request.amount)}`,
522
+ `dest=${sanitize(request.destination)}`,
523
+ `toon=${toonPreview}`
524
+ );
525
+ }
526
+ const verifyResult = await verifier.verify(meta, request.data);
527
+ if (!verifyResult.verified) {
528
+ if (verifyResult.rejection) {
529
+ return verifyResult.rejection;
530
+ }
531
+ return { accept: false, code: "F06", message: "Verification failed" };
532
+ }
533
+ let amount;
534
+ try {
535
+ amount = BigInt(request.amount);
536
+ } catch {
537
+ if (config.devMode ?? false) {
538
+ amount = 0n;
539
+ } else {
540
+ return {
541
+ accept: false,
542
+ code: "T00",
543
+ message: "Invalid payment amount"
544
+ };
545
+ }
546
+ }
547
+ if (!(config.devMode ?? false)) {
548
+ const priceResult = pricer.validate(meta, amount);
549
+ if (!priceResult.accepted) {
550
+ if (priceResult.rejection) {
551
+ return priceResult.rejection;
552
+ }
553
+ return {
554
+ accept: false,
555
+ code: "F04",
556
+ message: "Pricing validation failed"
557
+ };
558
+ }
559
+ }
560
+ const ctx = createHandlerContext({
561
+ toon: request.data,
562
+ meta,
563
+ amount,
564
+ destination: request.destination,
565
+ toonDecoder: contextDecoder
566
+ });
567
+ try {
568
+ const result = await registry.dispatch(ctx);
569
+ if (result.accept && meta.kind === 10032 && trackerRef.current) {
570
+ const decoded = ctx.decode();
571
+ if (decoded) {
572
+ trackerRef.current.processEvent(decoded);
573
+ }
574
+ }
575
+ return result;
576
+ } catch (err) {
577
+ const errMsg = err instanceof Error ? err.message : "Unknown error";
578
+ console.error("Handler dispatch failed:", errMsg);
579
+ return { accept: false, code: "T00", message: "Internal error" };
580
+ }
581
+ };
582
+ const ilpInfo = {
583
+ ilpAddress: config.ilpAddress ?? "g.toon.local",
584
+ btpEndpoint: config.btpEndpoint ?? "",
585
+ assetCode: config.assetCode ?? "USD",
586
+ assetScale: config.assetScale ?? 6
587
+ };
588
+ let ilpClient;
589
+ let adminClient;
590
+ let channelClient = null;
591
+ let bootstrapServiceInstance;
592
+ let discoveryTrackerInstance;
593
+ let httpServer = null;
594
+ let doStart;
595
+ let doStop;
596
+ if (embeddedMode) {
597
+ const toonNode = createToonNode({
598
+ connector: config.connector,
599
+ handlePacket: pipelinedHandler,
600
+ secretKey: config.secretKey,
601
+ ilpInfo,
602
+ toonEncoder: encoder,
603
+ toonDecoder: decoder,
604
+ relayUrl: config.relayUrl,
605
+ knownPeers: config.knownPeers,
606
+ settlementInfo: effectiveSettlementInfo,
607
+ basePricePerByte: config.basePricePerByte,
608
+ ardriveEnabled: config.ardriveEnabled
609
+ });
610
+ ilpClient = toonNode.ilpClient;
611
+ channelClient = toonNode.channelClient;
612
+ bootstrapServiceInstance = toonNode.bootstrapService;
613
+ discoveryTrackerInstance = toonNode.discoveryTracker;
614
+ adminClient = {
615
+ addPeer: () => Promise.resolve(),
616
+ removePeer: () => Promise.resolve()
617
+ };
618
+ trackerRef.current = discoveryTrackerInstance;
619
+ doStart = async () => toonNode.start();
620
+ doStop = async () => toonNode.stop();
621
+ } else {
622
+ const connectorUrl = config.connectorUrl;
623
+ const handlerPort = config.handlerPort;
624
+ ilpClient = createHttpIlpClient(connectorUrl);
625
+ adminClient = createHttpConnectorAdmin(connectorUrl, "");
626
+ if (effectiveSettlementInfo) {
627
+ channelClient = createHttpChannelClient(connectorUrl);
628
+ }
629
+ bootstrapServiceInstance = new BootstrapService(
630
+ {
631
+ knownPeers: config.knownPeers ?? [],
632
+ ardriveEnabled: config.ardriveEnabled ?? false,
633
+ defaultRelayUrl: config.relayUrl ?? "",
634
+ settlementInfo: effectiveSettlementInfo,
635
+ ownIlpAddress: ilpInfo.ilpAddress,
636
+ toonEncoder: encoder,
637
+ toonDecoder: decoder,
638
+ basePricePerByte: config.basePricePerByte ?? 10n
639
+ },
640
+ config.secretKey,
641
+ ilpInfo
642
+ );
643
+ bootstrapServiceInstance.setIlpClient(ilpClient);
644
+ bootstrapServiceInstance.setConnectorAdmin(adminClient);
645
+ if (channelClient) {
646
+ bootstrapServiceInstance.setChannelClient(channelClient);
647
+ }
648
+ discoveryTrackerInstance = createDiscoveryTracker({
649
+ secretKey: config.secretKey,
650
+ settlementInfo: effectiveSettlementInfo
651
+ });
652
+ discoveryTrackerInstance.setConnectorAdmin(adminClient);
653
+ if (channelClient) {
654
+ discoveryTrackerInstance.setChannelClient(channelClient);
655
+ }
656
+ trackerRef.current = discoveryTrackerInstance;
657
+ doStart = async () => {
658
+ httpServer = createServer(async (req, res) => {
659
+ if (req.method === "GET" && req.url === "/health") {
660
+ res.writeHead(200, { "Content-Type": "application/json" });
661
+ res.end(JSON.stringify({ status: "healthy", pubkey }));
662
+ return;
663
+ }
664
+ if (req.method === "POST" && req.url === "/handle-packet") {
665
+ let body = "";
666
+ req.on("data", (chunk) => {
667
+ body += chunk.toString();
668
+ });
669
+ req.on("end", async () => {
670
+ try {
671
+ const request = JSON.parse(body);
672
+ if (request.amount === void 0 || request.amount === null || request.destination === void 0 || request.destination === null || request.data === void 0 || request.data === null) {
673
+ res.writeHead(400, { "Content-Type": "application/json" });
674
+ res.end(
675
+ JSON.stringify({
676
+ accept: false,
677
+ code: "F00",
678
+ message: "Missing required fields"
679
+ })
680
+ );
681
+ return;
682
+ }
683
+ const result = await pipelinedHandler(request);
684
+ res.writeHead(result.accept ? 200 : 400, {
685
+ "Content-Type": "application/json"
686
+ });
687
+ res.end(JSON.stringify(result));
688
+ } catch (err) {
689
+ console.error("[SDK] handle-packet error:", err);
690
+ res.writeHead(500, { "Content-Type": "application/json" });
691
+ res.end(
692
+ JSON.stringify({
693
+ accept: false,
694
+ code: "T00",
695
+ message: "Internal server error"
696
+ })
697
+ );
698
+ }
699
+ });
700
+ return;
701
+ }
702
+ res.writeHead(404);
703
+ res.end();
704
+ });
705
+ await new Promise((resolve) => {
706
+ httpServer?.listen(handlerPort, () => resolve());
707
+ });
708
+ const results = await bootstrapServiceInstance.bootstrap();
709
+ const bootstrapPeerPubkeys = results.map((r) => r.knownPeer.pubkey);
710
+ discoveryTrackerInstance.addExcludedPubkeys(bootstrapPeerPubkeys);
711
+ return {
712
+ bootstrapResults: results,
713
+ peerCount: results.length,
714
+ channelCount: results.filter((r) => r.channelId).length
715
+ };
716
+ };
717
+ doStop = async () => {
718
+ const server = httpServer;
719
+ if (server) {
720
+ await new Promise((resolve) => {
721
+ server.close(() => resolve());
722
+ });
723
+ httpServer = null;
724
+ }
725
+ };
726
+ }
727
+ let started = false;
728
+ const node = {
729
+ get pubkey() {
730
+ return pubkey;
731
+ },
732
+ get evmAddress() {
733
+ return evmAddress;
734
+ },
735
+ get connector() {
736
+ return config.connector ?? null;
737
+ },
738
+ get channelClient() {
739
+ return channelClient;
740
+ },
741
+ getSkillDescriptor() {
742
+ return buildSkillDescriptor(registry, {
743
+ basePricePerByte: config.basePricePerByte ?? 10n,
744
+ kindPricing: config.kindPricing,
745
+ ...config.skillConfig
746
+ });
747
+ },
748
+ on(kindOrEvent, handlerOrListener) {
749
+ if (typeof kindOrEvent === "number") {
750
+ if (!Number.isInteger(kindOrEvent) || kindOrEvent < 0) {
751
+ throw new NodeError(
752
+ `Invalid event kind: expected a non-negative integer, got ${String(kindOrEvent)}`
753
+ );
754
+ }
755
+ registry.on(kindOrEvent, handlerOrListener);
756
+ } else if (kindOrEvent === "bootstrap") {
757
+ const listener = handlerOrListener;
758
+ bootstrapServiceInstance.on(listener);
759
+ discoveryTrackerInstance.on(listener);
760
+ } else {
761
+ const sanitized = String(kindOrEvent).replace(/[\x00-\x1f\x7f]/g, "");
762
+ throw new NodeError(
763
+ `Unknown lifecycle event: '${sanitized}'. Supported: 'bootstrap'`
764
+ );
765
+ }
766
+ return node;
767
+ },
768
+ onDefault(handler) {
769
+ registry.onDefault(handler);
770
+ return node;
771
+ },
772
+ async start() {
773
+ if (started) {
774
+ throw new NodeError("Node already started");
775
+ }
776
+ try {
777
+ const result = await doStart();
778
+ started = true;
779
+ return {
780
+ peerCount: result.peerCount,
781
+ channelCount: result.channelCount,
782
+ bootstrapResults: result.bootstrapResults
783
+ };
784
+ } catch (error) {
785
+ if (error instanceof NodeError) {
786
+ throw error;
787
+ }
788
+ throw new NodeError(
789
+ `Failed to start node: ${error instanceof Error ? error.message : String(error)}`,
790
+ error instanceof Error ? error : void 0
791
+ );
792
+ }
793
+ },
794
+ async stop() {
795
+ if (!started) {
796
+ return;
797
+ }
798
+ await doStop();
799
+ started = false;
800
+ },
801
+ async peerWith(targetPubkey) {
802
+ if (!started) {
803
+ throw new NodeError(
804
+ "Cannot peer: node not started. Call start() first."
805
+ );
806
+ }
807
+ if (typeof targetPubkey !== "string" || targetPubkey.length !== 64 || !/^[0-9a-f]{64}$/.test(targetPubkey)) {
808
+ throw new NodeError(
809
+ "Invalid pubkey: expected a 64-character lowercase hex string"
810
+ );
811
+ }
812
+ return discoveryTrackerInstance.peerWith(targetPubkey);
813
+ },
814
+ async publishEvent(event, options) {
815
+ if (!started) {
816
+ throw new NodeError(
817
+ "Cannot publish: node not started. Call start() first."
818
+ );
819
+ }
820
+ if (!options?.destination) {
821
+ throw new NodeError(
822
+ "Cannot publish: destination is required. Pass { destination: 'g.peer.address' }."
823
+ );
824
+ }
825
+ try {
826
+ const toonData = encoder(event);
827
+ const amount = (config.basePricePerByte ?? 10n) * BigInt(toonData.length);
828
+ const packet = buildIlpPrepare({
829
+ destination: options.destination,
830
+ amount,
831
+ data: toonData
832
+ });
833
+ const result = await ilpClient.sendIlpPacket(packet);
834
+ if (result.accepted) {
835
+ return {
836
+ success: true,
837
+ eventId: event.id,
838
+ fulfillment: result.fulfillment ?? ""
839
+ };
840
+ }
841
+ return {
842
+ success: false,
843
+ eventId: event.id,
844
+ code: result.code ?? "T00",
845
+ message: result.message ?? "Unknown error"
846
+ };
847
+ } catch (error) {
848
+ if (error instanceof NodeError) {
849
+ throw error;
850
+ }
851
+ throw new NodeError(
852
+ `Failed to publish event: ${error instanceof Error ? error.message : String(error)}`,
853
+ error instanceof Error ? error : void 0
854
+ );
855
+ }
856
+ },
857
+ async publishFeedback(requestEventId, customerPubkey, status, content, options) {
858
+ const feedbackEvent = buildJobFeedbackEvent(
859
+ { requestEventId, customerPubkey, status, content },
860
+ config.secretKey
861
+ );
862
+ return node.publishEvent(feedbackEvent, options);
863
+ },
864
+ async publishResult(requestEventId, customerPubkey, amount, content, options) {
865
+ const resultKind = options?.kind ?? 6100;
866
+ const resultEvent = buildJobResultEvent(
867
+ { kind: resultKind, requestEventId, customerPubkey, amount, content },
868
+ config.secretKey
869
+ );
870
+ return node.publishEvent(resultEvent, options);
871
+ },
872
+ async settleCompute(resultEvent, providerIlpAddress, options) {
873
+ if (!started) {
874
+ throw new NodeError(
875
+ "Cannot settle compute: node not started. Call start() first."
876
+ );
877
+ }
878
+ if (!providerIlpAddress || typeof providerIlpAddress !== "string" || providerIlpAddress.trim() === "") {
879
+ throw new NodeError(
880
+ "Cannot settle compute: providerIlpAddress is required. Resolve it from the provider's kind:10035 service discovery event."
881
+ );
882
+ }
883
+ const parsed = parseJobResult(resultEvent);
884
+ if (!parsed) {
885
+ throw new NodeError(
886
+ "Cannot settle compute: failed to parse result event. Ensure the event is a valid Kind 6xxx with an amount tag."
887
+ );
888
+ }
889
+ const computeAmount = parsed.amount;
890
+ let amountBigInt;
891
+ try {
892
+ amountBigInt = BigInt(computeAmount);
893
+ } catch {
894
+ throw new NodeError(
895
+ `Cannot settle compute: result event amount ('${computeAmount}') is not a valid numeric string.`
896
+ );
897
+ }
898
+ if (amountBigInt < 0n) {
899
+ throw new NodeError(
900
+ `Cannot settle compute: result event amount ('${computeAmount}') must be non-negative.`
901
+ );
902
+ }
903
+ if (options?.originalBid !== void 0) {
904
+ let bidBigInt;
905
+ try {
906
+ bidBigInt = BigInt(options.originalBid);
907
+ } catch {
908
+ throw new NodeError(
909
+ `Cannot settle compute: originalBid ('${options.originalBid}') is not a valid numeric string.`
910
+ );
911
+ }
912
+ if (amountBigInt > bidBigInt) {
913
+ throw new NodeError(
914
+ `Cannot settle compute: result amount (${computeAmount}) exceeds original bid (${options.originalBid}). Potential provider overcharge.`
915
+ );
916
+ }
917
+ }
918
+ return ilpClient.sendIlpPacket({
919
+ destination: providerIlpAddress,
920
+ amount: computeAmount,
921
+ data: ""
922
+ });
923
+ }
924
+ };
925
+ return node;
926
+ }
927
+ export {
928
+ HandlerError,
929
+ HandlerRegistry,
930
+ IdentityError,
931
+ NodeError,
932
+ PricingError,
933
+ VerificationError,
934
+ buildSkillDescriptor,
935
+ createEventStorageHandler,
936
+ createHandlerContext,
937
+ createNode,
938
+ createPaymentHandlerBridge,
939
+ createPricingValidator,
940
+ createVerificationPipeline,
941
+ fromMnemonic,
942
+ fromSecretKey,
943
+ generateMnemonic
944
+ };
945
+ //# sourceMappingURL=index.js.map