@noosphere/agent-core 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1875 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/EventMonitor.ts
9
+ import { EventEmitter } from "events";
10
+ import { ethers } from "ethers";
11
+ var EventMonitor = class extends EventEmitter {
12
+ constructor(config, routerAbi, coordinatorAbi, options) {
13
+ super();
14
+ this.config = config;
15
+ this.routerAbi = routerAbi;
16
+ this.coordinatorAbi = coordinatorAbi;
17
+ this.reconnectAttempts = 0;
18
+ this.maxReconnectAttempts = 10;
19
+ this.isReconnecting = false;
20
+ this.heartbeatInterval = null;
21
+ this.lastEventTime = Date.now();
22
+ this.lastProcessedBlock = config.deploymentBlock || 0;
23
+ this.useWebSocket = false;
24
+ this.checkpointCallbacks = options;
25
+ }
26
+ async connect() {
27
+ try {
28
+ if (this.config.wsRpcUrl) {
29
+ this.provider = new ethers.WebSocketProvider(this.config.wsRpcUrl);
30
+ this.useWebSocket = true;
31
+ console.log("\u2713 Connected via WebSocket (push-based events)");
32
+ } else {
33
+ throw new Error("WebSocket URL not provided");
34
+ }
35
+ } catch (error) {
36
+ console.warn("WebSocket unavailable, falling back to HTTP polling");
37
+ this.provider = new ethers.JsonRpcProvider(this.config.rpcUrl);
38
+ this.useWebSocket = false;
39
+ }
40
+ this.router = new ethers.Contract(this.config.routerAddress, this.routerAbi, this.provider);
41
+ this.coordinator = new ethers.Contract(
42
+ this.config.coordinatorAddress,
43
+ this.coordinatorAbi,
44
+ this.provider
45
+ );
46
+ }
47
+ async start() {
48
+ if (this.checkpointCallbacks?.loadCheckpoint) {
49
+ const checkpoint = this.checkpointCallbacks.loadCheckpoint();
50
+ if (checkpoint) {
51
+ this.lastProcessedBlock = checkpoint.blockNumber;
52
+ }
53
+ }
54
+ console.log(`Starting from block ${this.lastProcessedBlock}`);
55
+ await this.replayEvents(this.lastProcessedBlock, "latest");
56
+ if (this.useWebSocket) {
57
+ await this.startWebSocketListening();
58
+ } else {
59
+ await this.startPolling();
60
+ }
61
+ }
62
+ async replayEvents(fromBlock, toBlock) {
63
+ console.log(`Replaying events from block ${fromBlock} to ${toBlock}`);
64
+ const currentBlock = await this.provider.getBlockNumber();
65
+ const toBlockNumber = toBlock === "latest" ? currentBlock : Number(toBlock);
66
+ const chunkSize = 1e4;
67
+ for (let start = fromBlock; start <= toBlockNumber; start += chunkSize) {
68
+ const end = Math.min(start + chunkSize - 1, toBlockNumber);
69
+ const events = await this.coordinator.queryFilter(
70
+ this.coordinator.filters.RequestStarted(),
71
+ start,
72
+ end
73
+ );
74
+ for (const event of events) {
75
+ await this.processEvent(event);
76
+ }
77
+ if (events.length > 0) {
78
+ this.saveCheckpoint(end);
79
+ }
80
+ }
81
+ console.log(`Replayed events up to block ${toBlockNumber}`);
82
+ }
83
+ async startWebSocketListening() {
84
+ console.log("Starting WebSocket event listening...");
85
+ this.coordinator.on("RequestStarted", async (...args) => {
86
+ const event = args[args.length - 1];
87
+ this.lastEventTime = Date.now();
88
+ await this.processEvent(event);
89
+ const blockNumber = event.blockNumber;
90
+ if (blockNumber - this.lastProcessedBlock >= 10) {
91
+ this.saveCheckpoint(blockNumber);
92
+ }
93
+ });
94
+ if (this.provider instanceof ethers.WebSocketProvider) {
95
+ const wsProvider = this.provider;
96
+ const ws = wsProvider._websocket;
97
+ if (ws) {
98
+ ws.on("close", () => {
99
+ console.warn("\u26A0\uFE0F WebSocket connection closed");
100
+ this.handleDisconnect();
101
+ });
102
+ ws.on("error", (error) => {
103
+ console.error("\u26A0\uFE0F WebSocket error:", error.message);
104
+ this.handleDisconnect();
105
+ });
106
+ }
107
+ }
108
+ this.startHeartbeat();
109
+ console.log("\u2713 WebSocket event listener started");
110
+ }
111
+ startHeartbeat() {
112
+ this.heartbeatInterval = setInterval(async () => {
113
+ try {
114
+ if (this.provider instanceof ethers.WebSocketProvider) {
115
+ const blockNumber = await this.provider.getBlockNumber();
116
+ const timeSinceLastEvent = Date.now() - this.lastEventTime;
117
+ if (timeSinceLastEvent > 18e4 && blockNumber > this.lastProcessedBlock + 5) {
118
+ console.log(`\u26A0\uFE0F No events for ${Math.round(timeSinceLastEvent / 1e3)}s, checking for missed events...`);
119
+ await this.replayMissedEvents();
120
+ }
121
+ }
122
+ } catch (error) {
123
+ console.error("\u26A0\uFE0F Heartbeat failed, WebSocket may be disconnected");
124
+ this.handleDisconnect();
125
+ }
126
+ }, 12e4);
127
+ }
128
+ async replayMissedEvents() {
129
+ try {
130
+ const currentBlock = await this.provider.getBlockNumber();
131
+ if (currentBlock > this.lastProcessedBlock) {
132
+ console.log(`\u{1F4E5} Replaying events from block ${this.lastProcessedBlock + 1} to ${currentBlock}`);
133
+ await this.replayEvents(this.lastProcessedBlock + 1, currentBlock);
134
+ this.lastEventTime = Date.now();
135
+ }
136
+ } catch (error) {
137
+ console.error("Failed to replay missed events:", error);
138
+ }
139
+ }
140
+ handleDisconnect() {
141
+ if (this.isReconnecting) return;
142
+ this.isReconnecting = true;
143
+ if (this.heartbeatInterval) {
144
+ clearInterval(this.heartbeatInterval);
145
+ this.heartbeatInterval = null;
146
+ }
147
+ this.reconnect().finally(() => {
148
+ this.isReconnecting = false;
149
+ });
150
+ }
151
+ async startPolling() {
152
+ console.log("Starting HTTP polling (fallback mode)...");
153
+ const pollingInterval = this.config.pollingInterval || 12e3;
154
+ let lastBlock = await this.provider.getBlockNumber();
155
+ setInterval(async () => {
156
+ try {
157
+ const currentBlock = await this.provider.getBlockNumber();
158
+ if (currentBlock > lastBlock) {
159
+ const events = await this.coordinator.queryFilter(
160
+ this.coordinator.filters.RequestStarted(),
161
+ lastBlock + 1,
162
+ currentBlock
163
+ );
164
+ for (const event of events) {
165
+ await this.processEvent(event);
166
+ }
167
+ if (events.length > 0) {
168
+ this.saveCheckpoint(currentBlock);
169
+ }
170
+ lastBlock = currentBlock;
171
+ }
172
+ } catch (error) {
173
+ console.error("Polling error:", error);
174
+ }
175
+ }, pollingInterval);
176
+ }
177
+ async processEvent(event) {
178
+ const commitment = event.args.commitment;
179
+ const requestStartedEvent = {
180
+ requestId: event.args.requestId,
181
+ subscriptionId: event.args.subscriptionId,
182
+ containerId: event.args.containerId,
183
+ interval: commitment.interval,
184
+ redundancy: commitment.redundancy,
185
+ useDeliveryInbox: commitment.useDeliveryInbox,
186
+ feeAmount: commitment.feeAmount,
187
+ feeToken: commitment.feeToken,
188
+ verifier: commitment.verifier,
189
+ coordinator: commitment.coordinator,
190
+ walletAddress: commitment.walletAddress,
191
+ blockNumber: event.blockNumber
192
+ };
193
+ this.emit("RequestStarted", requestStartedEvent);
194
+ }
195
+ async reconnect() {
196
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
197
+ console.error("Max reconnection attempts reached. Falling back to HTTP polling.");
198
+ this.useWebSocket = false;
199
+ await this.connect();
200
+ await this.startPolling();
201
+ return;
202
+ }
203
+ const backoff = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 6e4);
204
+ console.log(
205
+ `\u{1F504} Reconnecting in ${backoff}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`
206
+ );
207
+ await new Promise((resolve) => setTimeout(resolve, backoff));
208
+ try {
209
+ if (this.coordinator) {
210
+ this.coordinator.removeAllListeners();
211
+ }
212
+ if (this.provider instanceof ethers.WebSocketProvider) {
213
+ try {
214
+ await this.provider.destroy();
215
+ } catch {
216
+ }
217
+ }
218
+ await this.connect();
219
+ await this.replayMissedEvents();
220
+ await this.startWebSocketListening();
221
+ console.log("\u2713 Reconnected successfully");
222
+ this.reconnectAttempts = 0;
223
+ this.lastEventTime = Date.now();
224
+ } catch (error) {
225
+ console.error("Reconnection failed:", error);
226
+ this.reconnectAttempts++;
227
+ await this.reconnect();
228
+ }
229
+ }
230
+ saveCheckpoint(blockNumber) {
231
+ this.lastProcessedBlock = blockNumber;
232
+ if (this.checkpointCallbacks?.saveCheckpoint) {
233
+ this.checkpointCallbacks.saveCheckpoint({
234
+ blockNumber,
235
+ blockTimestamp: Date.now()
236
+ });
237
+ }
238
+ }
239
+ async stop() {
240
+ if (this.heartbeatInterval) {
241
+ clearInterval(this.heartbeatInterval);
242
+ this.heartbeatInterval = null;
243
+ }
244
+ if (this.router) {
245
+ this.router.removeAllListeners();
246
+ }
247
+ if (this.coordinator) {
248
+ this.coordinator.removeAllListeners();
249
+ }
250
+ if (this.provider instanceof ethers.WebSocketProvider) {
251
+ await this.provider.destroy();
252
+ }
253
+ }
254
+ };
255
+
256
+ // src/ContainerManager.ts
257
+ import Docker from "dockerode";
258
+ import fs from "fs/promises";
259
+ import path from "path";
260
+ import axios from "axios";
261
+ var ContainerManager = class {
262
+ constructor() {
263
+ this.runningContainers = /* @__PURE__ */ new Set();
264
+ this.persistentContainers = /* @__PURE__ */ new Map();
265
+ this.containerPorts = /* @__PURE__ */ new Map();
266
+ this.docker = new Docker();
267
+ }
268
+ async runContainer(container, input, timeout = 3e5) {
269
+ const startTime = Date.now();
270
+ try {
271
+ const port = container.port ? parseInt(container.port) : 8081;
272
+ const url = `http://localhost:${port}/computation`;
273
+ let requestBody;
274
+ try {
275
+ const parsedInput = JSON.parse(input);
276
+ requestBody = { input, ...parsedInput };
277
+ } catch {
278
+ requestBody = { input };
279
+ }
280
+ const response = await axios.post(url, requestBody, {
281
+ timeout,
282
+ headers: {
283
+ "Content-Type": "application/json"
284
+ }
285
+ });
286
+ const executionTime = Date.now() - startTime;
287
+ let output;
288
+ if (typeof response.data === "string") {
289
+ output = response.data;
290
+ } else if (response.data.output !== void 0) {
291
+ output = typeof response.data.output === "string" ? response.data.output : JSON.stringify(response.data.output);
292
+ } else {
293
+ output = JSON.stringify(response.data);
294
+ }
295
+ return {
296
+ output,
297
+ exitCode: 0,
298
+ executionTime
299
+ };
300
+ } catch (error) {
301
+ const executionTime = Date.now() - startTime;
302
+ if (error.response) {
303
+ throw new Error(
304
+ `Container HTTP error ${error.response.status}: ${JSON.stringify(error.response.data)}`
305
+ );
306
+ } else if (error.code === "ECONNREFUSED") {
307
+ throw new Error(
308
+ `Cannot connect to container (port ${container.port || 8081}). Is it running?`
309
+ );
310
+ } else if (error.code === "ETIMEDOUT" || error.code === "ECONNABORTED") {
311
+ throw new Error(`Container execution timeout after ${timeout}ms`);
312
+ }
313
+ throw error;
314
+ }
315
+ }
316
+ async collectContainerResult(dockerContainer, workDir, startTime) {
317
+ try {
318
+ const inspectData = await dockerContainer.inspect();
319
+ const exitCode = inspectData.State.ExitCode || 0;
320
+ const outputPath = path.join(workDir, "output.json");
321
+ let output = "";
322
+ try {
323
+ output = await fs.readFile(outputPath, "utf-8");
324
+ } catch (error) {
325
+ const logs = await dockerContainer.logs({
326
+ stdout: true,
327
+ stderr: true
328
+ });
329
+ output = logs.toString();
330
+ }
331
+ const executionTime = Date.now() - startTime;
332
+ try {
333
+ await dockerContainer.remove({ force: true });
334
+ } catch (error) {
335
+ }
336
+ await fs.rm(workDir, { recursive: true, force: true });
337
+ return {
338
+ output,
339
+ exitCode,
340
+ executionTime
341
+ };
342
+ } catch (error) {
343
+ try {
344
+ await dockerContainer.remove({ force: true });
345
+ } catch {
346
+ }
347
+ await fs.rm(workDir, { recursive: true, force: true }).catch(() => {
348
+ });
349
+ throw error;
350
+ }
351
+ }
352
+ async pullImage(image, tag) {
353
+ const imageTag = `${image}:${tag}`;
354
+ try {
355
+ await this.docker.getImage(imageTag).inspect();
356
+ console.log(`Image ${imageTag} already exists`);
357
+ } catch {
358
+ console.log(`Pulling image ${imageTag}...`);
359
+ return new Promise((resolve, reject) => {
360
+ this.docker.pull(imageTag, (err, stream) => {
361
+ if (err) return reject(err);
362
+ this.docker.modem.followProgress(
363
+ stream,
364
+ (err2) => {
365
+ if (err2) return reject(err2);
366
+ console.log(`\u2713 Pulled image ${imageTag}`);
367
+ resolve();
368
+ },
369
+ (event) => {
370
+ if (event.status) {
371
+ console.log(`${event.status} ${event.progress || ""}`);
372
+ }
373
+ }
374
+ );
375
+ });
376
+ });
377
+ }
378
+ }
379
+ async waitForContainer(container) {
380
+ return new Promise((resolve, reject) => {
381
+ container.wait((err, data) => {
382
+ if (err) return reject(err);
383
+ resolve(data);
384
+ });
385
+ });
386
+ }
387
+ timeout(ms) {
388
+ return new Promise((resolve) => setTimeout(() => resolve(null), ms));
389
+ }
390
+ parseMemory(memory) {
391
+ const units = {
392
+ b: 1,
393
+ kb: 1024,
394
+ mb: 1024 * 1024,
395
+ gb: 1024 * 1024 * 1024
396
+ };
397
+ const match = memory.toLowerCase().match(/^(\d+)\s*(b|kb|mb|gb)$/);
398
+ if (!match) {
399
+ throw new Error(`Invalid memory format: ${memory}`);
400
+ }
401
+ const [, value, unit] = match;
402
+ return parseInt(value, 10) * units[unit];
403
+ }
404
+ async checkDockerAvailable() {
405
+ try {
406
+ await this.docker.ping();
407
+ return true;
408
+ } catch {
409
+ return false;
410
+ }
411
+ }
412
+ async getDockerInfo() {
413
+ return this.docker.info();
414
+ }
415
+ /**
416
+ * Cleanup all running containers
417
+ * Called when agent is shutting down
418
+ */
419
+ async cleanup() {
420
+ if (this.runningContainers.size === 0) {
421
+ return;
422
+ }
423
+ console.log(`\u{1F9F9} Cleaning up ${this.runningContainers.size} running containers...`);
424
+ const cleanupPromises = Array.from(this.runningContainers).map(async (container) => {
425
+ try {
426
+ const inspect = await container.inspect();
427
+ if (inspect.State.Running) {
428
+ console.log(` Stopping container ${inspect.Id.slice(0, 12)}...`);
429
+ await container.stop({ t: 10 });
430
+ await container.remove({ force: true });
431
+ }
432
+ } catch (error) {
433
+ console.warn(` Warning: Failed to cleanup container:`, error.message);
434
+ }
435
+ });
436
+ await Promise.all(cleanupPromises);
437
+ this.runningContainers.clear();
438
+ console.log("\u2713 Container cleanup completed");
439
+ }
440
+ /**
441
+ * Get number of running containers
442
+ */
443
+ getRunningContainerCount() {
444
+ return this.runningContainers.size;
445
+ }
446
+ /**
447
+ * Pre-pull container images and start persistent containers on startup
448
+ * This speeds up request handling by having containers ready
449
+ */
450
+ async prepareContainers(containers) {
451
+ if (containers.size === 0) {
452
+ console.log("No containers to prepare");
453
+ return;
454
+ }
455
+ console.log(`
456
+ \u{1F680} Preparing ${containers.size} containers...`);
457
+ const pullAndStartPromises = Array.from(containers.entries()).map(async ([id, container]) => {
458
+ try {
459
+ const imageTag = `${container.image}:${container.tag || "latest"}`;
460
+ console.log(` Pulling ${imageTag}...`);
461
+ await this.pullImage(container.image, container.tag || "latest");
462
+ console.log(` \u2713 ${imageTag} ready`);
463
+ await this.startPersistentContainer(id, container);
464
+ } catch (error) {
465
+ console.error(` \u274C Failed to prepare ${container.image}:`, error.message);
466
+ }
467
+ });
468
+ await Promise.all(pullAndStartPromises);
469
+ console.log("\u2713 All containers prepared\n");
470
+ }
471
+ /**
472
+ * Start a persistent container that stays running
473
+ */
474
+ async startPersistentContainer(containerId, metadata) {
475
+ const containerName = `noosphere-${containerId}`;
476
+ const imageTag = `${metadata.image}:${metadata.tag || "latest"}`;
477
+ try {
478
+ const existingContainer = this.docker.getContainer(containerName);
479
+ try {
480
+ const inspect = await existingContainer.inspect();
481
+ if (inspect.State.Running) {
482
+ console.log(` \u2713 Container ${containerName} already running`);
483
+ this.persistentContainers.set(containerId, existingContainer);
484
+ return;
485
+ } else {
486
+ await existingContainer.start();
487
+ console.log(` \u2713 Started existing container ${containerName}`);
488
+ this.persistentContainers.set(containerId, existingContainer);
489
+ return;
490
+ }
491
+ } catch (err) {
492
+ }
493
+ } catch (err) {
494
+ }
495
+ const createOptions = {
496
+ name: containerName,
497
+ Image: imageTag,
498
+ Tty: false,
499
+ AttachStdout: false,
500
+ AttachStderr: false,
501
+ ExposedPorts: metadata.port ? { [`${metadata.port}/tcp`]: {} } : void 0,
502
+ HostConfig: {
503
+ AutoRemove: false,
504
+ // Keep container for reuse
505
+ PortBindings: metadata.port ? {
506
+ [`${metadata.port}/tcp`]: [{ HostPort: metadata.port }]
507
+ } : void 0
508
+ },
509
+ Env: metadata.env ? Object.entries(metadata.env).map(([k, v]) => `${k}=${v}`) : void 0
510
+ };
511
+ if (metadata.requirements) {
512
+ const resources = {};
513
+ if (metadata.requirements.memory) {
514
+ resources.Memory = this.parseMemory(metadata.requirements.memory);
515
+ }
516
+ if (metadata.requirements.cpu) {
517
+ resources.NanoCpus = metadata.requirements.cpu * 1e9;
518
+ }
519
+ if (metadata.requirements.gpu) {
520
+ createOptions.HostConfig.DeviceRequests = [
521
+ {
522
+ Driver: "nvidia",
523
+ Count: -1,
524
+ Capabilities: [["gpu"]]
525
+ }
526
+ ];
527
+ }
528
+ if (Object.keys(resources).length > 0) {
529
+ createOptions.HostConfig = {
530
+ ...createOptions.HostConfig,
531
+ ...resources
532
+ };
533
+ }
534
+ }
535
+ const dockerContainer = await this.docker.createContainer(createOptions);
536
+ await dockerContainer.start();
537
+ this.persistentContainers.set(containerId, dockerContainer);
538
+ if (metadata.port) {
539
+ this.containerPorts.set(containerId, parseInt(metadata.port));
540
+ }
541
+ console.log(` \u2713 Started persistent container ${containerName}`);
542
+ }
543
+ /**
544
+ * Stop and remove all persistent containers
545
+ */
546
+ async stopPersistentContainers() {
547
+ if (this.persistentContainers.size === 0) {
548
+ return;
549
+ }
550
+ console.log(`
551
+ \u{1F6D1} Stopping ${this.persistentContainers.size} persistent containers...`);
552
+ const stopPromises = Array.from(this.persistentContainers.entries()).map(
553
+ async ([id, container]) => {
554
+ try {
555
+ const inspect = await container.inspect();
556
+ if (inspect.State.Running) {
557
+ console.log(` Stopping ${inspect.Name}...`);
558
+ await container.stop({ t: 10 });
559
+ }
560
+ await container.remove({ force: true });
561
+ console.log(` \u2713 Stopped ${inspect.Name}`);
562
+ } catch (error) {
563
+ console.warn(` Warning: Failed to stop container ${id}:`, error.message);
564
+ }
565
+ }
566
+ );
567
+ await Promise.all(stopPromises);
568
+ this.persistentContainers.clear();
569
+ this.containerPorts.clear();
570
+ console.log("\u2713 All persistent containers stopped\n");
571
+ }
572
+ };
573
+
574
+ // src/NoosphereAgent.ts
575
+ import { ethers as ethers4 } from "ethers";
576
+
577
+ // src/SchedulerService.ts
578
+ import { EventEmitter as EventEmitter2 } from "events";
579
+ import { SubscriptionBatchReaderContract } from "@noosphere/contracts";
580
+ var SchedulerService = class extends EventEmitter2 {
581
+ constructor(provider, router, coordinator, agentWallet, batchReaderAddress, config, getContainer) {
582
+ super();
583
+ this.provider = provider;
584
+ this.router = router;
585
+ this.coordinator = coordinator;
586
+ this.agentWallet = agentWallet;
587
+ this.subscriptions = /* @__PURE__ */ new Map();
588
+ this.committedIntervals = /* @__PURE__ */ new Set();
589
+ // subscriptionId:interval
590
+ this.pendingTxs = /* @__PURE__ */ new Map();
591
+ this.lastSyncedId = 0n;
592
+ this.config = {
593
+ cronIntervalMs: config?.cronIntervalMs ?? 6e4,
594
+ // 1 minute
595
+ maxRetryAttempts: config?.maxRetryAttempts ?? 3,
596
+ syncPeriodMs: config?.syncPeriodMs ?? 3e3,
597
+ // 3 seconds
598
+ loadCommittedIntervals: config?.loadCommittedIntervals,
599
+ saveCommittedInterval: config?.saveCommittedInterval
600
+ };
601
+ this.getContainer = getContainer;
602
+ if (batchReaderAddress) {
603
+ this.batchReader = new SubscriptionBatchReaderContract(batchReaderAddress, provider);
604
+ console.log(`\u2713 SubscriptionBatchReader configured: ${batchReaderAddress}`);
605
+ } else {
606
+ console.warn("\u26A0\uFE0F SubscriptionBatchReader not configured - subscription sync disabled");
607
+ }
608
+ }
609
+ /**
610
+ * Start the scheduler service
611
+ */
612
+ start() {
613
+ console.log("\u{1F550} Starting Scheduler Service...");
614
+ console.log(` Commitment generation interval: ${this.config.cronIntervalMs}ms`);
615
+ console.log(` Sync period: ${this.config.syncPeriodMs}ms`);
616
+ if (this.config.loadCommittedIntervals) {
617
+ const loaded = this.config.loadCommittedIntervals();
618
+ for (const key of loaded) {
619
+ this.committedIntervals.add(key);
620
+ }
621
+ if (loaded.length > 0) {
622
+ console.log(` Loaded ${loaded.length} committed intervals from storage`);
623
+ }
624
+ }
625
+ this.intervalTimer = setInterval(() => this.generateCommitments(), this.config.cronIntervalMs);
626
+ this.syncTimer = setInterval(() => this.syncSubscriptions(), this.config.syncPeriodMs);
627
+ console.log("\u2713 Scheduler Service started");
628
+ }
629
+ /**
630
+ * Stop the scheduler service
631
+ */
632
+ stop() {
633
+ if (this.intervalTimer) {
634
+ clearInterval(this.intervalTimer);
635
+ this.intervalTimer = void 0;
636
+ }
637
+ if (this.syncTimer) {
638
+ clearInterval(this.syncTimer);
639
+ this.syncTimer = void 0;
640
+ }
641
+ console.log("\u2713 Scheduler Service stopped");
642
+ }
643
+ /**
644
+ * Track a new subscription
645
+ */
646
+ trackSubscription(subscription) {
647
+ const key = subscription.subscriptionId.toString();
648
+ if (this.subscriptions.has(key)) {
649
+ console.log(` Subscription ${key} already tracked, updating...`);
650
+ }
651
+ const now = Math.floor(Date.now() / 1e3);
652
+ const elapsed = now - Number(subscription.activeAt);
653
+ const currentInterval = subscription.intervalSeconds > 0n ? BigInt(Math.max(1, Math.floor(elapsed / Number(subscription.intervalSeconds)) + 1)) : 1n;
654
+ this.subscriptions.set(key, {
655
+ ...subscription,
656
+ currentInterval,
657
+ lastProcessedAt: Date.now(),
658
+ txAttempts: 0
659
+ });
660
+ console.log(`\u2713 Tracking subscription ${key}`);
661
+ this.emit("subscription:tracked", subscription.subscriptionId);
662
+ }
663
+ /**
664
+ * Remove a subscription from tracking
665
+ */
666
+ untrackSubscription(subscriptionId) {
667
+ const key = subscriptionId.toString();
668
+ if (this.subscriptions.delete(key)) {
669
+ const prefix = `${key}:`;
670
+ let cleanedCount = 0;
671
+ for (const commitmentKey of this.committedIntervals) {
672
+ if (commitmentKey.startsWith(prefix)) {
673
+ this.committedIntervals.delete(commitmentKey);
674
+ cleanedCount++;
675
+ }
676
+ }
677
+ if (cleanedCount > 0) {
678
+ console.log(` \u{1F9F9} Cleaned up ${cleanedCount} committed intervals for subscription ${key}`);
679
+ }
680
+ console.log(`\u2713 Stopped tracking subscription ${key}`);
681
+ this.emit("subscription:untracked", subscriptionId);
682
+ }
683
+ }
684
+ /**
685
+ * Mark an interval as committed (for RequestStarted events)
686
+ * Also persists to storage if callback is configured
687
+ */
688
+ markIntervalCommitted(subscriptionId, interval) {
689
+ const commitmentKey = `${subscriptionId}:${interval}`;
690
+ this.addCommittedInterval(commitmentKey);
691
+ console.log(` \u2713 Marked interval ${interval} as committed for subscription ${subscriptionId}`);
692
+ }
693
+ /**
694
+ * Add to committed intervals set and persist to storage
695
+ */
696
+ addCommittedInterval(key) {
697
+ if (!this.committedIntervals.has(key)) {
698
+ this.committedIntervals.add(key);
699
+ if (this.config.saveCommittedInterval) {
700
+ this.config.saveCommittedInterval(key);
701
+ }
702
+ }
703
+ }
704
+ /**
705
+ * Main commitment generation loop (runs every cronIntervalMs)
706
+ * Equivalent to Java's CommitmentGenerationService.generateCommitment()
707
+ */
708
+ async generateCommitments() {
709
+ try {
710
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
711
+ console.log(`
712
+ \u{1F504} [${timestamp}] Starting commitment generation task...`);
713
+ this.pruneFailedTxs();
714
+ await this.processActiveSubscriptions();
715
+ const endTimestamp = (/* @__PURE__ */ new Date()).toISOString();
716
+ console.log(`\u2713 [${endTimestamp}] Finished commitment generation task.
717
+ `);
718
+ } catch (error) {
719
+ console.error("\u274C Error in commitment generation:", error);
720
+ this.emit("error", error);
721
+ }
722
+ }
723
+ /**
724
+ * Process all active subscriptions
725
+ */
726
+ async processActiveSubscriptions() {
727
+ const latestBlock = await this.provider.getBlock("latest");
728
+ if (!latestBlock) {
729
+ console.warn(" Could not fetch latest block, skipping this cycle");
730
+ return;
731
+ }
732
+ const currentBlockTime = latestBlock.timestamp;
733
+ if (this.subscriptions.size === 0) {
734
+ console.log(" No subscriptions to process");
735
+ return;
736
+ }
737
+ console.log(` Processing ${this.subscriptions.size} subscription(s)...`);
738
+ for (const [subId, sub] of this.subscriptions.entries()) {
739
+ let currentInterval = 0n;
740
+ try {
741
+ if (sub.intervalSeconds <= 0n) {
742
+ console.warn(` Skipping subscription ${subId}: invalid intervalSeconds (${sub.intervalSeconds})`);
743
+ this.untrackSubscription(sub.subscriptionId);
744
+ continue;
745
+ }
746
+ try {
747
+ currentInterval = BigInt(await this.router.getComputeSubscriptionInterval(sub.subscriptionId));
748
+ } catch (error) {
749
+ console.warn(` Could not get interval from router for subscription ${subId}:`, error.message);
750
+ const intervalsSinceActive = BigInt(currentBlockTime) - sub.activeAt;
751
+ currentInterval = intervalsSinceActive / sub.intervalSeconds + 1n;
752
+ }
753
+ console.log(` Subscription ${subId}: currentInterval=${currentInterval}, maxExecutions=${sub.maxExecutions}, activeAt=${sub.activeAt}`);
754
+ if (!this.shouldProcess(sub, currentBlockTime)) {
755
+ continue;
756
+ }
757
+ const commitmentKey = `${subId}:${currentInterval}`;
758
+ if (this.committedIntervals.has(commitmentKey)) {
759
+ continue;
760
+ }
761
+ const hasCommitment = await this.hasRequestCommitments(sub.subscriptionId, currentInterval);
762
+ if (hasCommitment) {
763
+ this.addCommittedInterval(commitmentKey);
764
+ console.log(` Subscription ${subId} interval ${currentInterval} already committed`);
765
+ continue;
766
+ }
767
+ await this.prepareNextInterval(sub, currentInterval);
768
+ } catch (error) {
769
+ const errorMessage = error.message;
770
+ console.error(` Error processing subscription ${subId}:`, error);
771
+ const containsError = (ex, text) => {
772
+ let current = ex;
773
+ while (current) {
774
+ if (current.message?.includes(text)) return true;
775
+ current = current.cause;
776
+ }
777
+ return false;
778
+ };
779
+ if (containsError(error, "Panic due to OVERFLOW") || containsError(error, "arithmetic underflow or overflow")) {
780
+ console.log(` Interval ${currentInterval} for subscription ${subId} appears to be already executed (overflow), marking as committed`);
781
+ const commitmentKey = `${subId}:${currentInterval}`;
782
+ this.addCommittedInterval(commitmentKey);
783
+ sub.currentInterval = currentInterval + 1n;
784
+ } else if (containsError(error, "0x3cdc51d3") || containsError(error, "NoNextInterval")) {
785
+ console.log(` Subscription ${subId}: waiting for client to trigger interval 1 (NoNextInterval)`);
786
+ } else if (containsError(error, "execution reverted") || containsError(error, "Transaction simulation failed")) {
787
+ console.log(` Subscription ${subId} appears to be cancelled or invalid, untracking...`);
788
+ this.untrackSubscription(sub.subscriptionId);
789
+ }
790
+ }
791
+ }
792
+ }
793
+ /**
794
+ * Check if subscription should be processed
795
+ */
796
+ shouldProcess(sub, currentBlockTime) {
797
+ const subId = sub.subscriptionId.toString();
798
+ if (BigInt(currentBlockTime) < sub.activeAt) {
799
+ console.log(` Skip: not active yet (currentTime=${currentBlockTime}, activeAt=${sub.activeAt})`);
800
+ return false;
801
+ }
802
+ const intervalsSinceActive = BigInt(currentBlockTime) - sub.activeAt;
803
+ const currentInterval = intervalsSinceActive / sub.intervalSeconds + 1n;
804
+ if (sub.maxExecutions > 0n && currentInterval > sub.maxExecutions) {
805
+ console.log(` Subscription ${subId} completed (interval ${currentInterval} > maxExecutions ${sub.maxExecutions}), untracking...`);
806
+ this.untrackSubscription(sub.subscriptionId);
807
+ return false;
808
+ }
809
+ const runKey = `${sub.subscriptionId}:${currentInterval}`;
810
+ if (this.pendingTxs.has(runKey)) {
811
+ console.log(` Skip: pending transaction for interval ${currentInterval}`);
812
+ return false;
813
+ }
814
+ if (sub.txAttempts >= this.config.maxRetryAttempts) {
815
+ console.log(` Skip: max retry attempts reached (${sub.txAttempts}/${this.config.maxRetryAttempts})`);
816
+ return false;
817
+ }
818
+ return true;
819
+ }
820
+ /**
821
+ * Check if interval already has commitments on-chain
822
+ */
823
+ async hasRequestCommitments(subscriptionId, interval) {
824
+ try {
825
+ const redundancy = await this.coordinator.redundancyCount(
826
+ this.getRequestId(subscriptionId, interval)
827
+ );
828
+ return redundancy > 0;
829
+ } catch (error) {
830
+ console.error("Error checking commitments:", error);
831
+ return false;
832
+ }
833
+ }
834
+ /**
835
+ * Prepare next interval by calling coordinator contract
836
+ * Equivalent to Java's CoordinatorService.prepareNextInterval()
837
+ */
838
+ async prepareNextInterval(sub, interval) {
839
+ const runKey = `${sub.subscriptionId}:${interval}`;
840
+ try {
841
+ console.log(` Preparing interval ${interval} for subscription ${sub.subscriptionId}...`);
842
+ const currentIntervalNow = BigInt(await this.router.getComputeSubscriptionInterval(sub.subscriptionId));
843
+ if (currentIntervalNow !== interval && currentIntervalNow !== interval - 1n) {
844
+ console.log(` \u26A0\uFE0F Interval changed: expected ${interval}, blockchain is at ${currentIntervalNow}. Skipping.`);
845
+ return;
846
+ }
847
+ const tx = await this.coordinator.prepareNextInterval(
848
+ sub.subscriptionId,
849
+ interval,
850
+ this.agentWallet
851
+ );
852
+ this.pendingTxs.set(runKey, tx.hash);
853
+ sub.pendingTx = tx.hash;
854
+ console.log(` \u{1F4E4} Transaction sent: ${tx.hash}`);
855
+ const receipt = await tx.wait();
856
+ if (receipt.status === 1) {
857
+ console.log(
858
+ ` \u2713 Interval ${interval} prepared successfully (block ${receipt.blockNumber})`
859
+ );
860
+ const commitmentKey = `${sub.subscriptionId}:${interval}`;
861
+ this.addCommittedInterval(commitmentKey);
862
+ sub.currentInterval = interval;
863
+ sub.lastProcessedAt = Date.now();
864
+ sub.txAttempts = 0;
865
+ sub.pendingTx = void 0;
866
+ this.pendingTxs.delete(runKey);
867
+ const requestStartedEvent = this.parseRequestStartedFromReceipt(receipt, sub);
868
+ const gasUsed = receipt.gasUsed;
869
+ const gasPrice = receipt.gasPrice ?? tx.gasPrice ?? 0n;
870
+ const gasCost = gasUsed * gasPrice;
871
+ this.emit("commitment:success", {
872
+ subscriptionId: sub.subscriptionId,
873
+ interval,
874
+ txHash: tx.hash,
875
+ blockNumber: receipt.blockNumber,
876
+ gasUsed: gasUsed.toString(),
877
+ gasPrice: gasPrice.toString(),
878
+ gasCost: gasCost.toString(),
879
+ requestStartedEvent
880
+ // Include parsed event for immediate processing
881
+ });
882
+ } else {
883
+ throw new Error(`Transaction failed with status ${receipt.status}`);
884
+ }
885
+ } catch (error) {
886
+ console.error(` Failed to prepare interval for ${runKey}:`, error);
887
+ sub.pendingTx = void 0;
888
+ this.pendingTxs.delete(runKey);
889
+ const errorMessage = error.message || "";
890
+ const isNoNextIntervalError = errorMessage.includes("0x3cdc51d3") || errorMessage.includes("NoNextInterval");
891
+ if (isNoNextIntervalError) {
892
+ console.log(` Subscription ${sub.subscriptionId}: NoNextInterval - waiting for client to trigger interval 1`);
893
+ sub.txAttempts = 0;
894
+ return;
895
+ }
896
+ sub.txAttempts++;
897
+ if (sub.txAttempts >= this.config.maxRetryAttempts) {
898
+ console.log(` Max retry attempts reached for ${runKey}`);
899
+ this.emit("commitment:failed", {
900
+ subscriptionId: sub.subscriptionId,
901
+ interval,
902
+ error
903
+ });
904
+ }
905
+ }
906
+ }
907
+ /**
908
+ * Prune transactions that have failed
909
+ */
910
+ pruneFailedTxs() {
911
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
912
+ for (const [subId, sub] of this.subscriptions.entries()) {
913
+ if (sub.lastProcessedAt < fiveMinutesAgo && sub.pendingTx) {
914
+ console.log(` Pruning stale transaction for subscription ${subId}`);
915
+ sub.pendingTx = void 0;
916
+ sub.txAttempts = 0;
917
+ }
918
+ }
919
+ }
920
+ /**
921
+ * Sync subscriptions (placeholder for blockchain event listening)
922
+ */
923
+ /**
924
+ * Sync subscriptions from blockchain
925
+ * Reads subscriptions in batches and tracks active ones
926
+ */
927
+ async syncSubscriptions() {
928
+ if (!this.batchReader) {
929
+ this.emit("sync:tick");
930
+ return;
931
+ }
932
+ try {
933
+ if (this.maxSubscriptionId === void 0) {
934
+ this.maxSubscriptionId = await this.router.getLastSubscriptionId();
935
+ console.log(`\u{1F4CA} Total subscriptions in registry: ${this.maxSubscriptionId}`);
936
+ }
937
+ if (this.lastSyncedId >= this.maxSubscriptionId) {
938
+ const latestMaxId = await this.router.getLastSubscriptionId();
939
+ if (latestMaxId > this.maxSubscriptionId) {
940
+ console.log(`\u{1F4CA} Found new subscriptions: ${latestMaxId} (was ${this.maxSubscriptionId})`);
941
+ this.maxSubscriptionId = latestMaxId;
942
+ } else {
943
+ this.emit("sync:tick");
944
+ return;
945
+ }
946
+ }
947
+ const maxSubId = this.maxSubscriptionId;
948
+ const currentBlock = await this.provider.getBlockNumber();
949
+ const block = await this.provider.getBlock(currentBlock);
950
+ const blockTime = block?.timestamp || Math.floor(Date.now() / 1e3);
951
+ const BATCH_SIZE = 100n;
952
+ const startId = this.lastSyncedId + 1n;
953
+ const endId = startId + BATCH_SIZE - 1n > maxSubId ? maxSubId : startId + BATCH_SIZE - 1n;
954
+ const subscriptions = await this.batchReader.getSubscriptions(startId, endId, currentBlock);
955
+ if (subscriptions.length === 0) {
956
+ this.emit("sync:tick");
957
+ return;
958
+ }
959
+ let newSubscriptions = 0;
960
+ let skippedContainers = 0;
961
+ let skippedInactive = 0;
962
+ let skippedEmpty = 0;
963
+ let skippedOnDemand = 0;
964
+ console.log(` Syncing ${subscriptions.length} subscriptions (blockTime: ${blockTime})`);
965
+ for (let i = 0; i < subscriptions.length; i++) {
966
+ const sub = subscriptions[i];
967
+ const subscriptionId = startId + BigInt(i);
968
+ if (sub.containerId === "0x0000000000000000000000000000000000000000000000000000000000000000") {
969
+ skippedEmpty++;
970
+ continue;
971
+ }
972
+ if (!this.isSubscriptionActive(sub, blockTime)) {
973
+ if (sub.intervalSeconds > 0) {
974
+ console.log(` Sub ${subscriptionId}: inactive (activeAt=${sub.activeAt}, now=${blockTime})`);
975
+ }
976
+ skippedInactive++;
977
+ continue;
978
+ }
979
+ if (this.getContainer && !this.getContainer(sub.containerId)) {
980
+ skippedContainers++;
981
+ continue;
982
+ }
983
+ if (this.trackSubscriptionFromConfig(sub, subscriptionId)) {
984
+ newSubscriptions++;
985
+ } else {
986
+ if (sub.intervalSeconds <= 0) {
987
+ skippedOnDemand++;
988
+ }
989
+ }
990
+ }
991
+ console.log(` Sync stats: ${newSubscriptions} tracked, ${skippedEmpty} empty, ${skippedInactive} inactive, ${skippedContainers} unsupported containers, ${skippedOnDemand} on-demand`);
992
+ this.lastSyncedId = endId;
993
+ if (newSubscriptions > 0) {
994
+ console.log(`\u2713 Synced ${newSubscriptions} active subscriptions (ID ${startId} - ${endId})`);
995
+ }
996
+ if (this.lastSyncedId >= maxSubId) {
997
+ console.log(`\u2713 Sync completed - processed all ${maxSubId} subscriptions`);
998
+ }
999
+ this.emit("sync:completed", {
1000
+ subscriptions: this.subscriptions.size,
1001
+ newSubscriptions
1002
+ });
1003
+ } catch (error) {
1004
+ console.error("Error syncing subscriptions:", error);
1005
+ this.emit("sync:error", error);
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Check if subscription is currently active
1010
+ */
1011
+ isSubscriptionActive(sub, currentBlockTime) {
1012
+ if (currentBlockTime < sub.activeAt) {
1013
+ return false;
1014
+ }
1015
+ if (sub.maxExecutions > 0 && sub.intervalSeconds > 0) {
1016
+ const elapsed = currentBlockTime - sub.activeAt;
1017
+ const currentInterval = Math.floor(elapsed / sub.intervalSeconds);
1018
+ if (currentInterval >= sub.maxExecutions) {
1019
+ return false;
1020
+ }
1021
+ }
1022
+ return true;
1023
+ }
1024
+ /**
1025
+ * Track subscription from ComputeSubscription config
1026
+ * Returns true if subscription was tracked, false if skipped
1027
+ */
1028
+ trackSubscriptionFromConfig(sub, subscriptionId) {
1029
+ if (sub.containerId === "0x0000000000000000000000000000000000000000000000000000000000000000") {
1030
+ return false;
1031
+ }
1032
+ if (sub.client === "0x0000000000000000000000000000000000000000") {
1033
+ return false;
1034
+ }
1035
+ const key = subscriptionId.toString();
1036
+ if (this.subscriptions.has(key)) {
1037
+ return false;
1038
+ }
1039
+ if (sub.intervalSeconds <= 0) {
1040
+ return false;
1041
+ }
1042
+ try {
1043
+ const { ethers: ethers6 } = __require("ethers");
1044
+ const containerIdStr = ethers6.decodeBytes32String(sub.containerId);
1045
+ console.log(` \u2713 Tracking subscription: ${containerIdStr}`);
1046
+ console.log(` Client: ${sub.client}`);
1047
+ console.log(` Interval: ${sub.intervalSeconds}s`);
1048
+ console.log(` Max Executions: ${sub.maxExecutions}`);
1049
+ } catch (e) {
1050
+ console.log(` \u2713 Tracking subscription: ${sub.containerId}`);
1051
+ }
1052
+ const now = Math.floor(Date.now() / 1e3);
1053
+ const elapsed = now - sub.activeAt;
1054
+ const currentInterval = sub.intervalSeconds > 0 ? Math.max(1, Math.floor(elapsed / sub.intervalSeconds) + 1) : 1;
1055
+ this.subscriptions.set(key, {
1056
+ subscriptionId,
1057
+ routeId: sub.routeId,
1058
+ containerId: sub.containerId,
1059
+ client: sub.client,
1060
+ wallet: sub.wallet,
1061
+ activeAt: BigInt(sub.activeAt),
1062
+ intervalSeconds: BigInt(sub.intervalSeconds),
1063
+ maxExecutions: Number.isFinite(sub.maxExecutions) ? BigInt(sub.maxExecutions) : 0n,
1064
+ redundancy: sub.redundancy,
1065
+ verifier: sub.verifier || void 0,
1066
+ currentInterval: BigInt(currentInterval),
1067
+ lastProcessedAt: Date.now(),
1068
+ txAttempts: 0
1069
+ });
1070
+ return true;
1071
+ }
1072
+ /**
1073
+ * Get request ID (hash of subscription ID and interval)
1074
+ */
1075
+ getRequestId(subscriptionId, interval) {
1076
+ const ethers6 = __require("ethers");
1077
+ const abiCoder = ethers6.AbiCoder.defaultAbiCoder();
1078
+ return ethers6.keccak256(abiCoder.encode(["uint256", "uint256"], [subscriptionId, interval]));
1079
+ }
1080
+ /**
1081
+ * Get scheduler statistics
1082
+ */
1083
+ getStats() {
1084
+ const now = Math.floor(Date.now() / 1e3);
1085
+ const activeCount = Array.from(this.subscriptions.values()).filter((sub) => {
1086
+ if (BigInt(now) < sub.activeAt) {
1087
+ return false;
1088
+ }
1089
+ if (sub.maxExecutions > 0n) {
1090
+ const elapsed = BigInt(now) - sub.activeAt;
1091
+ const currentInterval = elapsed / sub.intervalSeconds;
1092
+ if (currentInterval >= sub.maxExecutions) {
1093
+ return false;
1094
+ }
1095
+ }
1096
+ return true;
1097
+ }).length;
1098
+ return {
1099
+ totalSubscriptions: this.subscriptions.size,
1100
+ activeSubscriptions: activeCount,
1101
+ committedIntervals: this.committedIntervals.size,
1102
+ pendingTransactions: this.pendingTxs.size
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Get all tracked subscriptions
1107
+ */
1108
+ getSubscriptions() {
1109
+ return Array.from(this.subscriptions.values());
1110
+ }
1111
+ /**
1112
+ * Parse RequestStarted event from transaction receipt
1113
+ * This allows the agent to process the event immediately without waiting for WebSocket
1114
+ */
1115
+ parseRequestStartedFromReceipt(receipt, sub) {
1116
+ try {
1117
+ for (const log of receipt.logs) {
1118
+ try {
1119
+ const parsed = this.coordinator.interface.parseLog({
1120
+ topics: log.topics,
1121
+ data: log.data
1122
+ });
1123
+ if (parsed && parsed.name === "RequestStarted") {
1124
+ const commitment = parsed.args.commitment;
1125
+ return {
1126
+ requestId: parsed.args.requestId,
1127
+ subscriptionId: parsed.args.subscriptionId,
1128
+ containerId: parsed.args.containerId,
1129
+ interval: Number(commitment.interval),
1130
+ redundancy: Number(commitment.redundancy),
1131
+ useDeliveryInbox: commitment.useDeliveryInbox,
1132
+ feeAmount: commitment.feeAmount,
1133
+ feeToken: commitment.feeToken,
1134
+ verifier: commitment.verifier,
1135
+ coordinator: commitment.coordinator,
1136
+ walletAddress: commitment.walletAddress,
1137
+ blockNumber: receipt.blockNumber
1138
+ };
1139
+ }
1140
+ } catch {
1141
+ }
1142
+ }
1143
+ } catch (error) {
1144
+ console.warn(" \u26A0\uFE0F Could not parse RequestStarted event from receipt:", error);
1145
+ }
1146
+ return null;
1147
+ }
1148
+ };
1149
+
1150
+ // src/NoosphereAgent.ts
1151
+ import { WalletManager, KeystoreManager } from "@noosphere/crypto";
1152
+ import { RegistryManager } from "@noosphere/registry";
1153
+ import { ABIs } from "@noosphere/contracts";
1154
+
1155
+ // src/utils/CommitmentUtils.ts
1156
+ import { ethers as ethers2 } from "ethers";
1157
+ var CommitmentUtils = class {
1158
+ /**
1159
+ * Calculate commitment hash
1160
+ * Matches the keccak256(abi.encode(commitment)) in Solidity
1161
+ */
1162
+ static hash(commitment) {
1163
+ const encoded = ethers2.AbiCoder.defaultAbiCoder().encode(
1164
+ [
1165
+ "bytes32",
1166
+ // requestId
1167
+ "uint64",
1168
+ // subscriptionId
1169
+ "bytes32",
1170
+ // containerId
1171
+ "uint32",
1172
+ // interval
1173
+ "bool",
1174
+ // useDeliveryInbox
1175
+ "uint16",
1176
+ // redundancy
1177
+ "address",
1178
+ // walletAddress
1179
+ "uint256",
1180
+ // feeAmount
1181
+ "address",
1182
+ // feeToken
1183
+ "address",
1184
+ // verifier
1185
+ "address"
1186
+ // coordinator
1187
+ ],
1188
+ [
1189
+ commitment.requestId,
1190
+ commitment.subscriptionId,
1191
+ commitment.containerId,
1192
+ commitment.interval,
1193
+ commitment.useDeliveryInbox,
1194
+ commitment.redundancy,
1195
+ commitment.walletAddress,
1196
+ commitment.feeAmount,
1197
+ commitment.feeToken,
1198
+ commitment.verifier,
1199
+ commitment.coordinator
1200
+ ]
1201
+ );
1202
+ return ethers2.keccak256(encoded);
1203
+ }
1204
+ /**
1205
+ * Verify commitment hash matches expected value
1206
+ */
1207
+ static verify(commitment, expectedHash) {
1208
+ const actualHash = this.hash(commitment);
1209
+ return actualHash === expectedHash;
1210
+ }
1211
+ /**
1212
+ * Encode commitment data for reportComputeResult
1213
+ * Returns ABI-encoded commitment struct
1214
+ */
1215
+ static encode(commitment) {
1216
+ return ethers2.AbiCoder.defaultAbiCoder().encode(
1217
+ [
1218
+ "bytes32",
1219
+ // requestId
1220
+ "uint64",
1221
+ // subscriptionId
1222
+ "bytes32",
1223
+ // containerId
1224
+ "uint32",
1225
+ // interval
1226
+ "bool",
1227
+ // useDeliveryInbox
1228
+ "uint16",
1229
+ // redundancy
1230
+ "address",
1231
+ // walletAddress
1232
+ "uint256",
1233
+ // feeAmount
1234
+ "address",
1235
+ // feeToken
1236
+ "address",
1237
+ // verifier
1238
+ "address"
1239
+ // coordinator
1240
+ ],
1241
+ [
1242
+ commitment.requestId,
1243
+ commitment.subscriptionId,
1244
+ commitment.containerId,
1245
+ commitment.interval,
1246
+ commitment.useDeliveryInbox,
1247
+ commitment.redundancy,
1248
+ commitment.walletAddress,
1249
+ commitment.feeAmount,
1250
+ commitment.feeToken,
1251
+ commitment.verifier,
1252
+ commitment.coordinator
1253
+ ]
1254
+ );
1255
+ }
1256
+ /**
1257
+ * Create Commitment from RequestStartedEvent
1258
+ */
1259
+ static fromEvent(event, walletAddress) {
1260
+ return {
1261
+ requestId: event.requestId,
1262
+ subscriptionId: event.subscriptionId,
1263
+ containerId: event.containerId,
1264
+ interval: event.interval,
1265
+ redundancy: event.redundancy,
1266
+ useDeliveryInbox: event.useDeliveryInbox || false,
1267
+ feeToken: event.feeToken,
1268
+ feeAmount: event.feeAmount,
1269
+ walletAddress,
1270
+ // Client wallet from subscription
1271
+ verifier: event.verifier || ethers2.ZeroAddress,
1272
+ coordinator: event.coordinator
1273
+ };
1274
+ }
1275
+ };
1276
+
1277
+ // src/utils/ConfigLoader.ts
1278
+ import { readFileSync } from "fs";
1279
+ import { ethers as ethers3 } from "ethers";
1280
+ var ConfigLoader = class {
1281
+ /**
1282
+ * Load agent configuration from JSON file
1283
+ */
1284
+ static loadFromFile(configPath) {
1285
+ try {
1286
+ const configData = readFileSync(configPath, "utf-8");
1287
+ const config = JSON.parse(configData);
1288
+ if (!config.chain || !config.chain.enabled) {
1289
+ throw new Error("Chain configuration is required and must be enabled");
1290
+ }
1291
+ if (!config.chain.rpcUrl) {
1292
+ throw new Error("Chain RPC URL is required");
1293
+ }
1294
+ if (!config.chain.routerAddress) {
1295
+ throw new Error("Router address is required");
1296
+ }
1297
+ if (!config.containers || config.containers.length === 0) {
1298
+ console.warn("\u26A0\uFE0F No containers configured in config file");
1299
+ }
1300
+ return config;
1301
+ } catch (error) {
1302
+ if (error.code === "ENOENT") {
1303
+ throw new Error(`Config file not found: ${configPath}`);
1304
+ }
1305
+ throw error;
1306
+ }
1307
+ }
1308
+ /**
1309
+ * Convert ContainerConfig to ContainerMetadata format
1310
+ */
1311
+ static containerConfigToMetadata(containerConfig) {
1312
+ const [imageName, tag] = containerConfig.image.includes(":") ? containerConfig.image.split(":") : [containerConfig.image, "latest"];
1313
+ const name = imageName.split("/").pop() || containerConfig.id;
1314
+ let payments;
1315
+ if (containerConfig.acceptedPayments) {
1316
+ const basePrice = Object.values(containerConfig.acceptedPayments)[0]?.toString() || "0";
1317
+ payments = {
1318
+ basePrice,
1319
+ unit: "wei",
1320
+ per: "execution"
1321
+ };
1322
+ }
1323
+ return {
1324
+ id: containerConfig.id,
1325
+ name,
1326
+ image: imageName,
1327
+ tag,
1328
+ port: containerConfig.port,
1329
+ env: containerConfig.env,
1330
+ verified: !!containerConfig.verifierAddress,
1331
+ payments
1332
+ };
1333
+ }
1334
+ /**
1335
+ * Get all containers from config as ContainerMetadata array
1336
+ */
1337
+ static getContainersFromConfig(config) {
1338
+ const containersMap = /* @__PURE__ */ new Map();
1339
+ for (const containerConfig of config.containers) {
1340
+ const metadata = this.containerConfigToMetadata(containerConfig);
1341
+ const containerIdHash = ethers3.keccak256(
1342
+ ethers3.AbiCoder.defaultAbiCoder().encode(["string"], [containerConfig.id])
1343
+ );
1344
+ containersMap.set(containerIdHash, metadata);
1345
+ }
1346
+ return containersMap;
1347
+ }
1348
+ /**
1349
+ * Get container config by ID
1350
+ */
1351
+ static getContainerConfig(config, containerId) {
1352
+ return config.containers.find((c) => c.id === containerId);
1353
+ }
1354
+ };
1355
+
1356
+ // src/NoosphereAgent.ts
1357
+ var NoosphereAgent = class _NoosphereAgent {
1358
+ // Deduplication: track requests being processed
1359
+ constructor(options) {
1360
+ this.options = options;
1361
+ this.isRunning = false;
1362
+ this.processingRequests = /* @__PURE__ */ new Set();
1363
+ this.config = options.config;
1364
+ this.provider = new ethers4.JsonRpcProvider(options.config.rpcUrl);
1365
+ const provider = this.provider;
1366
+ const routerAbi = options.routerAbi || ABIs.Router;
1367
+ const coordinatorAbi = options.coordinatorAbi || ABIs.Coordinator;
1368
+ if (options.walletManager) {
1369
+ this.walletManager = options.walletManager;
1370
+ } else if (options.config.privateKey) {
1371
+ this.walletManager = new WalletManager(options.config.privateKey, provider);
1372
+ } else {
1373
+ throw new Error(
1374
+ "Either walletManager or config.privateKey must be provided. Recommended: Use NoosphereAgent.fromKeystore() for production."
1375
+ );
1376
+ }
1377
+ this.containerManager = new ContainerManager();
1378
+ this.registryManager = new RegistryManager({
1379
+ autoSync: true,
1380
+ // Enable automatic sync with remote registry
1381
+ cacheTTL: 36e5
1382
+ // 1 hour cache
1383
+ });
1384
+ this.eventMonitor = new EventMonitor(options.config, routerAbi, coordinatorAbi, {
1385
+ loadCheckpoint: options.loadCheckpoint,
1386
+ saveCheckpoint: options.saveCheckpoint
1387
+ });
1388
+ this.router = new ethers4.Contract(
1389
+ options.config.routerAddress,
1390
+ routerAbi,
1391
+ this.provider
1392
+ );
1393
+ this.coordinator = new ethers4.Contract(
1394
+ options.config.coordinatorAddress,
1395
+ coordinatorAbi,
1396
+ this.walletManager.getWallet()
1397
+ );
1398
+ this.getContainer = options.getContainer;
1399
+ this.containers = options.containers;
1400
+ this.scheduler = new SchedulerService(
1401
+ provider,
1402
+ this.router,
1403
+ this.coordinator,
1404
+ this.walletManager.getAddress(),
1405
+ void 0,
1406
+ // batchReaderAddress (set later)
1407
+ void 0,
1408
+ // config (set later)
1409
+ this.getContainer
1410
+ // Pass container filter
1411
+ );
1412
+ this.paymentWallet = options.paymentWallet;
1413
+ if (!this.getContainer && (!this.containers || this.containers.size === 0)) {
1414
+ console.warn("\u26A0\uFE0F No container source provided. Agent will not be able to execute requests.");
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Initialize NoosphereAgent from config.json (RECOMMENDED)
1419
+ * This loads all configuration including containers from a config file
1420
+ *
1421
+ * @param configPath - Path to config.json file
1422
+ * @param routerAbi - Router contract ABI (optional - defaults to ABIs.Router)
1423
+ * @param coordinatorAbi - Coordinator contract ABI (optional - defaults to ABIs.Coordinator)
1424
+ * @returns Initialized NoosphereAgent
1425
+ */
1426
+ static async fromConfig(configPath, routerAbi, coordinatorAbi) {
1427
+ const fullConfig = ConfigLoader.loadFromFile(configPath);
1428
+ const keystorePath = fullConfig.chain.wallet.keystore?.path;
1429
+ const password = fullConfig.chain.wallet.keystore?.password;
1430
+ if (!keystorePath || !password) {
1431
+ throw new Error("Keystore path and password are required in config.chain.wallet.keystore");
1432
+ }
1433
+ const containers = ConfigLoader.getContainersFromConfig(fullConfig);
1434
+ console.log(`\u{1F4E6} Loaded ${containers.size} containers from config:`);
1435
+ for (const [id, container] of containers.entries()) {
1436
+ console.log(` - ${id}: ${container.image}:${container.tag || "latest"}`);
1437
+ }
1438
+ const provider = new ethers4.JsonRpcProvider(fullConfig.chain.rpcUrl);
1439
+ const keystoreManager = new KeystoreManager(keystorePath, password);
1440
+ await keystoreManager.load();
1441
+ const walletManager = await WalletManager.fromKeystoreManager(keystoreManager, provider);
1442
+ const agentConfig = {
1443
+ rpcUrl: fullConfig.chain.rpcUrl,
1444
+ wsRpcUrl: fullConfig.chain.wsRpcUrl,
1445
+ routerAddress: fullConfig.chain.routerAddress,
1446
+ coordinatorAddress: fullConfig.chain.coordinatorAddress || fullConfig.chain.routerAddress,
1447
+ deploymentBlock: fullConfig.chain.deploymentBlock,
1448
+ pollingInterval: fullConfig.chain.processingInterval
1449
+ };
1450
+ const paymentWallet = fullConfig.chain.wallet.paymentAddress;
1451
+ return new _NoosphereAgent({
1452
+ config: agentConfig,
1453
+ routerAbi,
1454
+ coordinatorAbi,
1455
+ walletManager,
1456
+ containers,
1457
+ paymentWallet
1458
+ });
1459
+ }
1460
+ /**
1461
+ * Initialize NoosphereAgent from keystore (RECOMMENDED)
1462
+ * This is the secure way to initialize an agent in production
1463
+ *
1464
+ * @param keystorePath - Path to keystore file
1465
+ * @param password - Keystore password
1466
+ * @param options - Agent configuration options
1467
+ * @returns Initialized NoosphereAgent
1468
+ */
1469
+ static async fromKeystore(keystorePath, password, options) {
1470
+ const provider = new ethers4.JsonRpcProvider(options.config.rpcUrl);
1471
+ const keystoreManager = new KeystoreManager(keystorePath, password);
1472
+ await keystoreManager.load();
1473
+ const walletManager = await WalletManager.fromKeystoreManager(keystoreManager, provider);
1474
+ return new _NoosphereAgent({
1475
+ ...options,
1476
+ walletManager
1477
+ });
1478
+ }
1479
+ async start() {
1480
+ console.log("Starting Noosphere Agent...");
1481
+ console.log("\u{1F4CB} Loading container registry...");
1482
+ await this.registryManager.load();
1483
+ const stats = this.registryManager.getStats();
1484
+ console.log(
1485
+ `\u2713 Registry loaded: ${stats.totalContainers} containers, ${stats.totalVerifiers} verifiers`
1486
+ );
1487
+ const dockerAvailable = await this.containerManager.checkDockerAvailable();
1488
+ if (!dockerAvailable) {
1489
+ throw new Error("Docker is not available. Please ensure Docker daemon is running.");
1490
+ }
1491
+ const address = this.walletManager.getAddress();
1492
+ const balance = await this.walletManager.getBalance();
1493
+ console.log(`Agent wallet: ${address}`);
1494
+ console.log(`Balance: ${ethers4.formatEther(balance)} ETH`);
1495
+ if (balance === 0n) {
1496
+ console.warn("\u26A0\uFE0F Warning: Wallet has zero balance. Agent needs ETH for gas fees.");
1497
+ }
1498
+ if (this.containers && this.containers.size > 0) {
1499
+ console.log(`
1500
+ \u{1F680} Preparing ${this.containers.size} containers...`);
1501
+ await this.containerManager.prepareContainers(this.containers);
1502
+ }
1503
+ await this.eventMonitor.connect();
1504
+ this.eventMonitor.on("RequestStarted", async (event) => {
1505
+ await this.handleRequest(event);
1506
+ });
1507
+ await this.eventMonitor.start();
1508
+ try {
1509
+ const batchReaderAddress = await this.coordinator.getSubscriptionBatchReader();
1510
+ if (batchReaderAddress && batchReaderAddress !== "0x0000000000000000000000000000000000000000") {
1511
+ console.log(`\u2713 SubscriptionBatchReader found: ${batchReaderAddress}`);
1512
+ this.scheduler.stop();
1513
+ this.scheduler = new SchedulerService(
1514
+ this.provider,
1515
+ this.router,
1516
+ this.coordinator,
1517
+ this.walletManager.getAddress(),
1518
+ batchReaderAddress,
1519
+ this.options.schedulerConfig || {
1520
+ cronIntervalMs: 6e4,
1521
+ // 1 minute (default)
1522
+ syncPeriodMs: 3e3,
1523
+ // 3 seconds (default)
1524
+ maxRetryAttempts: 3
1525
+ // 3 retries (default)
1526
+ },
1527
+ this.getContainer
1528
+ // Pass container filter
1529
+ );
1530
+ } else {
1531
+ console.warn("\u26A0\uFE0F SubscriptionBatchReader not available - subscription sync disabled");
1532
+ }
1533
+ } catch (error) {
1534
+ console.warn("\u26A0\uFE0F Failed to get SubscriptionBatchReader address:", error.message);
1535
+ }
1536
+ this.scheduler.start();
1537
+ this.scheduler.on("commitment:success", async (data) => {
1538
+ if (this.options.onCommitmentSuccess) {
1539
+ this.options.onCommitmentSuccess({
1540
+ subscriptionId: data.subscriptionId,
1541
+ interval: data.interval,
1542
+ txHash: data.txHash,
1543
+ blockNumber: data.blockNumber,
1544
+ gasUsed: data.gasUsed || "0",
1545
+ gasPrice: data.gasPrice || "0",
1546
+ gasCost: data.gasCost || "0"
1547
+ });
1548
+ }
1549
+ if (data.requestStartedEvent) {
1550
+ console.log(` \u{1F4E5} Processing RequestStarted from prepare receipt (fallback for missed WebSocket)`);
1551
+ await this.handleRequest(data.requestStartedEvent);
1552
+ }
1553
+ });
1554
+ this.isRunning = true;
1555
+ console.log("\u2713 Noosphere Agent is running");
1556
+ console.log("Listening for requests...");
1557
+ }
1558
+ /**
1559
+ * Convert registry ContainerMetadata to agent-core ContainerMetadata
1560
+ */
1561
+ convertRegistryContainer(registryContainer) {
1562
+ const [image, tag] = registryContainer.imageName.split(":");
1563
+ return {
1564
+ id: registryContainer.id,
1565
+ name: registryContainer.name,
1566
+ image,
1567
+ tag: tag || "latest",
1568
+ port: registryContainer.port?.toString(),
1569
+ env: registryContainer.env,
1570
+ requirements: registryContainer.requirements,
1571
+ payments: registryContainer.payments ? {
1572
+ basePrice: registryContainer.payments.basePrice,
1573
+ unit: registryContainer.payments.token,
1574
+ per: registryContainer.payments.per
1575
+ } : void 0,
1576
+ verified: registryContainer.verified
1577
+ };
1578
+ }
1579
+ async handleRequest(event) {
1580
+ const requestIdShort = event.requestId.slice(0, 10);
1581
+ if (this.processingRequests.has(event.requestId)) {
1582
+ console.log(` \u23ED\uFE0F Request ${requestIdShort}... already being processed, skipping duplicate`);
1583
+ return;
1584
+ }
1585
+ if (this.options.isRequestProcessed && this.options.isRequestProcessed(event.requestId)) {
1586
+ console.log(` \u23ED\uFE0F Request ${requestIdShort}... already processed, skipping`);
1587
+ return;
1588
+ }
1589
+ this.processingRequests.add(event.requestId);
1590
+ console.log(`
1591
+ [${(/* @__PURE__ */ new Date()).toISOString()}] RequestStarted: ${requestIdShort}...`);
1592
+ console.log(` SubscriptionId: ${event.subscriptionId}`);
1593
+ console.log(` Interval: ${event.interval}`);
1594
+ console.log(` ContainerId: ${event.containerId.slice(0, 10)}...`);
1595
+ if (this.options.onRequestStarted) {
1596
+ this.options.onRequestStarted({
1597
+ requestId: event.requestId,
1598
+ subscriptionId: Number(event.subscriptionId),
1599
+ interval: Number(event.interval),
1600
+ containerId: event.containerId,
1601
+ redundancy: event.redundancy,
1602
+ feeAmount: event.feeAmount.toString(),
1603
+ feeToken: event.feeToken,
1604
+ verifier: event.verifier,
1605
+ walletAddress: event.walletAddress,
1606
+ blockNumber: event.blockNumber
1607
+ });
1608
+ }
1609
+ try {
1610
+ const currentInterval = await this.router.getComputeSubscriptionInterval(event.subscriptionId);
1611
+ const eventInterval = Number(event.interval);
1612
+ const isOneTimeExecution = currentInterval === 4294967295n;
1613
+ if (!isOneTimeExecution && currentInterval > eventInterval + 2) {
1614
+ console.log(` \u23ED\uFE0F Skipping old interval ${eventInterval} (current: ${currentInterval})`);
1615
+ if (this.options.onRequestSkipped) {
1616
+ this.options.onRequestSkipped(event.requestId, `Old interval ${eventInterval} (current: ${currentInterval})`);
1617
+ }
1618
+ this.processingRequests.delete(event.requestId);
1619
+ return;
1620
+ }
1621
+ } catch (error) {
1622
+ console.warn(` Could not verify interval currency:`, error.message);
1623
+ }
1624
+ this.scheduler.markIntervalCommitted(BigInt(event.subscriptionId), BigInt(event.interval));
1625
+ try {
1626
+ await this.waitForPriority(event);
1627
+ if (this.options.onRequestProcessing) {
1628
+ this.options.onRequestProcessing(event.requestId);
1629
+ }
1630
+ const currentCount = await this.coordinator.redundancyCount(event.requestId);
1631
+ if (currentCount >= event.redundancy) {
1632
+ console.log(` \u23ED\uFE0F Already fulfilled (${currentCount}/${event.redundancy}), skipping`);
1633
+ if (this.options.onRequestSkipped) {
1634
+ this.options.onRequestSkipped(event.requestId, `Already fulfilled (${currentCount}/${event.redundancy})`);
1635
+ }
1636
+ this.processingRequests.delete(event.requestId);
1637
+ return;
1638
+ }
1639
+ let container;
1640
+ if (this.getContainer) {
1641
+ container = this.getContainer(event.containerId);
1642
+ if (container) {
1643
+ console.log(` \u{1F4E6} Container found via callback: ${container.name}`);
1644
+ }
1645
+ }
1646
+ if (!container) {
1647
+ const registryContainer = this.registryManager.getContainer(event.containerId);
1648
+ if (registryContainer) {
1649
+ console.log(` \u{1F4CB} Container found in registry: ${registryContainer.name}`);
1650
+ container = this.convertRegistryContainer(registryContainer);
1651
+ }
1652
+ }
1653
+ if (!container && this.containers) {
1654
+ container = this.containers.get(event.containerId);
1655
+ if (container) {
1656
+ console.log(` \u{1F4E6} Container found in config: ${container.name}`);
1657
+ }
1658
+ }
1659
+ if (!container) {
1660
+ console.error(` \u274C Container not found: ${event.containerId}`);
1661
+ console.error(` \u{1F4A1} Try adding it to the registry or config file`);
1662
+ if (this.options.onRequestSkipped) {
1663
+ this.options.onRequestSkipped(event.requestId, `Container not found: ${event.containerId}`);
1664
+ }
1665
+ return;
1666
+ }
1667
+ console.log(
1668
+ ` \u{1F4E6} Using container: ${container.name} (${container.image}:${container.tag || "latest"})`
1669
+ );
1670
+ const subscription = await this.router.getComputeSubscription(event.subscriptionId);
1671
+ const clientAddress = subscription.client;
1672
+ if (!clientAddress || clientAddress === "0x0000000000000000000000000000000000000000") {
1673
+ console.error(` \u274C Invalid client address for subscription ${event.subscriptionId}`);
1674
+ if (this.options.onRequestFailed) {
1675
+ this.options.onRequestFailed(event.requestId, `Invalid client address for subscription ${event.subscriptionId}`);
1676
+ }
1677
+ return;
1678
+ }
1679
+ console.log(` \u{1F4DE} Fetching inputs from client: ${clientAddress.slice(0, 10)}...`);
1680
+ const clientAbi = [
1681
+ "function getComputeInputs(uint64 subscriptionId, uint32 interval, uint32 timestamp, address caller) external view returns (bytes memory)"
1682
+ ];
1683
+ const client = new ethers4.Contract(clientAddress, clientAbi, this.provider);
1684
+ const timestamp = Math.floor(Date.now() / 1e3);
1685
+ let inputBytes;
1686
+ try {
1687
+ inputBytes = await client.getComputeInputs(
1688
+ event.subscriptionId,
1689
+ event.interval,
1690
+ timestamp,
1691
+ this.walletManager.getAddress()
1692
+ );
1693
+ } catch (error) {
1694
+ const errorMessage = error.message || String(error);
1695
+ console.error(` \u274C Failed to get inputs from client:`, error);
1696
+ if (this.options.onRequestFailed) {
1697
+ this.options.onRequestFailed(event.requestId, `Failed to get inputs: ${errorMessage}`);
1698
+ }
1699
+ return;
1700
+ }
1701
+ const inputData = ethers4.toUtf8String(inputBytes);
1702
+ console.log(
1703
+ ` \u{1F4E5} Inputs received: ${inputData.substring(0, 100)}${inputData.length > 100 ? "..." : ""}`
1704
+ );
1705
+ console.log(` \u2699\uFE0F Executing...`);
1706
+ const result = await this.containerManager.runContainer(
1707
+ container,
1708
+ inputData,
1709
+ 3e5
1710
+ // 5 min timeout
1711
+ );
1712
+ if (result.exitCode !== 0) {
1713
+ console.error(` \u274C Container execution failed with exit code ${result.exitCode}`);
1714
+ console.error(` \u{1F4C4} Container output:`, result.output);
1715
+ if (this.options.onRequestFailed) {
1716
+ this.options.onRequestFailed(event.requestId, `Container execution failed with exit code ${result.exitCode}`);
1717
+ }
1718
+ return;
1719
+ }
1720
+ console.log(` \u2713 Execution completed in ${result.executionTime}ms`);
1721
+ console.log(` \u{1F4E4} Submitting result...`);
1722
+ const input = inputData;
1723
+ const output = result.output;
1724
+ const proof = event.verifier ? result.output : "";
1725
+ const subscriptionWallet = event.walletAddress;
1726
+ const commitment = CommitmentUtils.fromEvent(event, subscriptionWallet);
1727
+ const commitmentData = CommitmentUtils.encode(commitment);
1728
+ const nodeWallet = this.paymentWallet || this.walletManager.getAddress();
1729
+ const tx = await this.coordinator.reportComputeResult(
1730
+ event.interval,
1731
+ ethers4.toUtf8Bytes(input),
1732
+ ethers4.toUtf8Bytes(output),
1733
+ ethers4.toUtf8Bytes(proof),
1734
+ commitmentData,
1735
+ nodeWallet
1736
+ );
1737
+ console.log(` \u{1F4E4} Transaction sent: ${tx.hash}`);
1738
+ const receipt = await tx.wait();
1739
+ if (receipt.status === 1) {
1740
+ console.log(` \u2713 Result delivered successfully (block ${receipt.blockNumber})`);
1741
+ console.log(` \u{1F4B0} Fee earned: ${ethers4.formatEther(event.feeAmount)} ETH`);
1742
+ if (this.options.onComputeDelivered) {
1743
+ this.options.onComputeDelivered({
1744
+ requestId: event.requestId,
1745
+ subscriptionId: Number(event.subscriptionId),
1746
+ interval: Number(event.interval),
1747
+ containerId: event.containerId,
1748
+ redundancy: event.redundancy,
1749
+ feeAmount: event.feeAmount.toString(),
1750
+ feeToken: event.feeToken,
1751
+ input,
1752
+ output,
1753
+ txHash: tx.hash,
1754
+ blockNumber: receipt.blockNumber,
1755
+ gasUsed: receipt.gasUsed,
1756
+ gasPrice: receipt.gasPrice || tx.gasPrice || 0n
1757
+ });
1758
+ }
1759
+ } else {
1760
+ throw new Error(`Delivery transaction failed with status ${receipt.status}`);
1761
+ }
1762
+ } catch (error) {
1763
+ const errorMessage = error.message || String(error);
1764
+ const errorCode = error.code;
1765
+ if (errorCode === "NONCE_EXPIRED" || errorMessage.includes("nonce has already been used") || errorMessage.includes("nonce too low")) {
1766
+ console.log(` \u26A0\uFE0F Nonce expired (likely already processed by another handler)`);
1767
+ return;
1768
+ }
1769
+ console.error(` \u274C Error processing request:`, error);
1770
+ if (this.options.onRequestFailed) {
1771
+ this.options.onRequestFailed(event.requestId, errorMessage);
1772
+ }
1773
+ } finally {
1774
+ this.processingRequests.delete(event.requestId);
1775
+ }
1776
+ }
1777
+ /**
1778
+ * Self-coordination: Calculate priority and wait
1779
+ */
1780
+ async waitForPriority(event) {
1781
+ const priority = this.calculatePriority(event.requestId);
1782
+ const maxDelay = event.redundancy === 1 ? 1e3 : 200;
1783
+ const delay = Math.floor(priority / 4294967295 * maxDelay);
1784
+ if (delay > 0) {
1785
+ console.log(
1786
+ ` \u23F1\uFE0F Priority wait: ${delay}ms (priority: 0x${priority.toString(16).slice(0, 8)})`
1787
+ );
1788
+ await new Promise((resolve) => setTimeout(resolve, delay));
1789
+ }
1790
+ }
1791
+ /**
1792
+ * Calculate deterministic priority for this agent and request
1793
+ */
1794
+ calculatePriority(requestId) {
1795
+ const hash = ethers4.keccak256(ethers4.concat([requestId, this.walletManager.getAddress()]));
1796
+ return parseInt(hash.slice(2, 10), 16);
1797
+ }
1798
+ async stop() {
1799
+ console.log("Stopping Noosphere Agent...");
1800
+ await this.eventMonitor.stop();
1801
+ this.scheduler.stop();
1802
+ await this.containerManager.cleanup();
1803
+ await this.containerManager.stopPersistentContainers();
1804
+ this.isRunning = false;
1805
+ console.log("\u2713 Agent stopped");
1806
+ }
1807
+ getStatus() {
1808
+ return {
1809
+ running: this.isRunning,
1810
+ address: this.walletManager.getAddress(),
1811
+ scheduler: this.scheduler.getStats(),
1812
+ containers: {
1813
+ runningCount: this.containerManager.getRunningContainerCount()
1814
+ }
1815
+ };
1816
+ }
1817
+ /**
1818
+ * Get scheduler service (for advanced usage)
1819
+ */
1820
+ getScheduler() {
1821
+ return this.scheduler;
1822
+ }
1823
+ };
1824
+
1825
+ // src/types/index.ts
1826
+ var FulfillResult = /* @__PURE__ */ ((FulfillResult2) => {
1827
+ FulfillResult2[FulfillResult2["FULFILLED"] = 0] = "FULFILLED";
1828
+ FulfillResult2[FulfillResult2["INVALID_REQUEST_ID"] = 1] = "INVALID_REQUEST_ID";
1829
+ FulfillResult2[FulfillResult2["INVALID_COMMITMENT"] = 2] = "INVALID_COMMITMENT";
1830
+ FulfillResult2[FulfillResult2["SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION"] = 3] = "SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION";
1831
+ FulfillResult2[FulfillResult2["INSUFFICIENT_SUBSCRIPTION_BALANCE"] = 4] = "INSUFFICIENT_SUBSCRIPTION_BALANCE";
1832
+ FulfillResult2[FulfillResult2["COST_EXCEEDS_COMMITMENT"] = 5] = "COST_EXCEEDS_COMMITMENT";
1833
+ return FulfillResult2;
1834
+ })(FulfillResult || {});
1835
+
1836
+ // src/utils/RequestIdUtils.ts
1837
+ import { ethers as ethers5 } from "ethers";
1838
+ var RequestIdUtils = class {
1839
+ /**
1840
+ * Pack subscriptionId and interval into requestId
1841
+ * Matches Solidity: keccak256(abi.encodePacked(subscriptionId, interval))
1842
+ */
1843
+ static pack(subscriptionId, interval) {
1844
+ const subscriptionIdBytes = ethers5.zeroPadValue(ethers5.toBeHex(subscriptionId), 8);
1845
+ const intervalBytes = ethers5.zeroPadValue(ethers5.toBeHex(interval), 4);
1846
+ const packed = ethers5.concat([subscriptionIdBytes, intervalBytes]);
1847
+ return ethers5.keccak256(packed);
1848
+ }
1849
+ /**
1850
+ * Unpack requestId into subscriptionId and interval (if stored separately)
1851
+ * Note: This is not possible from the hash alone - only for informational purposes
1852
+ * In practice, subscriptionId and interval are stored in events
1853
+ */
1854
+ static format(requestId, subscriptionId, interval) {
1855
+ return `Request(id=${requestId.slice(0, 10)}..., sub=${subscriptionId}, interval=${interval})`;
1856
+ }
1857
+ };
1858
+
1859
+ // src/index.ts
1860
+ import { WalletManager as WalletManager2, KeystoreManager as KeystoreManager2 } from "@noosphere/crypto";
1861
+ import { RegistryManager as RegistryManager2 } from "@noosphere/registry";
1862
+ export {
1863
+ CommitmentUtils,
1864
+ ConfigLoader,
1865
+ ContainerManager,
1866
+ EventMonitor,
1867
+ FulfillResult,
1868
+ KeystoreManager2 as KeystoreManager,
1869
+ NoosphereAgent,
1870
+ RegistryManager2 as RegistryManager,
1871
+ RequestIdUtils,
1872
+ SchedulerService,
1873
+ WalletManager2 as WalletManager
1874
+ };
1875
+ //# sourceMappingURL=index.js.map