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