@hivemind-os/collective-indexer 0.2.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.d.ts +287 -0
- package/dist/index.js +2429 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2429 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import pino2 from "pino";
|
|
6
|
+
import { MeshSuiClient as MeshSuiClient2 } from "@hivemind-os/collective-core";
|
|
7
|
+
|
|
8
|
+
// src/analytics.ts
|
|
9
|
+
import { BidStatus } from "@hivemind-os/collective-types";
|
|
10
|
+
var AnalyticsEngine = class {
|
|
11
|
+
constructor(store) {
|
|
12
|
+
this.store = store;
|
|
13
|
+
}
|
|
14
|
+
store;
|
|
15
|
+
getSummary() {
|
|
16
|
+
const db = this.store.getDatabase();
|
|
17
|
+
const totals = db.prepare(
|
|
18
|
+
`SELECT
|
|
19
|
+
COUNT(*) AS total_agents,
|
|
20
|
+
SUM(CASE WHEN active = 1 THEN 1 ELSE 0 END) AS active_agents,
|
|
21
|
+
COALESCE((SELECT COUNT(*) FROM tasks), 0) AS total_tasks,
|
|
22
|
+
COALESCE((SELECT SUM(CASE WHEN completed_at IS NOT NULL THEN 1 ELSE 0 END) FROM tasks), 0) AS completed_tasks,
|
|
23
|
+
COALESCE((SELECT SUM(CASE WHEN disputed_at IS NOT NULL THEN 1 ELSE 0 END) FROM tasks), 0) AS disputed_tasks
|
|
24
|
+
FROM agents`
|
|
25
|
+
).get();
|
|
26
|
+
return {
|
|
27
|
+
totalAgents: Number(totals.total_agents ?? 0),
|
|
28
|
+
activeAgents: Number(totals.active_agents ?? 0),
|
|
29
|
+
totalTasks: Number(totals.total_tasks ?? 0),
|
|
30
|
+
completedTasks: Number(totals.completed_tasks ?? 0),
|
|
31
|
+
disputedTasks: Number(totals.disputed_tasks ?? 0),
|
|
32
|
+
totalVolumeMist: sumBigIntRows(db.prepare("SELECT price FROM tasks").all()),
|
|
33
|
+
averageGasCosts: this.getAverageGasCostsByTaskType(),
|
|
34
|
+
marketplace: this.getMarketplaceStats()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
getTaskVolume(period = "day", buckets = 14) {
|
|
38
|
+
const db = this.store.getDatabase();
|
|
39
|
+
const rows = db.prepare(
|
|
40
|
+
`SELECT ${bucketSql(period)} AS label, created_at, price
|
|
41
|
+
FROM tasks
|
|
42
|
+
ORDER BY created_at DESC`
|
|
43
|
+
).all();
|
|
44
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
const current = grouped.get(row.label) ?? { count: 0, volumeMist: 0n, createdAt: Number(row.created_at) };
|
|
47
|
+
current.count += 1;
|
|
48
|
+
current.volumeMist += toBigInt(row.price);
|
|
49
|
+
current.createdAt = Math.min(current.createdAt, Number(row.created_at));
|
|
50
|
+
grouped.set(row.label, current);
|
|
51
|
+
}
|
|
52
|
+
return [...grouped.entries()].sort((left, right) => right[1].createdAt - left[1].createdAt).slice(0, normalizeBucketCount(buckets)).reverse().map(([label, row]) => ({
|
|
53
|
+
label,
|
|
54
|
+
count: row.count,
|
|
55
|
+
volumeMist: row.volumeMist
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
getAverageGasCostsByTaskType() {
|
|
59
|
+
const db = this.store.getDatabase();
|
|
60
|
+
const rows = db.prepare(
|
|
61
|
+
`SELECT capability, gas_cost_mist_total
|
|
62
|
+
FROM tasks
|
|
63
|
+
ORDER BY capability ASC`
|
|
64
|
+
).all();
|
|
65
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
const current = grouped.get(row.capability) ?? { total: 0n, count: 0 };
|
|
68
|
+
current.total += toBigInt(row.gas_cost_mist_total);
|
|
69
|
+
current.count += 1;
|
|
70
|
+
grouped.set(row.capability, current);
|
|
71
|
+
}
|
|
72
|
+
return [...grouped.entries()].map(([capability, stats]) => ({
|
|
73
|
+
capability,
|
|
74
|
+
averageGasMist: stats.count === 0 ? 0n : stats.total / BigInt(stats.count),
|
|
75
|
+
taskCount: stats.count
|
|
76
|
+
})).sort((left, right) => compareNumber(right.taskCount, left.taskCount) || left.capability.localeCompare(right.capability));
|
|
77
|
+
}
|
|
78
|
+
getMarketplaceStats() {
|
|
79
|
+
const db = this.store.getDatabase();
|
|
80
|
+
const aggregates = db.prepare(
|
|
81
|
+
`SELECT
|
|
82
|
+
COALESCE(AVG(bid_count), 0) AS average_bid_count,
|
|
83
|
+
COALESCE((SELECT CAST(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS REAL) / NULLIF(COUNT(*), 0) FROM bids), 0) AS acceptance_rate
|
|
84
|
+
FROM tasks`
|
|
85
|
+
).get(BidStatus.ACCEPTED);
|
|
86
|
+
const categories = db.prepare("SELECT category, COUNT(*) AS task_count FROM tasks GROUP BY category ORDER BY task_count DESC, category ASC").all();
|
|
87
|
+
return {
|
|
88
|
+
averageBidCount: Number(aggregates.average_bid_count ?? 0),
|
|
89
|
+
acceptanceRate: Number(aggregates.acceptance_rate ?? 0),
|
|
90
|
+
categoryPopularity: categories.map((row) => ({ category: row.category, taskCount: Number(row.task_count) }))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
getReputationTrends(agentDid, period = "day", buckets = 12) {
|
|
94
|
+
const agent = this.store.getAgentByDid(agentDid);
|
|
95
|
+
if (!agent) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const db = this.store.getDatabase();
|
|
99
|
+
const rows = db.prepare(
|
|
100
|
+
`SELECT
|
|
101
|
+
${bucketSql(period)} AS label,
|
|
102
|
+
created_at,
|
|
103
|
+
completed_at,
|
|
104
|
+
disputed_at
|
|
105
|
+
FROM tasks
|
|
106
|
+
WHERE provider = ?
|
|
107
|
+
ORDER BY created_at DESC`
|
|
108
|
+
).all(agent.owner);
|
|
109
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
const current = grouped.get(row.label) ?? { completed: 0, failed: 0, disputed: 0, createdAt: Number(row.created_at) };
|
|
112
|
+
if (row.disputed_at != null) {
|
|
113
|
+
current.failed += 1;
|
|
114
|
+
current.disputed += 1;
|
|
115
|
+
} else if (row.completed_at != null) {
|
|
116
|
+
current.completed += 1;
|
|
117
|
+
}
|
|
118
|
+
current.createdAt = Math.min(current.createdAt, Number(row.created_at));
|
|
119
|
+
grouped.set(row.label, current);
|
|
120
|
+
}
|
|
121
|
+
return [...grouped.entries()].sort((left, right) => right[1].createdAt - left[1].createdAt).slice(0, normalizeBucketCount(buckets)).reverse().map(([label, row]) => ({
|
|
122
|
+
label,
|
|
123
|
+
completed: row.completed,
|
|
124
|
+
failed: row.failed,
|
|
125
|
+
disputed: row.disputed,
|
|
126
|
+
successRate: row.completed + row.failed === 0 ? 0 : row.completed / (row.completed + row.failed)
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
getTopProviders(limit = 10, sortBy = "completedTasks") {
|
|
130
|
+
const db = this.store.getDatabase();
|
|
131
|
+
const rows = db.prepare(
|
|
132
|
+
`SELECT
|
|
133
|
+
did,
|
|
134
|
+
owner,
|
|
135
|
+
name,
|
|
136
|
+
COALESCE(total_tasks_completed, 0) AS completed_tasks,
|
|
137
|
+
COALESCE(total_tasks_failed, 0) AS failed_tasks,
|
|
138
|
+
COALESCE(total_tasks_disputed, 0) AS dispute_count,
|
|
139
|
+
COALESCE(total_earnings_mist, '0') AS earnings_mist
|
|
140
|
+
FROM agents
|
|
141
|
+
WHERE active = 1
|
|
142
|
+
ORDER BY updated_at DESC`
|
|
143
|
+
).all();
|
|
144
|
+
const scored = rows.map((row) => {
|
|
145
|
+
const completedTasks = Number(row.completed_tasks ?? 0);
|
|
146
|
+
const failedTasks = Number(row.failed_tasks ?? 0);
|
|
147
|
+
const disputeCount = Number(row.dispute_count ?? 0);
|
|
148
|
+
const earningsMist = BigInt(row.earnings_mist ?? "0");
|
|
149
|
+
const successRate = completedTasks + failedTasks === 0 ? 0 : completedTasks / (completedTasks + failedTasks);
|
|
150
|
+
const reputation = successRate * 100 + completedTasks + Number(earningsMist > 0n ? 1n : 0n) - disputeCount;
|
|
151
|
+
return {
|
|
152
|
+
did: row.did,
|
|
153
|
+
owner: row.owner,
|
|
154
|
+
name: row.name,
|
|
155
|
+
completedTasks,
|
|
156
|
+
earningsMist,
|
|
157
|
+
disputeCount,
|
|
158
|
+
successRate,
|
|
159
|
+
reputation
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return [...scored].sort((left, right) => {
|
|
163
|
+
if (sortBy === "earnings") {
|
|
164
|
+
return compareBigInt(right.earningsMist, left.earningsMist) || compareNumber(right.completedTasks, left.completedTasks);
|
|
165
|
+
}
|
|
166
|
+
if (sortBy === "reputation") {
|
|
167
|
+
return compareNumber(right.reputation, left.reputation) || compareNumber(right.completedTasks, left.completedTasks);
|
|
168
|
+
}
|
|
169
|
+
return compareNumber(right.completedTasks, left.completedTasks) || compareBigInt(right.earningsMist, left.earningsMist);
|
|
170
|
+
}).slice(0, Math.max(1, Math.floor(limit)));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function normalizeBucketCount(buckets) {
|
|
174
|
+
return Number.isFinite(buckets) ? Math.max(1, Math.floor(buckets)) : 14;
|
|
175
|
+
}
|
|
176
|
+
function bucketSql(period) {
|
|
177
|
+
switch (period) {
|
|
178
|
+
case "hour":
|
|
179
|
+
return "strftime('%Y-%m-%dT%H:00:00Z', created_at / 1000, 'unixepoch')";
|
|
180
|
+
case "week":
|
|
181
|
+
return "strftime('%Y-W%W', created_at / 1000, 'unixepoch')";
|
|
182
|
+
case "day":
|
|
183
|
+
default:
|
|
184
|
+
return "strftime('%Y-%m-%d', created_at / 1000, 'unixepoch')";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function compareNumber(left, right) {
|
|
188
|
+
if (left === right) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
return left > right ? 1 : -1;
|
|
192
|
+
}
|
|
193
|
+
function compareBigInt(left, right) {
|
|
194
|
+
if (left === right) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
return left > right ? 1 : -1;
|
|
198
|
+
}
|
|
199
|
+
function sumBigIntRows(rows) {
|
|
200
|
+
return rows.reduce((sum, row) => sum + toBigInt(row.price), 0n);
|
|
201
|
+
}
|
|
202
|
+
function toBigInt(value) {
|
|
203
|
+
if (typeof value === "bigint") {
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
if (typeof value === "number") {
|
|
207
|
+
return BigInt(value);
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === "string" && value.length > 0) {
|
|
210
|
+
return BigInt(value);
|
|
211
|
+
}
|
|
212
|
+
return 0n;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/config.ts
|
|
216
|
+
import { homedir } from "os";
|
|
217
|
+
import { join, resolve } from "path";
|
|
218
|
+
function getDefaultIndexerConfig(baseDir = resolve(homedir(), ".hivemind-os/collective", "indexer")) {
|
|
219
|
+
return {
|
|
220
|
+
rpcUrl: "http://127.0.0.1:9000",
|
|
221
|
+
packageId: "",
|
|
222
|
+
sqlitePath: join(baseDir, "indexer.sqlite"),
|
|
223
|
+
pollingIntervalMs: 5e3,
|
|
224
|
+
server: {
|
|
225
|
+
host: "0.0.0.0",
|
|
226
|
+
port: 4e3
|
|
227
|
+
},
|
|
228
|
+
backfill: {}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function loadIndexerConfig(overrides = {}) {
|
|
232
|
+
const baseDir = process.env.COLLECTIVE_INDEXER_DATA_DIR ? resolve(process.env.COLLECTIVE_INDEXER_DATA_DIR) : void 0;
|
|
233
|
+
const defaults = getDefaultIndexerConfig(baseDir);
|
|
234
|
+
const config = {
|
|
235
|
+
rpcUrl: process.env.COLLECTIVE_RPC_URL ?? process.env.COLLECTIVE_INDEXER_RPC_URL ?? overrides.rpcUrl ?? defaults.rpcUrl,
|
|
236
|
+
packageId: process.env.COLLECTIVE_PACKAGE_ID ?? process.env.COLLECTIVE_INDEXER_PACKAGE_ID ?? overrides.packageId ?? defaults.packageId,
|
|
237
|
+
sqlitePath: resolve(overrides.sqlitePath ?? process.env.COLLECTIVE_INDEXER_SQLITE_PATH ?? defaults.sqlitePath),
|
|
238
|
+
pollingIntervalMs: readNumber(process.env.COLLECTIVE_INDEXER_POLLING_INTERVAL_MS) ?? overrides.pollingIntervalMs ?? defaults.pollingIntervalMs,
|
|
239
|
+
server: {
|
|
240
|
+
host: overrides.server?.host ?? process.env.COLLECTIVE_INDEXER_HOST ?? defaults.server.host,
|
|
241
|
+
port: readNumber(process.env.COLLECTIVE_INDEXER_PORT) ?? overrides.server?.port ?? defaults.server.port
|
|
242
|
+
},
|
|
243
|
+
backfill: overrides.backfill || process.env.COLLECTIVE_INDEXER_START_CHECKPOINT ? {
|
|
244
|
+
fromCheckpoint: overrides.backfill?.fromCheckpoint ?? readNumber(process.env.COLLECTIVE_INDEXER_START_CHECKPOINT)
|
|
245
|
+
} : defaults.backfill
|
|
246
|
+
};
|
|
247
|
+
validateIndexerConfig(config);
|
|
248
|
+
return config;
|
|
249
|
+
}
|
|
250
|
+
function validateIndexerConfig(config) {
|
|
251
|
+
if (!config.rpcUrl) {
|
|
252
|
+
throw new Error("Indexer rpcUrl is required.");
|
|
253
|
+
}
|
|
254
|
+
if (!Number.isInteger(config.pollingIntervalMs) || config.pollingIntervalMs <= 0) {
|
|
255
|
+
throw new Error("Indexer polling interval must be a positive integer.");
|
|
256
|
+
}
|
|
257
|
+
if (!config.server.host) {
|
|
258
|
+
throw new Error("Indexer server host is required.");
|
|
259
|
+
}
|
|
260
|
+
if (!Number.isInteger(config.server.port) || config.server.port <= 0) {
|
|
261
|
+
throw new Error("Indexer server port must be a positive integer.");
|
|
262
|
+
}
|
|
263
|
+
if (!config.sqlitePath) {
|
|
264
|
+
throw new Error("Indexer sqlitePath is required.");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function readNumber(value) {
|
|
268
|
+
if (!value) {
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
const parsed = Number(value);
|
|
272
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/graphql/server.ts
|
|
276
|
+
import { createServer } from "http";
|
|
277
|
+
import { createSchema, createYoga } from "graphql-yoga";
|
|
278
|
+
import { BidStatus as BidStatus3, DisputeStatus as DisputeStatus2, PaymentScheme as PaymentScheme2, TaskStatus as TaskStatus2 } from "@hivemind-os/collective-types";
|
|
279
|
+
|
|
280
|
+
// src/store.ts
|
|
281
|
+
import Database from "better-sqlite3";
|
|
282
|
+
import { ReputationScoreCalculator } from "@hivemind-os/collective-core";
|
|
283
|
+
import { BidStatus as BidStatus2, PaymentScheme, TaskStatus } from "@hivemind-os/collective-types";
|
|
284
|
+
var IndexerStore = class {
|
|
285
|
+
db;
|
|
286
|
+
scoreCalculator = new ReputationScoreCalculator();
|
|
287
|
+
constructor(dbPath) {
|
|
288
|
+
this.db = new Database(dbPath);
|
|
289
|
+
this.db.defaultSafeIntegers(true);
|
|
290
|
+
this.db.function("add_bigint", { deterministic: true }, (left, right) => {
|
|
291
|
+
return (toBigInt2(left) + toBigInt2(right)).toString();
|
|
292
|
+
});
|
|
293
|
+
this.db.function("subtract_bigint", { deterministic: true }, (left, right) => {
|
|
294
|
+
const value = toBigInt2(left) - toBigInt2(right);
|
|
295
|
+
return value < 0n ? "0" : value.toString();
|
|
296
|
+
});
|
|
297
|
+
this.db.exec(`
|
|
298
|
+
CREATE TABLE IF NOT EXISTS indexer_cursors (
|
|
299
|
+
stream_key TEXT PRIMARY KEY,
|
|
300
|
+
cursor_json TEXT NOT NULL,
|
|
301
|
+
updated_at INTEGER NOT NULL
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
305
|
+
event_id TEXT PRIMARY KEY,
|
|
306
|
+
event_type TEXT NOT NULL,
|
|
307
|
+
package_id TEXT NOT NULL,
|
|
308
|
+
tx_digest TEXT NOT NULL,
|
|
309
|
+
module_name TEXT,
|
|
310
|
+
checkpoint INTEGER,
|
|
311
|
+
timestamp_ms INTEGER NOT NULL,
|
|
312
|
+
payload_json TEXT NOT NULL,
|
|
313
|
+
indexed_at INTEGER NOT NULL
|
|
314
|
+
);
|
|
315
|
+
CREATE INDEX IF NOT EXISTS events_type_timestamp_idx ON events (event_type, timestamp_ms DESC);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS events_tx_digest_idx ON events (tx_digest);
|
|
317
|
+
|
|
318
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
319
|
+
id TEXT PRIMARY KEY,
|
|
320
|
+
owner TEXT NOT NULL,
|
|
321
|
+
did TEXT UNIQUE NOT NULL,
|
|
322
|
+
name TEXT NOT NULL,
|
|
323
|
+
description TEXT,
|
|
324
|
+
capabilities_json TEXT,
|
|
325
|
+
capabilities_text TEXT,
|
|
326
|
+
endpoint TEXT,
|
|
327
|
+
encryption_public_key TEXT,
|
|
328
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
329
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
330
|
+
registered_at INTEGER,
|
|
331
|
+
updated_at INTEGER,
|
|
332
|
+
total_tasks_completed INTEGER,
|
|
333
|
+
total_tasks_failed INTEGER,
|
|
334
|
+
total_tasks_disputed INTEGER,
|
|
335
|
+
total_earnings_mist TEXT,
|
|
336
|
+
has_stake INTEGER,
|
|
337
|
+
stake_mist TEXT,
|
|
338
|
+
stake_type TEXT
|
|
339
|
+
);
|
|
340
|
+
CREATE INDEX IF NOT EXISTS agents_owner_idx ON agents (owner);
|
|
341
|
+
CREATE INDEX IF NOT EXISTS agents_did_idx ON agents (did);
|
|
342
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS agents_fts USING fts5(agent_id UNINDEXED, name, description, capabilities_text);
|
|
343
|
+
|
|
344
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
345
|
+
id TEXT PRIMARY KEY,
|
|
346
|
+
requester TEXT NOT NULL,
|
|
347
|
+
provider TEXT,
|
|
348
|
+
capability TEXT NOT NULL,
|
|
349
|
+
category TEXT NOT NULL,
|
|
350
|
+
input_blob_id TEXT NOT NULL,
|
|
351
|
+
result_blob_id TEXT,
|
|
352
|
+
price TEXT NOT NULL,
|
|
353
|
+
payment_scheme TEXT,
|
|
354
|
+
max_price TEXT,
|
|
355
|
+
metered_units INTEGER,
|
|
356
|
+
unit_price TEXT,
|
|
357
|
+
verification_hash TEXT,
|
|
358
|
+
status INTEGER NOT NULL,
|
|
359
|
+
dispute_window_ms INTEGER,
|
|
360
|
+
created_at INTEGER NOT NULL,
|
|
361
|
+
accepted_at INTEGER,
|
|
362
|
+
completed_at INTEGER,
|
|
363
|
+
released_at INTEGER,
|
|
364
|
+
disputed_at INTEGER,
|
|
365
|
+
cancelled_at INTEGER,
|
|
366
|
+
expires_at INTEGER NOT NULL,
|
|
367
|
+
agreement_hash TEXT,
|
|
368
|
+
posted_tx_digest TEXT,
|
|
369
|
+
accepted_tx_digest TEXT,
|
|
370
|
+
completed_tx_digest TEXT,
|
|
371
|
+
released_tx_digest TEXT,
|
|
372
|
+
disputed_tx_digest TEXT,
|
|
373
|
+
cancelled_tx_digest TEXT,
|
|
374
|
+
gas_cost_mist_total TEXT DEFAULT '0',
|
|
375
|
+
bid_count INTEGER NOT NULL DEFAULT 0
|
|
376
|
+
);
|
|
377
|
+
CREATE INDEX IF NOT EXISTS tasks_status_created_idx ON tasks (status, created_at DESC);
|
|
378
|
+
CREATE INDEX IF NOT EXISTS tasks_provider_created_idx ON tasks (provider, created_at DESC);
|
|
379
|
+
CREATE INDEX IF NOT EXISTS tasks_requester_created_idx ON tasks (requester, created_at DESC);
|
|
380
|
+
CREATE INDEX IF NOT EXISTS tasks_category_created_idx ON tasks (category, created_at DESC);
|
|
381
|
+
CREATE INDEX IF NOT EXISTS tasks_capability_created_idx ON tasks (capability, created_at DESC);
|
|
382
|
+
CREATE INDEX IF NOT EXISTS tasks_created_cursor_idx ON tasks (created_at DESC, id DESC);
|
|
383
|
+
|
|
384
|
+
CREATE TABLE IF NOT EXISTS task_transitions (
|
|
385
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
386
|
+
task_id TEXT NOT NULL,
|
|
387
|
+
event_type TEXT NOT NULL,
|
|
388
|
+
status INTEGER NOT NULL,
|
|
389
|
+
tx_digest TEXT NOT NULL,
|
|
390
|
+
timestamp_ms INTEGER NOT NULL,
|
|
391
|
+
payload_json TEXT NOT NULL
|
|
392
|
+
);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS task_transitions_task_idx ON task_transitions (task_id, timestamp_ms ASC, id ASC);
|
|
394
|
+
|
|
395
|
+
CREATE TABLE IF NOT EXISTS bids (
|
|
396
|
+
id TEXT PRIMARY KEY,
|
|
397
|
+
task_id TEXT NOT NULL,
|
|
398
|
+
bidder TEXT NOT NULL,
|
|
399
|
+
bid_price TEXT NOT NULL,
|
|
400
|
+
reputation_score TEXT NOT NULL,
|
|
401
|
+
evidence_blob TEXT,
|
|
402
|
+
created_at INTEGER NOT NULL,
|
|
403
|
+
accepted_at INTEGER,
|
|
404
|
+
rejected_at INTEGER,
|
|
405
|
+
withdrawn_at INTEGER,
|
|
406
|
+
status INTEGER NOT NULL
|
|
407
|
+
);
|
|
408
|
+
CREATE INDEX IF NOT EXISTS bids_task_idx ON bids (task_id, created_at DESC);
|
|
409
|
+
CREATE INDEX IF NOT EXISTS bids_status_idx ON bids (status);
|
|
410
|
+
|
|
411
|
+
CREATE TABLE IF NOT EXISTS disputes (
|
|
412
|
+
id TEXT PRIMARY KEY,
|
|
413
|
+
task_id TEXT NOT NULL,
|
|
414
|
+
requester TEXT NOT NULL,
|
|
415
|
+
provider TEXT NOT NULL,
|
|
416
|
+
escrow_amount TEXT NOT NULL,
|
|
417
|
+
status INTEGER NOT NULL,
|
|
418
|
+
requester_evidence_blob TEXT NOT NULL,
|
|
419
|
+
provider_evidence_blob TEXT,
|
|
420
|
+
requester_proposed_split TEXT NOT NULL,
|
|
421
|
+
provider_proposed_split TEXT NOT NULL,
|
|
422
|
+
arbitrator TEXT,
|
|
423
|
+
ruling_split TEXT NOT NULL,
|
|
424
|
+
opened_at INTEGER NOT NULL,
|
|
425
|
+
responded_at INTEGER,
|
|
426
|
+
resolved_at INTEGER,
|
|
427
|
+
resolution_deadline INTEGER NOT NULL
|
|
428
|
+
);
|
|
429
|
+
CREATE INDEX IF NOT EXISTS disputes_task_idx ON disputes (task_id);
|
|
430
|
+
CREATE INDEX IF NOT EXISTS disputes_status_idx ON disputes (status);
|
|
431
|
+
|
|
432
|
+
CREATE TABLE IF NOT EXISTS stakes (
|
|
433
|
+
id TEXT PRIMARY KEY,
|
|
434
|
+
owner TEXT NOT NULL,
|
|
435
|
+
amount_mist TEXT NOT NULL DEFAULT '0',
|
|
436
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
437
|
+
stake_type TEXT,
|
|
438
|
+
staked_at INTEGER,
|
|
439
|
+
deactivated_at INTEGER,
|
|
440
|
+
withdrawn_at INTEGER,
|
|
441
|
+
slashed_amount_mist TEXT NOT NULL DEFAULT '0',
|
|
442
|
+
last_updated_at INTEGER NOT NULL
|
|
443
|
+
);
|
|
444
|
+
CREATE INDEX IF NOT EXISTS stakes_owner_idx ON stakes (owner);
|
|
445
|
+
|
|
446
|
+
CREATE TABLE IF NOT EXISTS reputation_anchors (
|
|
447
|
+
anchor_id TEXT PRIMARY KEY,
|
|
448
|
+
author TEXT NOT NULL,
|
|
449
|
+
merkle_root TEXT NOT NULL,
|
|
450
|
+
event_count INTEGER NOT NULL,
|
|
451
|
+
blob_id TEXT,
|
|
452
|
+
from_timestamp INTEGER,
|
|
453
|
+
to_timestamp INTEGER,
|
|
454
|
+
created_at INTEGER NOT NULL,
|
|
455
|
+
tx_digest TEXT NOT NULL
|
|
456
|
+
);
|
|
457
|
+
CREATE INDEX IF NOT EXISTS reputation_anchors_author_idx ON reputation_anchors (author, created_at DESC);
|
|
458
|
+
`);
|
|
459
|
+
ensureTaskColumns(this.db);
|
|
460
|
+
}
|
|
461
|
+
close() {
|
|
462
|
+
this.db.close();
|
|
463
|
+
}
|
|
464
|
+
getCursor(streamKey) {
|
|
465
|
+
const row = this.db.prepare("SELECT cursor_json FROM indexer_cursors WHERE stream_key = ?").get(streamKey);
|
|
466
|
+
if (!row) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
return JSON.parse(row.cursor_json);
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
setCursor(streamKey, cursor) {
|
|
476
|
+
this.db.prepare(
|
|
477
|
+
`INSERT INTO indexer_cursors (stream_key, cursor_json, updated_at)
|
|
478
|
+
VALUES (?, ?, ?)
|
|
479
|
+
ON CONFLICT(stream_key)
|
|
480
|
+
DO UPDATE SET cursor_json = excluded.cursor_json, updated_at = excluded.updated_at`
|
|
481
|
+
).run(streamKey, JSON.stringify(cursor), Date.now());
|
|
482
|
+
}
|
|
483
|
+
recordEvent(event) {
|
|
484
|
+
const result = this.db.prepare(
|
|
485
|
+
`INSERT OR IGNORE INTO events (
|
|
486
|
+
event_id, event_type, package_id, tx_digest, module_name, checkpoint, timestamp_ms, payload_json, indexed_at
|
|
487
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
488
|
+
).run(
|
|
489
|
+
event.eventId,
|
|
490
|
+
event.eventType,
|
|
491
|
+
event.packageId,
|
|
492
|
+
event.txDigest,
|
|
493
|
+
event.module ?? null,
|
|
494
|
+
event.checkpoint ?? null,
|
|
495
|
+
event.timestampMs,
|
|
496
|
+
JSON.stringify(event.payload, bigintReplacer),
|
|
497
|
+
Date.now()
|
|
498
|
+
);
|
|
499
|
+
return result.changes > 0;
|
|
500
|
+
}
|
|
501
|
+
upsertAgent(agent) {
|
|
502
|
+
const capabilitiesJson = JSON.stringify(agent.capabilities, bigintReplacer);
|
|
503
|
+
const capabilitiesText = buildCapabilitiesText(agent.capabilities);
|
|
504
|
+
this.db.prepare(
|
|
505
|
+
`INSERT INTO agents (
|
|
506
|
+
id, owner, did, name, description, capabilities_json, capabilities_text, endpoint,
|
|
507
|
+
encryption_public_key, active, version, registered_at, updated_at,
|
|
508
|
+
total_tasks_completed, total_tasks_failed, total_tasks_disputed, total_earnings_mist,
|
|
509
|
+
has_stake, stake_mist, stake_type
|
|
510
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
512
|
+
owner = excluded.owner,
|
|
513
|
+
did = excluded.did,
|
|
514
|
+
name = excluded.name,
|
|
515
|
+
description = excluded.description,
|
|
516
|
+
capabilities_json = excluded.capabilities_json,
|
|
517
|
+
capabilities_text = excluded.capabilities_text,
|
|
518
|
+
endpoint = excluded.endpoint,
|
|
519
|
+
encryption_public_key = excluded.encryption_public_key,
|
|
520
|
+
active = excluded.active,
|
|
521
|
+
version = excluded.version,
|
|
522
|
+
registered_at = excluded.registered_at,
|
|
523
|
+
updated_at = excluded.updated_at,
|
|
524
|
+
total_tasks_completed = COALESCE(excluded.total_tasks_completed, agents.total_tasks_completed),
|
|
525
|
+
total_tasks_failed = COALESCE(excluded.total_tasks_failed, agents.total_tasks_failed),
|
|
526
|
+
total_tasks_disputed = COALESCE(excluded.total_tasks_disputed, agents.total_tasks_disputed),
|
|
527
|
+
total_earnings_mist = COALESCE(excluded.total_earnings_mist, agents.total_earnings_mist),
|
|
528
|
+
has_stake = COALESCE(excluded.has_stake, agents.has_stake),
|
|
529
|
+
stake_mist = COALESCE(excluded.stake_mist, agents.stake_mist),
|
|
530
|
+
stake_type = COALESCE(excluded.stake_type, agents.stake_type)`
|
|
531
|
+
).run(
|
|
532
|
+
agent.id,
|
|
533
|
+
agent.owner,
|
|
534
|
+
agent.did,
|
|
535
|
+
agent.name,
|
|
536
|
+
agent.description,
|
|
537
|
+
capabilitiesJson,
|
|
538
|
+
capabilitiesText,
|
|
539
|
+
agent.endpoint ?? null,
|
|
540
|
+
agent.encryptionPublicKey ?? null,
|
|
541
|
+
agent.active ? 1 : 0,
|
|
542
|
+
agent.version,
|
|
543
|
+
agent.registeredAt,
|
|
544
|
+
agent.updatedAt,
|
|
545
|
+
agent.totalTasksCompleted ?? null,
|
|
546
|
+
agent.totalTasksFailed ?? null,
|
|
547
|
+
agent.totalTasksDisputed ?? null,
|
|
548
|
+
agent.totalEarningsMist?.toString() ?? null,
|
|
549
|
+
agent.hasStake == null ? null : agent.hasStake ? 1 : 0,
|
|
550
|
+
agent.stakeMist?.toString() ?? null,
|
|
551
|
+
agent.stakeType ?? null
|
|
552
|
+
);
|
|
553
|
+
this.db.prepare("DELETE FROM agents_fts WHERE agent_id = ?").run(agent.id);
|
|
554
|
+
this.db.prepare("INSERT INTO agents_fts (agent_id, name, description, capabilities_text) VALUES (?, ?, ?, ?)").run(agent.id, agent.name, agent.description, capabilitiesText);
|
|
555
|
+
}
|
|
556
|
+
markAgentInactive(agentId) {
|
|
557
|
+
this.db.prepare("UPDATE agents SET active = 0 WHERE id = ?").run(agentId);
|
|
558
|
+
}
|
|
559
|
+
getAgentByDid(did) {
|
|
560
|
+
const row = this.db.prepare("SELECT rowid, * FROM agents WHERE did = ?").get(did);
|
|
561
|
+
return row ? mapAgentRow(row) : null;
|
|
562
|
+
}
|
|
563
|
+
queryAgents(filters = {}) {
|
|
564
|
+
const limit = normalizeLimit(filters.limit, 20);
|
|
565
|
+
const offset = normalizeOffset(filters.offset);
|
|
566
|
+
const clauses = [];
|
|
567
|
+
const values = [];
|
|
568
|
+
if (filters.activeOnly !== false) {
|
|
569
|
+
clauses.push("a.active = 1");
|
|
570
|
+
}
|
|
571
|
+
if (filters.category) {
|
|
572
|
+
clauses.push("EXISTS (SELECT 1 FROM tasks t WHERE t.provider = a.owner AND t.category = ?)");
|
|
573
|
+
values.push(filters.category);
|
|
574
|
+
}
|
|
575
|
+
const search = filters.search?.trim();
|
|
576
|
+
const capability = filters.capability?.trim();
|
|
577
|
+
const searchQuery = search ?? capability;
|
|
578
|
+
let rows = [];
|
|
579
|
+
if (searchQuery) {
|
|
580
|
+
const ftsQuery = buildFtsQuery(searchQuery);
|
|
581
|
+
const where = clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "";
|
|
582
|
+
try {
|
|
583
|
+
rows = this.db.prepare(
|
|
584
|
+
`SELECT a.rowid, a.*
|
|
585
|
+
FROM agents_fts
|
|
586
|
+
JOIN agents a ON a.id = agents_fts.agent_id
|
|
587
|
+
WHERE agents_fts MATCH ? ${where}
|
|
588
|
+
LIMIT ? OFFSET ?`
|
|
589
|
+
).all(ftsQuery, ...values, Math.max(limit * 4, 50), offset);
|
|
590
|
+
} catch {
|
|
591
|
+
const like = `%${searchQuery}%`;
|
|
592
|
+
const whereClause = clauses.length > 0 ? `AND ${clauses.join(" AND ")}` : "";
|
|
593
|
+
rows = this.db.prepare(
|
|
594
|
+
`SELECT rowid, * FROM agents a
|
|
595
|
+
WHERE (a.name LIKE ? OR a.description LIKE ? OR a.capabilities_text LIKE ? OR a.capabilities_json LIKE ?)
|
|
596
|
+
${whereClause}
|
|
597
|
+
LIMIT ? OFFSET ?`
|
|
598
|
+
).all(like, like, like, like, ...values, Math.max(limit * 4, 50), offset);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
const whereClause = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
602
|
+
rows = this.db.prepare(`SELECT rowid, * FROM agents a ${whereClause} ORDER BY updated_at DESC LIMIT ? OFFSET ?`).all(Math.max(limit * 4, 50), offset);
|
|
603
|
+
}
|
|
604
|
+
let agents = rows.map(mapAgentRow);
|
|
605
|
+
if (capability) {
|
|
606
|
+
agents = agents.filter((agent) => agent.capabilities.some((entry) => equalsIgnoreCase(entry.name, capability)));
|
|
607
|
+
}
|
|
608
|
+
if (search) {
|
|
609
|
+
const searchLower = search.toLowerCase();
|
|
610
|
+
agents = agents.filter((agent) => matchesSearch(agent, searchLower));
|
|
611
|
+
}
|
|
612
|
+
const scores = new Map(agents.map((agent) => [agent.did, this.scoreCalculator.computeScore(agent, [])]));
|
|
613
|
+
const minReputation = typeof filters.minReputation === "number" ? filters.minReputation : void 0;
|
|
614
|
+
if (minReputation !== void 0) {
|
|
615
|
+
agents = agents.filter((agent) => (scores.get(agent.did)?.successRate ?? 0) >= minReputation);
|
|
616
|
+
}
|
|
617
|
+
const ranked = filters.sortBy === "reputation" ? this.scoreCalculator.rankByReputation(agents, scores) : [...agents].sort(compareStakePreference);
|
|
618
|
+
return ranked.slice(0, limit);
|
|
619
|
+
}
|
|
620
|
+
countAgents(filters = {}) {
|
|
621
|
+
return this.queryAgents({ ...filters, limit: 1e4, offset: 0 }).length;
|
|
622
|
+
}
|
|
623
|
+
upsertTask(task, txDigest, gasCostMist = 0n) {
|
|
624
|
+
this.db.prepare(
|
|
625
|
+
`INSERT INTO tasks (
|
|
626
|
+
id, requester, provider, capability, category, input_blob_id, result_blob_id, price,
|
|
627
|
+
payment_scheme, max_price, metered_units, unit_price, verification_hash,
|
|
628
|
+
status, dispute_window_ms, created_at, accepted_at, completed_at, released_at, disputed_at, cancelled_at,
|
|
629
|
+
expires_at, agreement_hash, posted_tx_digest, gas_cost_mist_total, bid_count
|
|
630
|
+
) VALUES (
|
|
631
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
632
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
633
|
+
COALESCE((SELECT bid_count FROM tasks WHERE id = ?), 0)
|
|
634
|
+
)
|
|
635
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
636
|
+
requester = excluded.requester,
|
|
637
|
+
provider = COALESCE(excluded.provider, tasks.provider),
|
|
638
|
+
capability = excluded.capability,
|
|
639
|
+
category = excluded.category,
|
|
640
|
+
input_blob_id = excluded.input_blob_id,
|
|
641
|
+
result_blob_id = COALESCE(excluded.result_blob_id, tasks.result_blob_id),
|
|
642
|
+
price = excluded.price,
|
|
643
|
+
payment_scheme = COALESCE(excluded.payment_scheme, tasks.payment_scheme),
|
|
644
|
+
max_price = COALESCE(excluded.max_price, tasks.max_price),
|
|
645
|
+
metered_units = COALESCE(excluded.metered_units, tasks.metered_units),
|
|
646
|
+
unit_price = COALESCE(excluded.unit_price, tasks.unit_price),
|
|
647
|
+
verification_hash = COALESCE(excluded.verification_hash, tasks.verification_hash),
|
|
648
|
+
status = excluded.status,
|
|
649
|
+
dispute_window_ms = excluded.dispute_window_ms,
|
|
650
|
+
created_at = excluded.created_at,
|
|
651
|
+
accepted_at = COALESCE(excluded.accepted_at, tasks.accepted_at),
|
|
652
|
+
completed_at = COALESCE(excluded.completed_at, tasks.completed_at),
|
|
653
|
+
expires_at = excluded.expires_at,
|
|
654
|
+
agreement_hash = COALESCE(excluded.agreement_hash, tasks.agreement_hash),
|
|
655
|
+
posted_tx_digest = COALESCE(excluded.posted_tx_digest, tasks.posted_tx_digest),
|
|
656
|
+
gas_cost_mist_total = add_bigint(tasks.gas_cost_mist_total, excluded.gas_cost_mist_total)`
|
|
657
|
+
).run(
|
|
658
|
+
task.id,
|
|
659
|
+
task.requester,
|
|
660
|
+
task.provider ?? null,
|
|
661
|
+
task.capability,
|
|
662
|
+
task.category,
|
|
663
|
+
task.inputBlobId,
|
|
664
|
+
task.resultBlobId ?? null,
|
|
665
|
+
task.price.toString(),
|
|
666
|
+
task.paymentScheme ?? null,
|
|
667
|
+
task.maxPrice?.toString() ?? null,
|
|
668
|
+
task.meteredUnits ?? null,
|
|
669
|
+
task.unitPrice?.toString() ?? null,
|
|
670
|
+
task.verificationHash ?? null,
|
|
671
|
+
task.status,
|
|
672
|
+
task.disputeWindowMs,
|
|
673
|
+
task.createdAt,
|
|
674
|
+
task.acceptedAt ?? null,
|
|
675
|
+
task.completedAt ?? null,
|
|
676
|
+
null,
|
|
677
|
+
null,
|
|
678
|
+
null,
|
|
679
|
+
task.expiresAt,
|
|
680
|
+
task.agreementHash ?? null,
|
|
681
|
+
txDigest,
|
|
682
|
+
gasCostMist.toString(),
|
|
683
|
+
task.id
|
|
684
|
+
);
|
|
685
|
+
this.recordTaskTransition(task.id, "task.posted", TaskStatus.OPEN, txDigest, task.createdAt, task);
|
|
686
|
+
}
|
|
687
|
+
updateTaskStatus(params) {
|
|
688
|
+
const gasCost = params.gasCostMist?.toString() ?? "0";
|
|
689
|
+
const columnUpdates = ["status = ?", "provider = COALESCE(?, provider)", "gas_cost_mist_total = add_bigint(gas_cost_mist_total, ?)"];
|
|
690
|
+
const values = [params.status, params.provider ?? null, gasCost];
|
|
691
|
+
if (params.requester !== void 0) {
|
|
692
|
+
columnUpdates.push("requester = COALESCE(?, requester)");
|
|
693
|
+
values.push(params.requester || null);
|
|
694
|
+
}
|
|
695
|
+
if (params.resultBlobId !== void 0) {
|
|
696
|
+
columnUpdates.push("result_blob_id = COALESCE(?, result_blob_id)");
|
|
697
|
+
values.push(params.resultBlobId || null);
|
|
698
|
+
}
|
|
699
|
+
if (params.price !== void 0) {
|
|
700
|
+
columnUpdates.push("price = COALESCE(?, price)");
|
|
701
|
+
values.push(params.price?.toString() ?? null);
|
|
702
|
+
}
|
|
703
|
+
if (params.paymentScheme !== void 0) {
|
|
704
|
+
columnUpdates.push("payment_scheme = COALESCE(?, payment_scheme)");
|
|
705
|
+
values.push(params.paymentScheme ?? null);
|
|
706
|
+
}
|
|
707
|
+
if (params.meteredUnits !== void 0) {
|
|
708
|
+
columnUpdates.push("metered_units = COALESCE(?, metered_units)");
|
|
709
|
+
values.push(params.meteredUnits);
|
|
710
|
+
}
|
|
711
|
+
if (params.maxPrice !== void 0) {
|
|
712
|
+
columnUpdates.push("max_price = COALESCE(?, max_price)");
|
|
713
|
+
values.push(params.maxPrice?.toString() ?? null);
|
|
714
|
+
}
|
|
715
|
+
if (params.unitPrice !== void 0) {
|
|
716
|
+
columnUpdates.push("unit_price = COALESCE(?, unit_price)");
|
|
717
|
+
values.push(params.unitPrice?.toString() ?? null);
|
|
718
|
+
}
|
|
719
|
+
if (params.verificationHash !== void 0) {
|
|
720
|
+
columnUpdates.push("verification_hash = COALESCE(?, verification_hash)");
|
|
721
|
+
values.push(params.verificationHash || null);
|
|
722
|
+
}
|
|
723
|
+
switch (params.status) {
|
|
724
|
+
case TaskStatus.ACCEPTED:
|
|
725
|
+
columnUpdates.push("accepted_at = ?", "accepted_tx_digest = ?");
|
|
726
|
+
values.push(params.timestampMs, params.txDigest);
|
|
727
|
+
break;
|
|
728
|
+
case TaskStatus.COMPLETED:
|
|
729
|
+
columnUpdates.push("completed_at = ?", "completed_tx_digest = ?");
|
|
730
|
+
values.push(params.timestampMs, params.txDigest);
|
|
731
|
+
break;
|
|
732
|
+
case TaskStatus.RELEASED:
|
|
733
|
+
columnUpdates.push("released_at = ?", "released_tx_digest = ?");
|
|
734
|
+
values.push(params.timestampMs, params.txDigest);
|
|
735
|
+
break;
|
|
736
|
+
case TaskStatus.DISPUTED:
|
|
737
|
+
columnUpdates.push("disputed_at = ?", "disputed_tx_digest = ?");
|
|
738
|
+
values.push(params.timestampMs, params.txDigest);
|
|
739
|
+
break;
|
|
740
|
+
case TaskStatus.CANCELLED:
|
|
741
|
+
columnUpdates.push("cancelled_at = ?", "cancelled_tx_digest = ?");
|
|
742
|
+
values.push(params.timestampMs, params.txDigest);
|
|
743
|
+
break;
|
|
744
|
+
default:
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
this.db.prepare(`UPDATE tasks SET ${columnUpdates.join(", ")} WHERE id = ?`).run(...values, params.taskId);
|
|
748
|
+
this.recordTaskTransition(params.taskId, params.eventType, params.status, params.txDigest, params.timestampMs, params.payload);
|
|
749
|
+
}
|
|
750
|
+
getTask(taskId, includeTransitions = true) {
|
|
751
|
+
const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
752
|
+
if (!row) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
return mapTaskRow(row, includeTransitions ? this.getTaskTransitions(taskId) : void 0);
|
|
756
|
+
}
|
|
757
|
+
queryTasks(filters = {}) {
|
|
758
|
+
const limit = normalizeLimit(filters.limit, 20);
|
|
759
|
+
const clauses = [];
|
|
760
|
+
const values = [];
|
|
761
|
+
if (filters.status !== void 0) {
|
|
762
|
+
clauses.push("status = ?");
|
|
763
|
+
values.push(filters.status);
|
|
764
|
+
}
|
|
765
|
+
if (filters.requester) {
|
|
766
|
+
clauses.push("requester = ?");
|
|
767
|
+
values.push(filters.requester);
|
|
768
|
+
}
|
|
769
|
+
if (filters.provider) {
|
|
770
|
+
clauses.push("provider = ?");
|
|
771
|
+
values.push(filters.provider);
|
|
772
|
+
}
|
|
773
|
+
if (filters.category) {
|
|
774
|
+
clauses.push("category = ?");
|
|
775
|
+
values.push(filters.category);
|
|
776
|
+
}
|
|
777
|
+
const cursor = decodeCursor(filters.after);
|
|
778
|
+
if (cursor) {
|
|
779
|
+
clauses.push("(created_at < ? OR (created_at = ? AND id < ?))");
|
|
780
|
+
values.push(cursor.createdAt, cursor.createdAt, cursor.id);
|
|
781
|
+
}
|
|
782
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
783
|
+
const rows = this.db.prepare(`SELECT * FROM tasks ${where} ORDER BY created_at DESC, id DESC LIMIT ?`).all(...values, limit);
|
|
784
|
+
return rows.map((row) => mapTaskRow(row));
|
|
785
|
+
}
|
|
786
|
+
getTaskTransitions(taskId) {
|
|
787
|
+
const rows = this.db.prepare(
|
|
788
|
+
"SELECT task_id, event_type, status, tx_digest, timestamp_ms, payload_json FROM task_transitions WHERE task_id = ? ORDER BY timestamp_ms ASC, id ASC"
|
|
789
|
+
).all(taskId);
|
|
790
|
+
return rows.map((row) => ({
|
|
791
|
+
taskId: row.task_id,
|
|
792
|
+
eventType: row.event_type,
|
|
793
|
+
status: Number(row.status),
|
|
794
|
+
txDigest: row.tx_digest,
|
|
795
|
+
timestampMs: Number(row.timestamp_ms),
|
|
796
|
+
payload: JSON.parse(row.payload_json)
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
upsertBid(bid) {
|
|
800
|
+
const existing = this.db.prepare("SELECT 1 FROM bids WHERE id = ?").get(bid.id);
|
|
801
|
+
this.db.prepare(
|
|
802
|
+
`INSERT INTO bids (
|
|
803
|
+
id, task_id, bidder, bid_price, reputation_score, evidence_blob, created_at, accepted_at, rejected_at, withdrawn_at, status
|
|
804
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
805
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
806
|
+
task_id = excluded.task_id,
|
|
807
|
+
bidder = excluded.bidder,
|
|
808
|
+
bid_price = excluded.bid_price,
|
|
809
|
+
reputation_score = excluded.reputation_score,
|
|
810
|
+
evidence_blob = excluded.evidence_blob,
|
|
811
|
+
created_at = excluded.created_at,
|
|
812
|
+
status = excluded.status`
|
|
813
|
+
).run(
|
|
814
|
+
bid.id,
|
|
815
|
+
bid.taskId,
|
|
816
|
+
bid.bidder,
|
|
817
|
+
bid.bidPrice.toString(),
|
|
818
|
+
bid.reputationScore.toString(),
|
|
819
|
+
bid.evidenceBlob ?? null,
|
|
820
|
+
bid.createdAt,
|
|
821
|
+
null,
|
|
822
|
+
null,
|
|
823
|
+
null,
|
|
824
|
+
bid.status
|
|
825
|
+
);
|
|
826
|
+
if (!existing) {
|
|
827
|
+
this.db.prepare("UPDATE tasks SET bid_count = bid_count + 1 WHERE id = ?").run(bid.taskId);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
updateBidStatus(bidId, status, timestampMs) {
|
|
831
|
+
const columns = ["status = ?"];
|
|
832
|
+
const values = [status];
|
|
833
|
+
if (status === BidStatus2.ACCEPTED) {
|
|
834
|
+
columns.push("accepted_at = ?");
|
|
835
|
+
values.push(timestampMs);
|
|
836
|
+
} else if (status === BidStatus2.REJECTED) {
|
|
837
|
+
columns.push("rejected_at = ?");
|
|
838
|
+
values.push(timestampMs);
|
|
839
|
+
} else if (status === BidStatus2.WITHDRAWN) {
|
|
840
|
+
columns.push("withdrawn_at = ?");
|
|
841
|
+
values.push(timestampMs);
|
|
842
|
+
}
|
|
843
|
+
this.db.prepare(`UPDATE bids SET ${columns.join(", ")} WHERE id = ?`).run(...values, bidId);
|
|
844
|
+
}
|
|
845
|
+
getBids(taskId, status) {
|
|
846
|
+
const rows = status === void 0 ? this.db.prepare("SELECT * FROM bids WHERE task_id = ? ORDER BY created_at DESC").all(taskId) : this.db.prepare("SELECT * FROM bids WHERE task_id = ? AND status = ? ORDER BY created_at DESC").all(taskId, status);
|
|
847
|
+
return rows.map(mapBidRow);
|
|
848
|
+
}
|
|
849
|
+
upsertDispute(dispute) {
|
|
850
|
+
this.db.prepare(
|
|
851
|
+
`INSERT INTO disputes (
|
|
852
|
+
id, task_id, requester, provider, escrow_amount, status, requester_evidence_blob,
|
|
853
|
+
provider_evidence_blob, requester_proposed_split, provider_proposed_split, arbitrator,
|
|
854
|
+
ruling_split, opened_at, responded_at, resolved_at, resolution_deadline
|
|
855
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
856
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
857
|
+
task_id = excluded.task_id,
|
|
858
|
+
requester = excluded.requester,
|
|
859
|
+
provider = excluded.provider,
|
|
860
|
+
escrow_amount = excluded.escrow_amount,
|
|
861
|
+
status = excluded.status,
|
|
862
|
+
requester_evidence_blob = excluded.requester_evidence_blob,
|
|
863
|
+
provider_evidence_blob = COALESCE(excluded.provider_evidence_blob, disputes.provider_evidence_blob),
|
|
864
|
+
requester_proposed_split = excluded.requester_proposed_split,
|
|
865
|
+
provider_proposed_split = COALESCE(excluded.provider_proposed_split, disputes.provider_proposed_split),
|
|
866
|
+
arbitrator = COALESCE(excluded.arbitrator, disputes.arbitrator),
|
|
867
|
+
ruling_split = COALESCE(excluded.ruling_split, disputes.ruling_split),
|
|
868
|
+
opened_at = excluded.opened_at,
|
|
869
|
+
responded_at = COALESCE(excluded.responded_at, disputes.responded_at),
|
|
870
|
+
resolved_at = COALESCE(excluded.resolved_at, disputes.resolved_at),
|
|
871
|
+
resolution_deadline = excluded.resolution_deadline`
|
|
872
|
+
).run(
|
|
873
|
+
dispute.id,
|
|
874
|
+
dispute.taskId,
|
|
875
|
+
dispute.requester,
|
|
876
|
+
dispute.provider,
|
|
877
|
+
dispute.escrowAmount.toString(),
|
|
878
|
+
dispute.status,
|
|
879
|
+
dispute.requesterEvidenceBlob,
|
|
880
|
+
dispute.providerEvidenceBlob ?? null,
|
|
881
|
+
dispute.requesterProposedSplit.toString(),
|
|
882
|
+
dispute.providerProposedSplit.toString(),
|
|
883
|
+
dispute.arbitrator ?? null,
|
|
884
|
+
dispute.rulingSplit.toString(),
|
|
885
|
+
dispute.openedAt,
|
|
886
|
+
dispute.respondedAt ?? null,
|
|
887
|
+
dispute.resolvedAt ?? null,
|
|
888
|
+
dispute.resolutionDeadline
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
updateDispute(params) {
|
|
892
|
+
const clauses = ["status = ?"];
|
|
893
|
+
const values = [params.status];
|
|
894
|
+
if (params.respondedAt !== void 0) {
|
|
895
|
+
clauses.push("responded_at = ?");
|
|
896
|
+
values.push(params.respondedAt);
|
|
897
|
+
}
|
|
898
|
+
if (params.resolvedAt !== void 0) {
|
|
899
|
+
clauses.push("resolved_at = ?");
|
|
900
|
+
values.push(params.resolvedAt);
|
|
901
|
+
}
|
|
902
|
+
if (params.providerEvidenceBlob !== void 0) {
|
|
903
|
+
clauses.push("provider_evidence_blob = ?");
|
|
904
|
+
values.push(params.providerEvidenceBlob);
|
|
905
|
+
}
|
|
906
|
+
if (params.providerProposedSplit !== void 0) {
|
|
907
|
+
clauses.push("provider_proposed_split = ?");
|
|
908
|
+
values.push(params.providerProposedSplit.toString());
|
|
909
|
+
}
|
|
910
|
+
if (params.arbitrator !== void 0) {
|
|
911
|
+
clauses.push("arbitrator = ?");
|
|
912
|
+
values.push(params.arbitrator);
|
|
913
|
+
}
|
|
914
|
+
if (params.rulingSplit !== void 0) {
|
|
915
|
+
clauses.push("ruling_split = ?");
|
|
916
|
+
values.push(params.rulingSplit.toString());
|
|
917
|
+
}
|
|
918
|
+
this.db.prepare(`UPDATE disputes SET ${clauses.join(", ")} WHERE id = ?`).run(...values, params.disputeId);
|
|
919
|
+
}
|
|
920
|
+
getDisputes(filters = {}) {
|
|
921
|
+
const clauses = [];
|
|
922
|
+
const values = [];
|
|
923
|
+
if (filters.status !== void 0) {
|
|
924
|
+
clauses.push("status = ?");
|
|
925
|
+
values.push(filters.status);
|
|
926
|
+
}
|
|
927
|
+
if (filters.agent) {
|
|
928
|
+
clauses.push("(requester = ? OR provider = ? OR arbitrator = ?)");
|
|
929
|
+
values.push(filters.agent, filters.agent, filters.agent);
|
|
930
|
+
}
|
|
931
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
932
|
+
const rows = this.db.prepare(`SELECT * FROM disputes ${where} ORDER BY opened_at DESC`).all(...values);
|
|
933
|
+
return rows.map(mapDisputeRow);
|
|
934
|
+
}
|
|
935
|
+
upsertStake(params) {
|
|
936
|
+
this.db.prepare(
|
|
937
|
+
`INSERT INTO stakes (
|
|
938
|
+
id, owner, amount_mist, active, stake_type, staked_at, deactivated_at, withdrawn_at, slashed_amount_mist, last_updated_at
|
|
939
|
+
) VALUES (?, ?, COALESCE(?, (SELECT amount_mist FROM stakes WHERE id = ?), '0'), ?, ?, ?, ?, ?, COALESCE(?, (SELECT slashed_amount_mist FROM stakes WHERE id = ?), '0'), ?)
|
|
940
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
941
|
+
owner = excluded.owner,
|
|
942
|
+
amount_mist = COALESCE(excluded.amount_mist, stakes.amount_mist),
|
|
943
|
+
active = excluded.active,
|
|
944
|
+
stake_type = COALESCE(excluded.stake_type, stakes.stake_type),
|
|
945
|
+
staked_at = COALESCE(excluded.staked_at, stakes.staked_at),
|
|
946
|
+
deactivated_at = COALESCE(excluded.deactivated_at, stakes.deactivated_at),
|
|
947
|
+
withdrawn_at = COALESCE(excluded.withdrawn_at, stakes.withdrawn_at),
|
|
948
|
+
slashed_amount_mist = COALESCE(excluded.slashed_amount_mist, stakes.slashed_amount_mist),
|
|
949
|
+
last_updated_at = excluded.last_updated_at`
|
|
950
|
+
).run(
|
|
951
|
+
params.stakeId,
|
|
952
|
+
params.owner,
|
|
953
|
+
params.amountMist?.toString() ?? null,
|
|
954
|
+
params.stakeId,
|
|
955
|
+
params.active === false ? 0 : 1,
|
|
956
|
+
params.stakeType ?? null,
|
|
957
|
+
params.stakedAt ?? null,
|
|
958
|
+
params.deactivatedAt ?? null,
|
|
959
|
+
params.withdrawnAt ?? null,
|
|
960
|
+
params.slashedAmountMist?.toString() ?? null,
|
|
961
|
+
params.stakeId,
|
|
962
|
+
Date.now()
|
|
963
|
+
);
|
|
964
|
+
this.syncAgentStake(params.owner);
|
|
965
|
+
}
|
|
966
|
+
addStakeSlash(stakeId, amountMist) {
|
|
967
|
+
this.db.prepare(
|
|
968
|
+
`UPDATE stakes
|
|
969
|
+
SET slashed_amount_mist = add_bigint(slashed_amount_mist, ?),
|
|
970
|
+
amount_mist = subtract_bigint(amount_mist, ?),
|
|
971
|
+
last_updated_at = ?
|
|
972
|
+
WHERE id = ?`
|
|
973
|
+
).run(amountMist.toString(), amountMist.toString(), Date.now(), stakeId);
|
|
974
|
+
const owner = this.db.prepare("SELECT owner FROM stakes WHERE id = ?").get(stakeId);
|
|
975
|
+
if (owner) {
|
|
976
|
+
this.syncAgentStake(owner.owner);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
upsertReputationAnchor(params) {
|
|
980
|
+
this.db.prepare(
|
|
981
|
+
`INSERT OR REPLACE INTO reputation_anchors (
|
|
982
|
+
anchor_id, author, merkle_root, event_count, blob_id, from_timestamp, to_timestamp, created_at, tx_digest
|
|
983
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
984
|
+
).run(
|
|
985
|
+
params.anchorId,
|
|
986
|
+
params.author,
|
|
987
|
+
params.merkleRoot,
|
|
988
|
+
params.eventCount,
|
|
989
|
+
params.blobId ?? null,
|
|
990
|
+
params.fromTimestamp ?? null,
|
|
991
|
+
params.toTimestamp ?? null,
|
|
992
|
+
params.createdAt,
|
|
993
|
+
params.txDigest
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
getDatabase() {
|
|
997
|
+
return this.db;
|
|
998
|
+
}
|
|
999
|
+
recordTaskTransition(taskId, eventType, status, txDigest, timestampMs, payload) {
|
|
1000
|
+
const exists = this.db.prepare("SELECT 1 FROM task_transitions WHERE task_id = ? AND event_type = ? AND tx_digest = ? LIMIT 1").get(taskId, eventType, txDigest);
|
|
1001
|
+
if (exists) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
this.db.prepare(
|
|
1005
|
+
"INSERT INTO task_transitions (task_id, event_type, status, tx_digest, timestamp_ms, payload_json) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1006
|
+
).run(taskId, eventType, status, txDigest, timestampMs, JSON.stringify(payload, bigintReplacer));
|
|
1007
|
+
}
|
|
1008
|
+
syncAgentStake(owner) {
|
|
1009
|
+
const stake = this.db.prepare("SELECT amount_mist, active, stake_type FROM stakes WHERE owner = ? ORDER BY last_updated_at DESC LIMIT 1").get(owner);
|
|
1010
|
+
if (!stake) {
|
|
1011
|
+
this.db.prepare("UPDATE agents SET has_stake = 0, stake_mist = NULL, stake_type = NULL WHERE owner = ?").run(owner);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
this.db.prepare("UPDATE agents SET has_stake = ?, stake_mist = ?, stake_type = ? WHERE owner = ?").run(Number(stake.active) === 1 ? 1 : 0, stake.amount_mist, stake.stake_type, owner);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function encodeCursor(task) {
|
|
1018
|
+
return Buffer.from(JSON.stringify({ id: task.id, createdAt: task.createdAt })).toString("base64url");
|
|
1019
|
+
}
|
|
1020
|
+
function decodeCursor(cursor) {
|
|
1021
|
+
if (!cursor) {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
1026
|
+
return typeof parsed.id === "string" && typeof parsed.createdAt === "number" ? { id: parsed.id, createdAt: parsed.createdAt } : null;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function mapAgentRow(row) {
|
|
1032
|
+
return {
|
|
1033
|
+
id: row.id,
|
|
1034
|
+
owner: row.owner,
|
|
1035
|
+
did: row.did,
|
|
1036
|
+
name: row.name,
|
|
1037
|
+
description: row.description ?? "",
|
|
1038
|
+
capabilities: parseCapabilities(row.capabilities_json),
|
|
1039
|
+
endpoint: row.endpoint ?? void 0,
|
|
1040
|
+
encryptionPublicKey: row.encryption_public_key ?? void 0,
|
|
1041
|
+
active: Number(row.active) === 1,
|
|
1042
|
+
version: Number(row.version),
|
|
1043
|
+
registeredAt: Number(row.registered_at ?? 0),
|
|
1044
|
+
updatedAt: Number(row.updated_at ?? 0),
|
|
1045
|
+
totalTasksCompleted: row.total_tasks_completed == null ? void 0 : Number(row.total_tasks_completed),
|
|
1046
|
+
totalTasksFailed: row.total_tasks_failed == null ? void 0 : Number(row.total_tasks_failed),
|
|
1047
|
+
totalTasksDisputed: row.total_tasks_disputed == null ? void 0 : Number(row.total_tasks_disputed),
|
|
1048
|
+
totalEarningsMist: row.total_earnings_mist == null ? void 0 : BigInt(row.total_earnings_mist),
|
|
1049
|
+
hasStake: row.has_stake == null ? void 0 : Number(row.has_stake) === 1,
|
|
1050
|
+
stakeMist: row.stake_mist == null ? void 0 : BigInt(row.stake_mist),
|
|
1051
|
+
stakeType: row.stake_type === "agent" || row.stake_type === "relay" ? row.stake_type : void 0
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function mapTaskRow(row, transitions) {
|
|
1055
|
+
return {
|
|
1056
|
+
id: row.id,
|
|
1057
|
+
requester: row.requester,
|
|
1058
|
+
provider: row.provider ?? void 0,
|
|
1059
|
+
capability: row.capability,
|
|
1060
|
+
category: row.category,
|
|
1061
|
+
inputBlobId: row.input_blob_id,
|
|
1062
|
+
resultBlobId: row.result_blob_id ?? void 0,
|
|
1063
|
+
price: BigInt(row.price),
|
|
1064
|
+
paymentScheme: normalizePaymentScheme(row.payment_scheme),
|
|
1065
|
+
maxPrice: row.max_price == null ? void 0 : BigInt(row.max_price),
|
|
1066
|
+
meteredUnits: row.metered_units == null ? void 0 : Number(row.metered_units),
|
|
1067
|
+
unitPrice: row.unit_price == null ? void 0 : BigInt(row.unit_price),
|
|
1068
|
+
verificationHash: row.verification_hash ?? void 0,
|
|
1069
|
+
status: Number(row.status),
|
|
1070
|
+
disputeWindowMs: Number(row.dispute_window_ms ?? 0),
|
|
1071
|
+
createdAt: Number(row.created_at),
|
|
1072
|
+
acceptedAt: row.accepted_at == null ? void 0 : Number(row.accepted_at),
|
|
1073
|
+
completedAt: row.completed_at == null ? void 0 : Number(row.completed_at),
|
|
1074
|
+
releasedAt: row.released_at == null ? void 0 : Number(row.released_at),
|
|
1075
|
+
disputedAt: row.disputed_at == null ? void 0 : Number(row.disputed_at),
|
|
1076
|
+
cancelledAt: row.cancelled_at == null ? void 0 : Number(row.cancelled_at),
|
|
1077
|
+
expiresAt: Number(row.expires_at),
|
|
1078
|
+
agreementHash: row.agreement_hash ?? void 0,
|
|
1079
|
+
bidCount: Number(row.bid_count ?? 0),
|
|
1080
|
+
gasCostMistTotal: BigInt(row.gas_cost_mist_total ?? "0"),
|
|
1081
|
+
transitions
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function mapBidRow(row) {
|
|
1085
|
+
return {
|
|
1086
|
+
id: row.id,
|
|
1087
|
+
taskId: row.task_id,
|
|
1088
|
+
bidder: row.bidder,
|
|
1089
|
+
bidPrice: BigInt(row.bid_price),
|
|
1090
|
+
reputationScore: BigInt(row.reputation_score),
|
|
1091
|
+
evidenceBlob: row.evidence_blob ?? void 0,
|
|
1092
|
+
createdAt: Number(row.created_at),
|
|
1093
|
+
status: Number(row.status)
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function mapDisputeRow(row) {
|
|
1097
|
+
return {
|
|
1098
|
+
id: row.id,
|
|
1099
|
+
taskId: row.task_id,
|
|
1100
|
+
requester: row.requester,
|
|
1101
|
+
provider: row.provider,
|
|
1102
|
+
escrowAmount: BigInt(row.escrow_amount),
|
|
1103
|
+
status: Number(row.status),
|
|
1104
|
+
requesterEvidenceBlob: row.requester_evidence_blob,
|
|
1105
|
+
providerEvidenceBlob: row.provider_evidence_blob ?? void 0,
|
|
1106
|
+
requesterProposedSplit: BigInt(row.requester_proposed_split),
|
|
1107
|
+
providerProposedSplit: BigInt(row.provider_proposed_split),
|
|
1108
|
+
arbitrator: row.arbitrator ?? void 0,
|
|
1109
|
+
rulingSplit: BigInt(row.ruling_split),
|
|
1110
|
+
openedAt: Number(row.opened_at),
|
|
1111
|
+
respondedAt: row.responded_at == null ? void 0 : Number(row.responded_at),
|
|
1112
|
+
resolvedAt: row.resolved_at == null ? void 0 : Number(row.resolved_at),
|
|
1113
|
+
resolutionDeadline: Number(row.resolution_deadline)
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function parseCapabilities(value) {
|
|
1117
|
+
if (!value) {
|
|
1118
|
+
return [];
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const parsed = JSON.parse(value);
|
|
1122
|
+
return parsed.map((entry) => ({
|
|
1123
|
+
...entry,
|
|
1124
|
+
pricing: {
|
|
1125
|
+
...entry.pricing,
|
|
1126
|
+
amount: BigInt(entry.pricing.amount)
|
|
1127
|
+
}
|
|
1128
|
+
}));
|
|
1129
|
+
} catch {
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function buildCapabilitiesText(capabilities) {
|
|
1134
|
+
return capabilities.map((entry) => `${entry.name} ${entry.description} ${entry.version}`).join(" ");
|
|
1135
|
+
}
|
|
1136
|
+
function buildFtsQuery(query) {
|
|
1137
|
+
return query.trim().split(/\s+/).map((term) => term.replace(/[^\p{L}\p{N}_-]/gu, "")).filter(Boolean).map((term) => `"${term}"*`).join(" OR ");
|
|
1138
|
+
}
|
|
1139
|
+
function normalizeLimit(limit, fallback) {
|
|
1140
|
+
if (typeof limit !== "number" || Number.isNaN(limit)) {
|
|
1141
|
+
return fallback;
|
|
1142
|
+
}
|
|
1143
|
+
return Math.max(1, Math.floor(limit));
|
|
1144
|
+
}
|
|
1145
|
+
function normalizeOffset(offset) {
|
|
1146
|
+
if (typeof offset !== "number" || Number.isNaN(offset)) {
|
|
1147
|
+
return 0;
|
|
1148
|
+
}
|
|
1149
|
+
return Math.max(0, Math.floor(offset));
|
|
1150
|
+
}
|
|
1151
|
+
function compareStakePreference(left, right) {
|
|
1152
|
+
return compareBoolean(left.hasStake ?? false, right.hasStake ?? false) || compareBigInt2(left.stakeMist ?? 0n, right.stakeMist ?? 0n) || compareNumber2(left.updatedAt, right.updatedAt);
|
|
1153
|
+
}
|
|
1154
|
+
function compareBoolean(left, right) {
|
|
1155
|
+
if (left === right) {
|
|
1156
|
+
return 0;
|
|
1157
|
+
}
|
|
1158
|
+
return left ? -1 : 1;
|
|
1159
|
+
}
|
|
1160
|
+
function compareBigInt2(left, right) {
|
|
1161
|
+
if (left === right) {
|
|
1162
|
+
return 0;
|
|
1163
|
+
}
|
|
1164
|
+
return left > right ? -1 : 1;
|
|
1165
|
+
}
|
|
1166
|
+
function compareNumber2(left, right) {
|
|
1167
|
+
if (left === right) {
|
|
1168
|
+
return 0;
|
|
1169
|
+
}
|
|
1170
|
+
return left > right ? -1 : 1;
|
|
1171
|
+
}
|
|
1172
|
+
function equalsIgnoreCase(left, right) {
|
|
1173
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
1174
|
+
}
|
|
1175
|
+
function matchesSearch(agent, searchLower) {
|
|
1176
|
+
return agent.name.toLowerCase().includes(searchLower) || agent.description.toLowerCase().includes(searchLower) || agent.capabilities.some(
|
|
1177
|
+
(entry) => entry.name.toLowerCase().includes(searchLower) || entry.description.toLowerCase().includes(searchLower) || entry.version.toLowerCase().includes(searchLower)
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
function bigintReplacer(_key, value) {
|
|
1181
|
+
return typeof value === "bigint" ? value.toString() : value;
|
|
1182
|
+
}
|
|
1183
|
+
function toBigInt2(value) {
|
|
1184
|
+
if (typeof value === "bigint") {
|
|
1185
|
+
return value;
|
|
1186
|
+
}
|
|
1187
|
+
if (typeof value === "number") {
|
|
1188
|
+
return BigInt(value);
|
|
1189
|
+
}
|
|
1190
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1191
|
+
return BigInt(value);
|
|
1192
|
+
}
|
|
1193
|
+
return 0n;
|
|
1194
|
+
}
|
|
1195
|
+
function normalizePaymentScheme(value) {
|
|
1196
|
+
switch (value) {
|
|
1197
|
+
case PaymentScheme.EXACT:
|
|
1198
|
+
return PaymentScheme.EXACT;
|
|
1199
|
+
case PaymentScheme.UPTO:
|
|
1200
|
+
return PaymentScheme.UPTO;
|
|
1201
|
+
case PaymentScheme.STREAM:
|
|
1202
|
+
return PaymentScheme.STREAM;
|
|
1203
|
+
default:
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function ensureTaskColumns(db) {
|
|
1208
|
+
const rows = db.prepare("PRAGMA table_info(tasks)").all();
|
|
1209
|
+
const existing = new Set(rows.map((row) => row.name).filter((name) => typeof name === "string"));
|
|
1210
|
+
const missingColumns = [
|
|
1211
|
+
["payment_scheme", "TEXT"],
|
|
1212
|
+
["max_price", "TEXT"],
|
|
1213
|
+
["metered_units", "INTEGER"],
|
|
1214
|
+
["unit_price", "TEXT"],
|
|
1215
|
+
["verification_hash", "TEXT"]
|
|
1216
|
+
];
|
|
1217
|
+
for (const [column, type] of missingColumns) {
|
|
1218
|
+
if (!existing.has(column)) {
|
|
1219
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN ${column} ${type}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/graphql/server.ts
|
|
1225
|
+
var MAX_PAGE_SIZE = 100;
|
|
1226
|
+
function createIndexerGraphQLServer(options) {
|
|
1227
|
+
const analytics = options.analytics ?? new AnalyticsEngine(options.store);
|
|
1228
|
+
const schema = createSchema({
|
|
1229
|
+
typeDefs: (
|
|
1230
|
+
/* GraphQL */
|
|
1231
|
+
`
|
|
1232
|
+
enum TaskStatus {
|
|
1233
|
+
OPEN
|
|
1234
|
+
ACCEPTED
|
|
1235
|
+
COMPLETED
|
|
1236
|
+
RELEASED
|
|
1237
|
+
DISPUTED
|
|
1238
|
+
CANCELLED
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
enum PaymentScheme {
|
|
1242
|
+
EXACT
|
|
1243
|
+
UPTO
|
|
1244
|
+
STREAM
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
enum BidStatus {
|
|
1248
|
+
ACTIVE
|
|
1249
|
+
ACCEPTED
|
|
1250
|
+
REJECTED
|
|
1251
|
+
WITHDRAWN
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
enum DisputeStatus {
|
|
1255
|
+
OPEN
|
|
1256
|
+
RESPONDED
|
|
1257
|
+
MUTUAL_RESOLVED
|
|
1258
|
+
ARBITRATED
|
|
1259
|
+
EXPIRED
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
enum TimePeriod {
|
|
1263
|
+
HOUR
|
|
1264
|
+
DAY
|
|
1265
|
+
WEEK
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
enum ProviderSortField {
|
|
1269
|
+
COMPLETED_TASKS
|
|
1270
|
+
EARNINGS
|
|
1271
|
+
REPUTATION
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
type PageInfo {
|
|
1275
|
+
hasNextPage: Boolean!
|
|
1276
|
+
endCursor: String
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
type AgentConnection {
|
|
1280
|
+
nodes: [Agent!]!
|
|
1281
|
+
totalCount: Int!
|
|
1282
|
+
pageInfo: PageInfo!
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
type TaskConnection {
|
|
1286
|
+
nodes: [Task!]!
|
|
1287
|
+
pageInfo: PageInfo!
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
type PricingInfo {
|
|
1291
|
+
rail: String!
|
|
1292
|
+
amount: String!
|
|
1293
|
+
currency: String!
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
type Capability {
|
|
1297
|
+
name: String!
|
|
1298
|
+
description: String!
|
|
1299
|
+
version: String!
|
|
1300
|
+
pricing: PricingInfo!
|
|
1301
|
+
executionMode: String
|
|
1302
|
+
paymentRails: [String!]!
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
type AgentReputation {
|
|
1306
|
+
successRate: Float!
|
|
1307
|
+
totalTasks: Int!
|
|
1308
|
+
totalDisputes: Int!
|
|
1309
|
+
totalEarningsMist: String!
|
|
1310
|
+
stakeAmountMist: String!
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
type Agent {
|
|
1314
|
+
id: ID!
|
|
1315
|
+
owner: String!
|
|
1316
|
+
did: String!
|
|
1317
|
+
name: String!
|
|
1318
|
+
description: String!
|
|
1319
|
+
endpoint: String
|
|
1320
|
+
active: Boolean!
|
|
1321
|
+
version: Int!
|
|
1322
|
+
registeredAt: String!
|
|
1323
|
+
updatedAt: String!
|
|
1324
|
+
capabilities: [Capability!]!
|
|
1325
|
+
totalTasksCompleted: Int!
|
|
1326
|
+
totalTasksFailed: Int!
|
|
1327
|
+
totalTasksDisputed: Int!
|
|
1328
|
+
totalEarningsMist: String!
|
|
1329
|
+
hasStake: Boolean!
|
|
1330
|
+
stakeMist: String
|
|
1331
|
+
stakeType: String
|
|
1332
|
+
categories: [String!]!
|
|
1333
|
+
reputation: AgentReputation!
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
type TaskTransition {
|
|
1337
|
+
eventType: String!
|
|
1338
|
+
status: TaskStatus!
|
|
1339
|
+
txDigest: String!
|
|
1340
|
+
timestampMs: String!
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
type Task {
|
|
1344
|
+
id: ID!
|
|
1345
|
+
requester: String!
|
|
1346
|
+
provider: String
|
|
1347
|
+
capability: String!
|
|
1348
|
+
category: String!
|
|
1349
|
+
inputBlobId: String!
|
|
1350
|
+
resultBlobId: String
|
|
1351
|
+
price: String!
|
|
1352
|
+
paymentScheme: PaymentScheme
|
|
1353
|
+
maxPrice: String
|
|
1354
|
+
meteredUnits: Int
|
|
1355
|
+
unitPrice: String
|
|
1356
|
+
verificationHash: String
|
|
1357
|
+
status: TaskStatus!
|
|
1358
|
+
disputeWindowMs: Int!
|
|
1359
|
+
createdAt: String!
|
|
1360
|
+
acceptedAt: String
|
|
1361
|
+
completedAt: String
|
|
1362
|
+
releasedAt: String
|
|
1363
|
+
disputedAt: String
|
|
1364
|
+
cancelledAt: String
|
|
1365
|
+
expiresAt: String!
|
|
1366
|
+
agreementHash: String
|
|
1367
|
+
bidCount: Int!
|
|
1368
|
+
gasCostMistTotal: String!
|
|
1369
|
+
transitions: [TaskTransition!]!
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
type Bid {
|
|
1373
|
+
id: ID!
|
|
1374
|
+
taskId: String!
|
|
1375
|
+
bidder: String!
|
|
1376
|
+
bidPrice: String!
|
|
1377
|
+
reputationScore: String!
|
|
1378
|
+
evidenceBlob: String
|
|
1379
|
+
createdAt: String!
|
|
1380
|
+
status: BidStatus!
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
type Dispute {
|
|
1384
|
+
id: ID!
|
|
1385
|
+
taskId: String!
|
|
1386
|
+
requester: String!
|
|
1387
|
+
provider: String!
|
|
1388
|
+
escrowAmount: String!
|
|
1389
|
+
status: DisputeStatus!
|
|
1390
|
+
requesterEvidenceBlob: String!
|
|
1391
|
+
providerEvidenceBlob: String
|
|
1392
|
+
requesterProposedSplit: String!
|
|
1393
|
+
providerProposedSplit: String!
|
|
1394
|
+
arbitrator: String
|
|
1395
|
+
rulingSplit: String!
|
|
1396
|
+
openedAt: String!
|
|
1397
|
+
respondedAt: String
|
|
1398
|
+
resolvedAt: String
|
|
1399
|
+
resolutionDeadline: String!
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
type GasCostStat {
|
|
1403
|
+
capability: String!
|
|
1404
|
+
averageGasMist: String!
|
|
1405
|
+
taskCount: Int!
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
type CategoryStat {
|
|
1409
|
+
category: String!
|
|
1410
|
+
taskCount: Int!
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
type MarketplaceStats {
|
|
1414
|
+
averageBidCount: Float!
|
|
1415
|
+
acceptanceRate: Float!
|
|
1416
|
+
categoryPopularity: [CategoryStat!]!
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
type Analytics {
|
|
1420
|
+
totalAgents: Int!
|
|
1421
|
+
activeAgents: Int!
|
|
1422
|
+
totalTasks: Int!
|
|
1423
|
+
completedTasks: Int!
|
|
1424
|
+
disputedTasks: Int!
|
|
1425
|
+
totalVolumeMist: String!
|
|
1426
|
+
averageGasCosts: [GasCostStat!]!
|
|
1427
|
+
marketplace: MarketplaceStats!
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
type TimeBucket {
|
|
1431
|
+
label: String!
|
|
1432
|
+
count: Int!
|
|
1433
|
+
volumeMist: String!
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
type ProviderStats {
|
|
1437
|
+
did: String!
|
|
1438
|
+
owner: String!
|
|
1439
|
+
name: String!
|
|
1440
|
+
completedTasks: Int!
|
|
1441
|
+
earningsMist: String!
|
|
1442
|
+
disputeCount: Int!
|
|
1443
|
+
successRate: Float!
|
|
1444
|
+
reputation: Float!
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
type Query {
|
|
1448
|
+
agents(capability: String, minReputation: Float, category: String, limit: Int, offset: Int): AgentConnection!
|
|
1449
|
+
agent(did: String!): Agent
|
|
1450
|
+
tasks(status: TaskStatus, requester: String, provider: String, category: String, after: String, limit: Int): TaskConnection!
|
|
1451
|
+
task(id: String!): Task
|
|
1452
|
+
bids(taskId: String!, status: BidStatus): [Bid!]!
|
|
1453
|
+
disputes(status: DisputeStatus, agent: String): [Dispute!]!
|
|
1454
|
+
analytics: Analytics!
|
|
1455
|
+
taskVolume(period: TimePeriod!, buckets: Int): [TimeBucket!]!
|
|
1456
|
+
topProviders(limit: Int, sortBy: ProviderSortField): [ProviderStats!]!
|
|
1457
|
+
}
|
|
1458
|
+
`
|
|
1459
|
+
),
|
|
1460
|
+
resolvers: {
|
|
1461
|
+
TaskStatus: {
|
|
1462
|
+
OPEN: TaskStatus2.OPEN,
|
|
1463
|
+
ACCEPTED: TaskStatus2.ACCEPTED,
|
|
1464
|
+
COMPLETED: TaskStatus2.COMPLETED,
|
|
1465
|
+
RELEASED: TaskStatus2.RELEASED,
|
|
1466
|
+
DISPUTED: TaskStatus2.DISPUTED,
|
|
1467
|
+
CANCELLED: TaskStatus2.CANCELLED
|
|
1468
|
+
},
|
|
1469
|
+
BidStatus: {
|
|
1470
|
+
ACTIVE: BidStatus3.ACTIVE,
|
|
1471
|
+
ACCEPTED: BidStatus3.ACCEPTED,
|
|
1472
|
+
REJECTED: BidStatus3.REJECTED,
|
|
1473
|
+
WITHDRAWN: BidStatus3.WITHDRAWN
|
|
1474
|
+
},
|
|
1475
|
+
PaymentScheme: {
|
|
1476
|
+
EXACT: PaymentScheme2.EXACT,
|
|
1477
|
+
UPTO: PaymentScheme2.UPTO,
|
|
1478
|
+
STREAM: PaymentScheme2.STREAM
|
|
1479
|
+
},
|
|
1480
|
+
DisputeStatus: {
|
|
1481
|
+
OPEN: DisputeStatus2.OPEN,
|
|
1482
|
+
RESPONDED: DisputeStatus2.RESPONDED,
|
|
1483
|
+
MUTUAL_RESOLVED: DisputeStatus2.MUTUAL_RESOLVED,
|
|
1484
|
+
ARBITRATED: DisputeStatus2.ARBITRATED,
|
|
1485
|
+
EXPIRED: DisputeStatus2.EXPIRED
|
|
1486
|
+
},
|
|
1487
|
+
TimePeriod: {
|
|
1488
|
+
HOUR: "hour",
|
|
1489
|
+
DAY: "day",
|
|
1490
|
+
WEEK: "week"
|
|
1491
|
+
},
|
|
1492
|
+
ProviderSortField: {
|
|
1493
|
+
COMPLETED_TASKS: "completedTasks",
|
|
1494
|
+
EARNINGS: "earnings",
|
|
1495
|
+
REPUTATION: "reputation"
|
|
1496
|
+
},
|
|
1497
|
+
Query: {
|
|
1498
|
+
agents: (_root, args) => {
|
|
1499
|
+
const limit = normalizeLimit2(args.limit, 20);
|
|
1500
|
+
const offset = normalizeOffset2(args.offset);
|
|
1501
|
+
const capability = trimOptional(args.capability);
|
|
1502
|
+
const category = trimOptional(args.category);
|
|
1503
|
+
const nodes = options.store.queryAgents({
|
|
1504
|
+
capability,
|
|
1505
|
+
minReputation: normalizeOptionalFloat(args.minReputation, "minReputation"),
|
|
1506
|
+
category,
|
|
1507
|
+
limit,
|
|
1508
|
+
offset,
|
|
1509
|
+
sortBy: "reputation"
|
|
1510
|
+
});
|
|
1511
|
+
const totalCount = options.store.countAgents({
|
|
1512
|
+
capability,
|
|
1513
|
+
minReputation: normalizeOptionalFloat(args.minReputation, "minReputation"),
|
|
1514
|
+
category,
|
|
1515
|
+
sortBy: "reputation"
|
|
1516
|
+
});
|
|
1517
|
+
return {
|
|
1518
|
+
nodes,
|
|
1519
|
+
totalCount,
|
|
1520
|
+
pageInfo: {
|
|
1521
|
+
hasNextPage: offset + nodes.length < totalCount,
|
|
1522
|
+
endCursor: nodes.length > 0 ? Buffer.from(String(offset + nodes.length)).toString("base64url") : null
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
},
|
|
1526
|
+
agent: (_root, args) => options.store.getAgentByDid(requireNonEmpty(args.did, "did")),
|
|
1527
|
+
tasks: (_root, args) => {
|
|
1528
|
+
const limit = normalizeLimit2(args.limit, 20);
|
|
1529
|
+
const rows = options.store.queryTasks({
|
|
1530
|
+
status: args.status,
|
|
1531
|
+
requester: trimOptional(args.requester),
|
|
1532
|
+
provider: trimOptional(args.provider),
|
|
1533
|
+
category: trimOptional(args.category),
|
|
1534
|
+
after: trimOptional(args.after),
|
|
1535
|
+
limit: limit + 1
|
|
1536
|
+
});
|
|
1537
|
+
const nodes = rows.slice(0, limit);
|
|
1538
|
+
const endCursor = nodes.length > 0 ? encodeCursor(nodes[nodes.length - 1]) : null;
|
|
1539
|
+
return {
|
|
1540
|
+
nodes,
|
|
1541
|
+
pageInfo: {
|
|
1542
|
+
hasNextPage: rows.length > limit,
|
|
1543
|
+
endCursor
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
},
|
|
1547
|
+
task: (_root, args) => options.store.getTask(requireNonEmpty(args.id, "id")),
|
|
1548
|
+
bids: (_root, args) => options.store.getBids(requireNonEmpty(args.taskId, "taskId"), args.status),
|
|
1549
|
+
disputes: (_root, args) => options.store.getDisputes({ status: args.status, agent: trimOptional(args.agent) }),
|
|
1550
|
+
analytics: () => analytics.getSummary(),
|
|
1551
|
+
taskVolume: (_root, args) => analytics.getTaskVolume(args.period, args.buckets),
|
|
1552
|
+
topProviders: (_root, args) => analytics.getTopProviders(normalizeLimit2(args.limit, 10), args.sortBy)
|
|
1553
|
+
},
|
|
1554
|
+
Agent: {
|
|
1555
|
+
registeredAt: (agent) => String(agent.registeredAt),
|
|
1556
|
+
updatedAt: (agent) => String(agent.updatedAt),
|
|
1557
|
+
totalTasksCompleted: (agent) => agent.totalTasksCompleted ?? 0,
|
|
1558
|
+
totalTasksFailed: (agent) => agent.totalTasksFailed ?? 0,
|
|
1559
|
+
totalTasksDisputed: (agent) => agent.totalTasksDisputed ?? 0,
|
|
1560
|
+
totalEarningsMist: (agent) => (agent.totalEarningsMist ?? 0n).toString(),
|
|
1561
|
+
hasStake: (agent) => Boolean(agent.hasStake),
|
|
1562
|
+
stakeMist: (agent) => agent.stakeMist?.toString() ?? null,
|
|
1563
|
+
categories: (agent) => listAgentCategories(options.store, agent.owner),
|
|
1564
|
+
reputation: (agent) => buildAgentReputation(agent)
|
|
1565
|
+
},
|
|
1566
|
+
Capability: {
|
|
1567
|
+
paymentRails: (capability) => capability.paymentRails ?? []
|
|
1568
|
+
},
|
|
1569
|
+
PricingInfo: {
|
|
1570
|
+
amount: (pricing) => pricing.amount.toString()
|
|
1571
|
+
},
|
|
1572
|
+
Task: {
|
|
1573
|
+
price: (task) => task.price.toString(),
|
|
1574
|
+
maxPrice: (task) => task.maxPrice?.toString() ?? null,
|
|
1575
|
+
meteredUnits: (task) => task.meteredUnits ?? null,
|
|
1576
|
+
unitPrice: (task) => task.unitPrice?.toString() ?? null,
|
|
1577
|
+
verificationHash: (task) => task.verificationHash ?? null,
|
|
1578
|
+
createdAt: (task) => String(task.createdAt),
|
|
1579
|
+
acceptedAt: (task) => nullableString(task.acceptedAt),
|
|
1580
|
+
completedAt: (task) => nullableString(task.completedAt),
|
|
1581
|
+
releasedAt: (task) => nullableString(task.releasedAt),
|
|
1582
|
+
disputedAt: (task) => nullableString(task.disputedAt),
|
|
1583
|
+
cancelledAt: (task) => nullableString(task.cancelledAt),
|
|
1584
|
+
expiresAt: (task) => String(task.expiresAt),
|
|
1585
|
+
gasCostMistTotal: (task) => task.gasCostMistTotal.toString(),
|
|
1586
|
+
transitions: (task) => task.transitions ?? options.store.getTaskTransitions(task.id)
|
|
1587
|
+
},
|
|
1588
|
+
TaskTransition: {
|
|
1589
|
+
timestampMs: (transition) => String(transition.timestampMs)
|
|
1590
|
+
},
|
|
1591
|
+
Bid: {
|
|
1592
|
+
taskId: (bid) => bid.taskId,
|
|
1593
|
+
bidPrice: (bid) => bid.bidPrice.toString(),
|
|
1594
|
+
reputationScore: (bid) => bid.reputationScore.toString(),
|
|
1595
|
+
createdAt: (bid) => String(bid.createdAt)
|
|
1596
|
+
},
|
|
1597
|
+
Dispute: {
|
|
1598
|
+
taskId: (dispute) => dispute.taskId,
|
|
1599
|
+
escrowAmount: (dispute) => dispute.escrowAmount.toString(),
|
|
1600
|
+
requesterProposedSplit: (dispute) => dispute.requesterProposedSplit.toString(),
|
|
1601
|
+
providerProposedSplit: (dispute) => dispute.providerProposedSplit.toString(),
|
|
1602
|
+
rulingSplit: (dispute) => dispute.rulingSplit.toString(),
|
|
1603
|
+
openedAt: (dispute) => String(dispute.openedAt),
|
|
1604
|
+
respondedAt: (dispute) => nullableString(dispute.respondedAt),
|
|
1605
|
+
resolvedAt: (dispute) => nullableString(dispute.resolvedAt),
|
|
1606
|
+
resolutionDeadline: (dispute) => String(dispute.resolutionDeadline)
|
|
1607
|
+
},
|
|
1608
|
+
GasCostStat: {
|
|
1609
|
+
averageGasMist: (row) => row.averageGasMist.toString()
|
|
1610
|
+
},
|
|
1611
|
+
Analytics: {
|
|
1612
|
+
totalVolumeMist: (summary) => summary.totalVolumeMist.toString()
|
|
1613
|
+
},
|
|
1614
|
+
TimeBucket: {
|
|
1615
|
+
volumeMist: (bucket) => bucket.volumeMist.toString()
|
|
1616
|
+
},
|
|
1617
|
+
ProviderStats: {
|
|
1618
|
+
earningsMist: (row) => row.earningsMist.toString()
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
const yoga = createYoga({
|
|
1623
|
+
schema,
|
|
1624
|
+
graphqlEndpoint: "/graphql",
|
|
1625
|
+
maskedErrors: true
|
|
1626
|
+
});
|
|
1627
|
+
const server = createServer(yoga);
|
|
1628
|
+
return {
|
|
1629
|
+
schema,
|
|
1630
|
+
server,
|
|
1631
|
+
fetch: yoga.fetch.bind(yoga),
|
|
1632
|
+
start: async () => await startServer(server, options.host ?? "0.0.0.0", options.port ?? 4e3, options.logger),
|
|
1633
|
+
stop: async () => {
|
|
1634
|
+
if (!server.listening) {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
await new Promise((resolvePromise, reject) => {
|
|
1638
|
+
server.close((error) => {
|
|
1639
|
+
if (error) {
|
|
1640
|
+
reject(error);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
resolvePromise();
|
|
1644
|
+
});
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function listAgentCategories(store, owner) {
|
|
1650
|
+
const rows = store.getDatabase().prepare("SELECT DISTINCT category FROM tasks WHERE provider = ? ORDER BY category ASC").all(owner);
|
|
1651
|
+
return rows.map((row) => row.category);
|
|
1652
|
+
}
|
|
1653
|
+
function buildAgentReputation(agent) {
|
|
1654
|
+
const completed = agent.totalTasksCompleted ?? 0;
|
|
1655
|
+
const failed = agent.totalTasksFailed ?? 0;
|
|
1656
|
+
const totalTasks = completed + failed;
|
|
1657
|
+
return {
|
|
1658
|
+
successRate: totalTasks === 0 ? 0 : completed / totalTasks,
|
|
1659
|
+
totalTasks,
|
|
1660
|
+
totalDisputes: agent.totalTasksDisputed ?? 0,
|
|
1661
|
+
totalEarningsMist: (agent.totalEarningsMist ?? 0n).toString(),
|
|
1662
|
+
stakeAmountMist: (agent.stakeMist ?? 0n).toString()
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function nullableString(value) {
|
|
1666
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
1667
|
+
}
|
|
1668
|
+
async function startServer(server, host, port, logger2) {
|
|
1669
|
+
const address = await new Promise((resolvePromise, reject) => {
|
|
1670
|
+
server.once("error", reject);
|
|
1671
|
+
server.listen(port, host, () => {
|
|
1672
|
+
server.off("error", reject);
|
|
1673
|
+
const boundAddress = server.address();
|
|
1674
|
+
const resolvedPort = typeof boundAddress === "object" && boundAddress ? boundAddress.port : port;
|
|
1675
|
+
resolvePromise(`http://${host}:${resolvedPort}/graphql`);
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
logger2?.info?.({ address }, "Indexer GraphQL server started");
|
|
1679
|
+
return address;
|
|
1680
|
+
}
|
|
1681
|
+
function normalizeLimit2(limit, fallback) {
|
|
1682
|
+
return typeof limit === "number" && Number.isFinite(limit) ? Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(limit))) : Math.min(MAX_PAGE_SIZE, fallback);
|
|
1683
|
+
}
|
|
1684
|
+
function normalizeOffset2(offset) {
|
|
1685
|
+
return typeof offset === "number" && Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
1686
|
+
}
|
|
1687
|
+
function normalizeOptionalFloat(value, field) {
|
|
1688
|
+
if (value === void 0) {
|
|
1689
|
+
return void 0;
|
|
1690
|
+
}
|
|
1691
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1692
|
+
throw new Error(`${field} must be a non-negative finite number.`);
|
|
1693
|
+
}
|
|
1694
|
+
return value;
|
|
1695
|
+
}
|
|
1696
|
+
function trimOptional(value) {
|
|
1697
|
+
const normalized = value?.trim();
|
|
1698
|
+
return normalized ? normalized : void 0;
|
|
1699
|
+
}
|
|
1700
|
+
function requireNonEmpty(value, field) {
|
|
1701
|
+
const normalized = value.trim();
|
|
1702
|
+
if (!normalized) {
|
|
1703
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
1704
|
+
}
|
|
1705
|
+
return normalized;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/indexer.ts
|
|
1709
|
+
import pino from "pino";
|
|
1710
|
+
import { parseRawEvent } from "@hivemind-os/collective-core";
|
|
1711
|
+
import { DisputeStatus as DisputeStatus3, TaskStatus as TaskStatus3 } from "@hivemind-os/collective-types";
|
|
1712
|
+
var logger = pino({ name: "@hivemind-os/collective-indexer" });
|
|
1713
|
+
var EVENT_NAMES = {
|
|
1714
|
+
agentRegistered: "registry::AgentRegistered",
|
|
1715
|
+
agentUpdated: "registry::AgentUpdated",
|
|
1716
|
+
agentDeactivated: "registry::AgentDeactivated",
|
|
1717
|
+
taskPosted: "task::TaskPosted",
|
|
1718
|
+
taskAccepted: "task::TaskAccepted",
|
|
1719
|
+
taskCompleted: "task::TaskCompleted",
|
|
1720
|
+
taskReleased: "task::TaskPaymentReleased",
|
|
1721
|
+
taskDisputed: "task::TaskDisputed",
|
|
1722
|
+
taskCancelled: "task::TaskCancelled",
|
|
1723
|
+
taskExpiredRefunded: "task::TaskExpiredRefunded",
|
|
1724
|
+
bidPlaced: "marketplace::BidPlaced",
|
|
1725
|
+
bidAccepted: "marketplace::BidAccepted",
|
|
1726
|
+
bidWithdrawn: "marketplace::BidWithdrawn",
|
|
1727
|
+
bidRejected: "marketplace::BidRejected",
|
|
1728
|
+
disputeOpened: "dispute::DisputeOpened",
|
|
1729
|
+
disputeResponded: "dispute::DisputeResponded",
|
|
1730
|
+
disputeMutuallyResolved: "dispute::DisputeMutuallyResolved",
|
|
1731
|
+
disputeArbitrated: "dispute::DisputeArbitrated",
|
|
1732
|
+
disputeExpired: "dispute::DisputeExpired",
|
|
1733
|
+
stakeDeposited: "staking::StakeDeposited",
|
|
1734
|
+
stakeWithdrawn: "staking::StakeWithdrawn",
|
|
1735
|
+
stakeSlashed: "staking::StakeSlashed",
|
|
1736
|
+
deactivationStarted: "staking::DeactivationStarted",
|
|
1737
|
+
anchorPublished: "reputation::AnchorPublished"
|
|
1738
|
+
};
|
|
1739
|
+
var SUPPORTED_EVENT_TYPES = Object.values(EVENT_NAMES);
|
|
1740
|
+
var MeshIndexer = class {
|
|
1741
|
+
constructor(options) {
|
|
1742
|
+
this.options = options;
|
|
1743
|
+
this.logger = options.logger ?? logger;
|
|
1744
|
+
}
|
|
1745
|
+
options;
|
|
1746
|
+
logger;
|
|
1747
|
+
txMetadataCache = /* @__PURE__ */ new Map();
|
|
1748
|
+
running = false;
|
|
1749
|
+
timer;
|
|
1750
|
+
pollLoopPromise;
|
|
1751
|
+
start() {
|
|
1752
|
+
if (this.running) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
this.running = true;
|
|
1756
|
+
this.pollLoopPromise = this.pollLoop().finally(() => {
|
|
1757
|
+
this.pollLoopPromise = void 0;
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
async stop() {
|
|
1761
|
+
this.running = false;
|
|
1762
|
+
if (this.timer) {
|
|
1763
|
+
clearTimeout(this.timer);
|
|
1764
|
+
this.timer = void 0;
|
|
1765
|
+
}
|
|
1766
|
+
await this.pollLoopPromise?.catch(() => void 0);
|
|
1767
|
+
}
|
|
1768
|
+
isRunning() {
|
|
1769
|
+
return this.running;
|
|
1770
|
+
}
|
|
1771
|
+
async backfill(fromCheckpoint = this.options.startCheckpoint) {
|
|
1772
|
+
return await this.indexAllStreams(fromCheckpoint);
|
|
1773
|
+
}
|
|
1774
|
+
async pollOnce() {
|
|
1775
|
+
return await this.indexAllStreams(this.options.startCheckpoint);
|
|
1776
|
+
}
|
|
1777
|
+
async processEvent(rawEvent, metadata) {
|
|
1778
|
+
const txMetadata = metadata ?? await this.getTransactionMetadata(rawEvent.id.txDigest);
|
|
1779
|
+
const wasInserted = this.options.store.recordEvent({
|
|
1780
|
+
eventId: formatEventId(rawEvent),
|
|
1781
|
+
eventType: rawEvent.type,
|
|
1782
|
+
packageId: this.options.packageId,
|
|
1783
|
+
txDigest: rawEvent.id.txDigest,
|
|
1784
|
+
timestampMs: readNumber2(rawEvent.timestampMs),
|
|
1785
|
+
payload: normalizeMoveValue(rawEvent.parsedJson),
|
|
1786
|
+
checkpoint: txMetadata.checkpoint,
|
|
1787
|
+
module: rawEvent.transactionModule
|
|
1788
|
+
});
|
|
1789
|
+
if (!wasInserted) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const parsed = parseRawEvent(rawEvent, this.options.packageId);
|
|
1793
|
+
if (parsed) {
|
|
1794
|
+
this.handleParsedEvent(parsed, txMetadata);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const extended = await this.parseExtendedEvent(rawEvent);
|
|
1798
|
+
if (extended) {
|
|
1799
|
+
this.handleExtendedEvent(extended, txMetadata);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
async pollLoop() {
|
|
1803
|
+
if (!this.running) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
let nextDelay = this.options.pollIntervalMs ?? 5e3;
|
|
1807
|
+
try {
|
|
1808
|
+
const processed = await this.pollOnce();
|
|
1809
|
+
if (processed > 0) {
|
|
1810
|
+
nextDelay = 0;
|
|
1811
|
+
}
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
this.logger.error({ err: error }, "Indexer polling failed.");
|
|
1814
|
+
}
|
|
1815
|
+
if (this.running) {
|
|
1816
|
+
this.timer = setTimeout(() => {
|
|
1817
|
+
void this.pollLoop();
|
|
1818
|
+
}, nextDelay);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
async indexAllStreams(fromCheckpoint) {
|
|
1822
|
+
let processed = 0;
|
|
1823
|
+
for (const suffix of SUPPORTED_EVENT_TYPES) {
|
|
1824
|
+
processed += await this.indexEventType(`${this.options.packageId}::${suffix}`, fromCheckpoint);
|
|
1825
|
+
}
|
|
1826
|
+
return processed;
|
|
1827
|
+
}
|
|
1828
|
+
async indexEventType(eventType, fromCheckpoint) {
|
|
1829
|
+
const cursorKey = `event:${eventType}`;
|
|
1830
|
+
let cursor = this.options.store.getCursor(cursorKey);
|
|
1831
|
+
let processed = 0;
|
|
1832
|
+
while (true) {
|
|
1833
|
+
const page = await this.options.suiClient.queryEvents(eventType, cursor, 100);
|
|
1834
|
+
for (const event of page.events) {
|
|
1835
|
+
const metadata = await this.getTransactionMetadata(event.id.txDigest);
|
|
1836
|
+
if (typeof fromCheckpoint === "number" && typeof metadata.checkpoint === "number" && metadata.checkpoint < fromCheckpoint) {
|
|
1837
|
+
cursor = event.id;
|
|
1838
|
+
this.options.store.setCursor(cursorKey, event.id);
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
await this.processEvent(event, metadata);
|
|
1842
|
+
cursor = event.id;
|
|
1843
|
+
this.options.store.setCursor(cursorKey, event.id);
|
|
1844
|
+
processed += 1;
|
|
1845
|
+
}
|
|
1846
|
+
if (!page.hasMore || !page.nextCursor) {
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
cursor = page.nextCursor;
|
|
1850
|
+
}
|
|
1851
|
+
return processed;
|
|
1852
|
+
}
|
|
1853
|
+
handleParsedEvent(event, metadata) {
|
|
1854
|
+
switch (event.type) {
|
|
1855
|
+
case "agent.registered":
|
|
1856
|
+
case "agent.updated":
|
|
1857
|
+
this.options.store.upsertAgent(event.agent);
|
|
1858
|
+
break;
|
|
1859
|
+
case "agent.deactivated":
|
|
1860
|
+
this.options.store.markAgentInactive(event.agentId);
|
|
1861
|
+
break;
|
|
1862
|
+
case "task.posted":
|
|
1863
|
+
this.options.store.upsertTask(event.task, event.txDigest, metadata.gasCostMist);
|
|
1864
|
+
break;
|
|
1865
|
+
case "task.accepted":
|
|
1866
|
+
this.options.store.updateTaskStatus({
|
|
1867
|
+
taskId: event.taskId,
|
|
1868
|
+
status: event.status,
|
|
1869
|
+
txDigest: event.txDigest,
|
|
1870
|
+
timestampMs: event.acceptedAt,
|
|
1871
|
+
provider: event.provider,
|
|
1872
|
+
requester: event.requester,
|
|
1873
|
+
price: event.price,
|
|
1874
|
+
gasCostMist: metadata.gasCostMist,
|
|
1875
|
+
eventType: event.type,
|
|
1876
|
+
payload: event
|
|
1877
|
+
});
|
|
1878
|
+
break;
|
|
1879
|
+
case "task.completed":
|
|
1880
|
+
this.options.store.updateTaskStatus({
|
|
1881
|
+
taskId: event.taskId,
|
|
1882
|
+
status: event.status,
|
|
1883
|
+
txDigest: event.txDigest,
|
|
1884
|
+
timestampMs: event.completedAt,
|
|
1885
|
+
provider: event.provider,
|
|
1886
|
+
resultBlobId: event.resultBlobId,
|
|
1887
|
+
price: event.price,
|
|
1888
|
+
paymentScheme: event.paymentScheme,
|
|
1889
|
+
meteredUnits: event.meteredUnits,
|
|
1890
|
+
verificationHash: event.verificationHash,
|
|
1891
|
+
gasCostMist: metadata.gasCostMist,
|
|
1892
|
+
eventType: event.type,
|
|
1893
|
+
payload: event
|
|
1894
|
+
});
|
|
1895
|
+
break;
|
|
1896
|
+
case "task.released":
|
|
1897
|
+
this.options.store.updateTaskStatus({
|
|
1898
|
+
taskId: event.taskId,
|
|
1899
|
+
status: event.status,
|
|
1900
|
+
txDigest: event.txDigest,
|
|
1901
|
+
timestampMs: event.releasedAt,
|
|
1902
|
+
provider: event.provider,
|
|
1903
|
+
requester: event.requester,
|
|
1904
|
+
price: event.price,
|
|
1905
|
+
gasCostMist: metadata.gasCostMist,
|
|
1906
|
+
eventType: event.type,
|
|
1907
|
+
payload: event
|
|
1908
|
+
});
|
|
1909
|
+
break;
|
|
1910
|
+
case "task.disputed":
|
|
1911
|
+
this.options.store.updateTaskStatus({
|
|
1912
|
+
taskId: event.taskId,
|
|
1913
|
+
status: event.status,
|
|
1914
|
+
txDigest: event.txDigest,
|
|
1915
|
+
timestampMs: event.disputedAt,
|
|
1916
|
+
provider: event.provider,
|
|
1917
|
+
requester: event.requester,
|
|
1918
|
+
gasCostMist: metadata.gasCostMist,
|
|
1919
|
+
eventType: event.type,
|
|
1920
|
+
payload: event
|
|
1921
|
+
});
|
|
1922
|
+
break;
|
|
1923
|
+
case "task.cancelled":
|
|
1924
|
+
this.options.store.updateTaskStatus({
|
|
1925
|
+
taskId: event.taskId,
|
|
1926
|
+
status: event.status,
|
|
1927
|
+
txDigest: event.txDigest,
|
|
1928
|
+
timestampMs: event.cancelledAt,
|
|
1929
|
+
requester: event.requester,
|
|
1930
|
+
gasCostMist: metadata.gasCostMist,
|
|
1931
|
+
eventType: event.type,
|
|
1932
|
+
payload: event
|
|
1933
|
+
});
|
|
1934
|
+
break;
|
|
1935
|
+
case "bid.placed":
|
|
1936
|
+
this.options.store.upsertBid(event.bid);
|
|
1937
|
+
break;
|
|
1938
|
+
case "bid.accepted":
|
|
1939
|
+
this.options.store.updateBidStatus(event.bidId, event.status, event.acceptedAt);
|
|
1940
|
+
break;
|
|
1941
|
+
case "bid.withdrawn":
|
|
1942
|
+
this.options.store.updateBidStatus(event.bidId, event.status, event.withdrawnAt);
|
|
1943
|
+
break;
|
|
1944
|
+
case "bid.rejected":
|
|
1945
|
+
this.options.store.updateBidStatus(event.bidId, event.status, event.rejectedAt);
|
|
1946
|
+
break;
|
|
1947
|
+
default:
|
|
1948
|
+
break;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
handleExtendedEvent(event, metadata) {
|
|
1952
|
+
switch (event.type) {
|
|
1953
|
+
case "dispute.opened":
|
|
1954
|
+
this.options.store.upsertDispute(event.dispute);
|
|
1955
|
+
break;
|
|
1956
|
+
case "dispute.updated": {
|
|
1957
|
+
const dispute = event.dispute;
|
|
1958
|
+
if (dispute) {
|
|
1959
|
+
this.options.store.upsertDispute(dispute);
|
|
1960
|
+
} else {
|
|
1961
|
+
this.options.store.updateDispute({
|
|
1962
|
+
disputeId: event.disputeId,
|
|
1963
|
+
status: event.status,
|
|
1964
|
+
respondedAt: event.status === DisputeStatus3.RESPONDED ? event.timestampMs : void 0,
|
|
1965
|
+
resolvedAt: event.status !== DisputeStatus3.RESPONDED ? event.timestampMs : void 0,
|
|
1966
|
+
providerEvidenceBlob: event.providerEvidenceBlob,
|
|
1967
|
+
providerProposedSplit: event.providerProposedSplit,
|
|
1968
|
+
arbitrator: event.arbitrator,
|
|
1969
|
+
rulingSplit: event.rulingSplit
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
if (event.status !== DisputeStatus3.RESPONDED) {
|
|
1973
|
+
const taskId = dispute?.taskId;
|
|
1974
|
+
if (taskId) {
|
|
1975
|
+
this.options.store.updateTaskStatus({
|
|
1976
|
+
taskId,
|
|
1977
|
+
status: TaskStatus3.RELEASED,
|
|
1978
|
+
txDigest: event.txDigest,
|
|
1979
|
+
timestampMs: event.timestampMs,
|
|
1980
|
+
requester: dispute.requester,
|
|
1981
|
+
provider: dispute.provider,
|
|
1982
|
+
gasCostMist: metadata.gasCostMist,
|
|
1983
|
+
eventType: `task.released_after_${event.type}`,
|
|
1984
|
+
payload: event
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
break;
|
|
1989
|
+
}
|
|
1990
|
+
case "stake.deposited":
|
|
1991
|
+
this.options.store.upsertStake({
|
|
1992
|
+
stakeId: event.stakeId,
|
|
1993
|
+
owner: event.owner,
|
|
1994
|
+
amountMist: event.amountMist,
|
|
1995
|
+
stakeType: event.stakeType,
|
|
1996
|
+
stakedAt: event.timestampMs,
|
|
1997
|
+
active: true
|
|
1998
|
+
});
|
|
1999
|
+
break;
|
|
2000
|
+
case "stake.withdrawn":
|
|
2001
|
+
this.options.store.upsertStake({
|
|
2002
|
+
stakeId: event.stakeId,
|
|
2003
|
+
owner: event.owner,
|
|
2004
|
+
amountMist: 0n,
|
|
2005
|
+
withdrawnAt: event.timestampMs,
|
|
2006
|
+
active: false
|
|
2007
|
+
});
|
|
2008
|
+
break;
|
|
2009
|
+
case "stake.slashed":
|
|
2010
|
+
this.options.store.addStakeSlash(event.stakeId, event.amountMist);
|
|
2011
|
+
break;
|
|
2012
|
+
case "stake.deactivation_started":
|
|
2013
|
+
this.options.store.upsertStake({
|
|
2014
|
+
stakeId: event.stakeId,
|
|
2015
|
+
owner: event.owner,
|
|
2016
|
+
deactivatedAt: event.timestampMs,
|
|
2017
|
+
active: false
|
|
2018
|
+
});
|
|
2019
|
+
break;
|
|
2020
|
+
case "reputation.anchor_published":
|
|
2021
|
+
this.options.store.upsertReputationAnchor({
|
|
2022
|
+
anchorId: event.anchor.anchorId,
|
|
2023
|
+
author: event.anchor.author,
|
|
2024
|
+
merkleRoot: event.anchor.merkleRoot,
|
|
2025
|
+
eventCount: event.anchor.eventCount,
|
|
2026
|
+
blobId: event.anchor.blobId,
|
|
2027
|
+
fromTimestamp: event.anchor.fromTimestamp,
|
|
2028
|
+
toTimestamp: event.anchor.toTimestamp,
|
|
2029
|
+
createdAt: event.timestampMs,
|
|
2030
|
+
txDigest: event.txDigest
|
|
2031
|
+
});
|
|
2032
|
+
break;
|
|
2033
|
+
default:
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
async parseExtendedEvent(rawEvent) {
|
|
2038
|
+
const payload = toRecord(normalizeMoveValue(rawEvent.parsedJson)) ?? {};
|
|
2039
|
+
const base = {
|
|
2040
|
+
txDigest: rawEvent.id.txDigest,
|
|
2041
|
+
timestampMs: readNumber2(rawEvent.timestampMs)
|
|
2042
|
+
};
|
|
2043
|
+
switch (rawEvent.type) {
|
|
2044
|
+
case `${this.options.packageId}::${EVENT_NAMES.disputeOpened}`: {
|
|
2045
|
+
const disputeId = readString(payload.dispute_id, payload.disputeId);
|
|
2046
|
+
const dispute = await this.fetchDispute(disputeId) ?? buildFallbackDispute(payload, disputeId, base.timestampMs);
|
|
2047
|
+
return { ...base, type: "dispute.opened", disputeId, dispute };
|
|
2048
|
+
}
|
|
2049
|
+
case `${this.options.packageId}::${EVENT_NAMES.disputeResponded}`: {
|
|
2050
|
+
const disputeId = readString(payload.dispute_id, payload.disputeId);
|
|
2051
|
+
return {
|
|
2052
|
+
...base,
|
|
2053
|
+
type: "dispute.updated",
|
|
2054
|
+
disputeId,
|
|
2055
|
+
dispute: await this.fetchDispute(disputeId),
|
|
2056
|
+
status: DisputeStatus3.RESPONDED,
|
|
2057
|
+
providerEvidenceBlob: readBytes(payload.provider_evidence_blob, payload.providerEvidenceBlob)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
case `${this.options.packageId}::${EVENT_NAMES.disputeMutuallyResolved}`: {
|
|
2061
|
+
const disputeId = readString(payload.dispute_id, payload.disputeId);
|
|
2062
|
+
return {
|
|
2063
|
+
...base,
|
|
2064
|
+
type: "dispute.updated",
|
|
2065
|
+
disputeId,
|
|
2066
|
+
dispute: await this.fetchDispute(disputeId),
|
|
2067
|
+
status: DisputeStatus3.MUTUAL_RESOLVED,
|
|
2068
|
+
rulingSplit: readBigInt(payload.provider_amount, payload.providerAmount)
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
case `${this.options.packageId}::${EVENT_NAMES.disputeArbitrated}`: {
|
|
2072
|
+
const disputeId = readString(payload.dispute_id, payload.disputeId);
|
|
2073
|
+
return {
|
|
2074
|
+
...base,
|
|
2075
|
+
type: "dispute.updated",
|
|
2076
|
+
disputeId,
|
|
2077
|
+
dispute: await this.fetchDispute(disputeId),
|
|
2078
|
+
status: DisputeStatus3.ARBITRATED,
|
|
2079
|
+
arbitrator: readString(payload.arbitrator),
|
|
2080
|
+
rulingSplit: readBigInt(payload.provider_amount, payload.providerAmount)
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
case `${this.options.packageId}::${EVENT_NAMES.disputeExpired}`: {
|
|
2084
|
+
const disputeId = readString(payload.dispute_id, payload.disputeId);
|
|
2085
|
+
return {
|
|
2086
|
+
...base,
|
|
2087
|
+
type: "dispute.updated",
|
|
2088
|
+
disputeId,
|
|
2089
|
+
dispute: await this.fetchDispute(disputeId),
|
|
2090
|
+
status: DisputeStatus3.EXPIRED
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
case `${this.options.packageId}::${EVENT_NAMES.stakeDeposited}`:
|
|
2094
|
+
return {
|
|
2095
|
+
...base,
|
|
2096
|
+
type: "stake.deposited",
|
|
2097
|
+
stakeId: readString(payload.stake_id, payload.stakeId),
|
|
2098
|
+
owner: readString(payload.owner),
|
|
2099
|
+
amountMist: readBigInt(payload.amount),
|
|
2100
|
+
stakeType: normalizeStakeType(payload.stake_type, payload.stakeType)
|
|
2101
|
+
};
|
|
2102
|
+
case `${this.options.packageId}::${EVENT_NAMES.stakeWithdrawn}`:
|
|
2103
|
+
return {
|
|
2104
|
+
...base,
|
|
2105
|
+
type: "stake.withdrawn",
|
|
2106
|
+
stakeId: readString(payload.stake_id, payload.stakeId),
|
|
2107
|
+
owner: readString(payload.owner),
|
|
2108
|
+
amountMist: readBigInt(payload.amount)
|
|
2109
|
+
};
|
|
2110
|
+
case `${this.options.packageId}::${EVENT_NAMES.stakeSlashed}`:
|
|
2111
|
+
return {
|
|
2112
|
+
...base,
|
|
2113
|
+
type: "stake.slashed",
|
|
2114
|
+
stakeId: readString(payload.stake_id, payload.stakeId),
|
|
2115
|
+
target: readString(payload.target),
|
|
2116
|
+
amountMist: readBigInt(payload.amount),
|
|
2117
|
+
taskId: readString(payload.task_id, payload.taskId)
|
|
2118
|
+
};
|
|
2119
|
+
case `${this.options.packageId}::${EVENT_NAMES.deactivationStarted}`:
|
|
2120
|
+
return {
|
|
2121
|
+
...base,
|
|
2122
|
+
type: "stake.deactivation_started",
|
|
2123
|
+
stakeId: readString(payload.stake_id, payload.stakeId),
|
|
2124
|
+
owner: readString(payload.owner),
|
|
2125
|
+
cooldownEndsAt: readNumber2(payload.cooldown_ends_at, payload.cooldownEndsAt)
|
|
2126
|
+
};
|
|
2127
|
+
case `${this.options.packageId}::${EVENT_NAMES.anchorPublished}`: {
|
|
2128
|
+
const anchorId = readString(payload.anchor_id, payload.anchorId);
|
|
2129
|
+
const anchor = await this.fetchAnchor(anchorId) ?? {
|
|
2130
|
+
anchorId,
|
|
2131
|
+
author: readString(payload.author),
|
|
2132
|
+
merkleRoot: readHex(payload.merkle_root, payload.merkleRoot),
|
|
2133
|
+
eventCount: readNumber2(payload.event_count, payload.eventCount),
|
|
2134
|
+
blobId: "",
|
|
2135
|
+
fromTimestamp: 0,
|
|
2136
|
+
toTimestamp: 0
|
|
2137
|
+
};
|
|
2138
|
+
return { ...base, type: "reputation.anchor_published", anchor };
|
|
2139
|
+
}
|
|
2140
|
+
default:
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
async getTransactionMetadata(txDigest) {
|
|
2145
|
+
const cached = this.txMetadataCache.get(txDigest);
|
|
2146
|
+
if (cached) {
|
|
2147
|
+
return await cached;
|
|
2148
|
+
}
|
|
2149
|
+
const pending = this.loadTransactionMetadata(txDigest);
|
|
2150
|
+
this.txMetadataCache.set(txDigest, pending);
|
|
2151
|
+
pending.finally(() => {
|
|
2152
|
+
if (this.txMetadataCache.get(txDigest) === pending) {
|
|
2153
|
+
this.txMetadataCache.delete(txDigest);
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
return await pending;
|
|
2157
|
+
}
|
|
2158
|
+
async loadTransactionMetadata(txDigest) {
|
|
2159
|
+
try {
|
|
2160
|
+
const response = await this.options.suiClient.client.getTransactionBlock({
|
|
2161
|
+
digest: txDigest,
|
|
2162
|
+
options: { showEffects: true }
|
|
2163
|
+
});
|
|
2164
|
+
return {
|
|
2165
|
+
checkpoint: response.checkpoint == null ? void 0 : Number(response.checkpoint),
|
|
2166
|
+
gasCostMist: computeGasCost(response)
|
|
2167
|
+
};
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
this.logger.warn({ err: error, txDigest }, "Failed to load transaction metadata.");
|
|
2170
|
+
return { gasCostMist: 0n };
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
async fetchDispute(disputeId) {
|
|
2174
|
+
if (!disputeId) {
|
|
2175
|
+
return void 0;
|
|
2176
|
+
}
|
|
2177
|
+
try {
|
|
2178
|
+
const object = await this.options.suiClient.getObject(disputeId);
|
|
2179
|
+
return parseDisputeObject(object, disputeId);
|
|
2180
|
+
} catch {
|
|
2181
|
+
return void 0;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
async fetchAnchor(anchorId) {
|
|
2185
|
+
if (!anchorId) {
|
|
2186
|
+
return void 0;
|
|
2187
|
+
}
|
|
2188
|
+
try {
|
|
2189
|
+
const object = await this.options.suiClient.getObject(anchorId);
|
|
2190
|
+
return {
|
|
2191
|
+
anchorId,
|
|
2192
|
+
author: readString(object.author),
|
|
2193
|
+
merkleRoot: readHex(object.merkle_root, object.merkleRoot),
|
|
2194
|
+
eventCount: readNumber2(object.event_count, object.eventCount),
|
|
2195
|
+
blobId: readBytes(object.blob_id, object.blobId),
|
|
2196
|
+
fromTimestamp: readNumber2(object.from_timestamp, object.fromTimestamp),
|
|
2197
|
+
toTimestamp: readNumber2(object.to_timestamp, object.toTimestamp)
|
|
2198
|
+
};
|
|
2199
|
+
} catch {
|
|
2200
|
+
return void 0;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
function buildFallbackDispute(payload, disputeId, timestampMs) {
|
|
2205
|
+
return {
|
|
2206
|
+
id: disputeId,
|
|
2207
|
+
taskId: readString(payload.task_id, payload.taskId),
|
|
2208
|
+
requester: readString(payload.requester),
|
|
2209
|
+
provider: readString(payload.provider),
|
|
2210
|
+
escrowAmount: readBigInt(payload.escrow_amount, payload.escrowAmount),
|
|
2211
|
+
status: DisputeStatus3.OPEN,
|
|
2212
|
+
requesterEvidenceBlob: "",
|
|
2213
|
+
providerEvidenceBlob: void 0,
|
|
2214
|
+
requesterProposedSplit: 0n,
|
|
2215
|
+
providerProposedSplit: 0n,
|
|
2216
|
+
arbitrator: void 0,
|
|
2217
|
+
rulingSplit: 0n,
|
|
2218
|
+
openedAt: timestampMs,
|
|
2219
|
+
respondedAt: void 0,
|
|
2220
|
+
resolvedAt: void 0,
|
|
2221
|
+
resolutionDeadline: timestampMs
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
function parseDisputeObject(object, disputeId) {
|
|
2225
|
+
const respondedAt = readNumber2(object.responded_at, object.respondedAt);
|
|
2226
|
+
const resolvedAt = readNumber2(object.resolved_at, object.resolvedAt);
|
|
2227
|
+
return {
|
|
2228
|
+
id: disputeId,
|
|
2229
|
+
taskId: readString(object.task_id, object.taskId),
|
|
2230
|
+
requester: readString(object.requester),
|
|
2231
|
+
provider: readString(object.provider),
|
|
2232
|
+
escrowAmount: readBigInt(object.escrow_amount, object.escrowAmount),
|
|
2233
|
+
status: readNumber2(object.status),
|
|
2234
|
+
requesterEvidenceBlob: readBytes(object.requester_evidence_blob, object.requesterEvidenceBlob),
|
|
2235
|
+
providerEvidenceBlob: readBytes(object.provider_evidence_blob, object.providerEvidenceBlob) || void 0,
|
|
2236
|
+
requesterProposedSplit: readBigInt(object.requester_proposed_split, object.requesterProposedSplit),
|
|
2237
|
+
providerProposedSplit: readBigInt(object.provider_proposed_split, object.providerProposedSplit),
|
|
2238
|
+
arbitrator: readString(object.arbitrator) || void 0,
|
|
2239
|
+
rulingSplit: readBigInt(object.ruling_split, object.rulingSplit),
|
|
2240
|
+
openedAt: readNumber2(object.opened_at, object.openedAt),
|
|
2241
|
+
respondedAt: respondedAt > 0 ? respondedAt : void 0,
|
|
2242
|
+
resolvedAt: resolvedAt > 0 ? resolvedAt : void 0,
|
|
2243
|
+
resolutionDeadline: readNumber2(object.resolution_deadline, object.resolutionDeadline)
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function formatEventId(event) {
|
|
2247
|
+
return `${event.id.txDigest}:${event.id.eventSeq}`;
|
|
2248
|
+
}
|
|
2249
|
+
function computeGasCost(response) {
|
|
2250
|
+
const gasUsed = response.effects?.gasUsed;
|
|
2251
|
+
if (!gasUsed) {
|
|
2252
|
+
return 0n;
|
|
2253
|
+
}
|
|
2254
|
+
const computationCost = readBigInt(gasUsed.computationCost);
|
|
2255
|
+
const storageCost = readBigInt(gasUsed.storageCost);
|
|
2256
|
+
const storageRebate = readBigInt(gasUsed.storageRebate);
|
|
2257
|
+
const nonRefundable = readBigInt(gasUsed.nonRefundableStorageFee);
|
|
2258
|
+
const total = computationCost + storageCost + nonRefundable - storageRebate;
|
|
2259
|
+
return total > 0n ? total : 0n;
|
|
2260
|
+
}
|
|
2261
|
+
function normalizeMoveValue(value) {
|
|
2262
|
+
if (Array.isArray(value)) {
|
|
2263
|
+
return value.map((entry) => normalizeMoveValue(entry));
|
|
2264
|
+
}
|
|
2265
|
+
if (!toRecord(value)) {
|
|
2266
|
+
return value;
|
|
2267
|
+
}
|
|
2268
|
+
if (typeof value.id === "string" && Object.keys(value).length === 1) {
|
|
2269
|
+
return value.id;
|
|
2270
|
+
}
|
|
2271
|
+
if (toRecord(value.fields)) {
|
|
2272
|
+
return normalizeMoveValue(value.fields);
|
|
2273
|
+
}
|
|
2274
|
+
return Object.fromEntries(
|
|
2275
|
+
Object.entries(value).map(([key, entry]) => [key, normalizeMoveValue(entry)])
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
function toRecord(value) {
|
|
2279
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
2280
|
+
}
|
|
2281
|
+
function readString(...values) {
|
|
2282
|
+
const match = values.find((value) => typeof value === "string");
|
|
2283
|
+
return typeof match === "string" ? match : "";
|
|
2284
|
+
}
|
|
2285
|
+
function readBytes(...values) {
|
|
2286
|
+
for (const value of values) {
|
|
2287
|
+
if (typeof value === "string") {
|
|
2288
|
+
return value;
|
|
2289
|
+
}
|
|
2290
|
+
if (value instanceof Uint8Array) {
|
|
2291
|
+
return new TextDecoder().decode(value);
|
|
2292
|
+
}
|
|
2293
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "number")) {
|
|
2294
|
+
return new TextDecoder().decode(new Uint8Array(value));
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return "";
|
|
2298
|
+
}
|
|
2299
|
+
function readHex(...values) {
|
|
2300
|
+
for (const value of values) {
|
|
2301
|
+
if (typeof value === "string" && /^[a-f0-9]+$/i.test(value)) {
|
|
2302
|
+
return value;
|
|
2303
|
+
}
|
|
2304
|
+
if (value instanceof Uint8Array) {
|
|
2305
|
+
return Buffer.from(value).toString("hex");
|
|
2306
|
+
}
|
|
2307
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "number")) {
|
|
2308
|
+
return Buffer.from(value).toString("hex");
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return "";
|
|
2312
|
+
}
|
|
2313
|
+
function readNumber2(...values) {
|
|
2314
|
+
for (const value of values) {
|
|
2315
|
+
if (typeof value === "number") {
|
|
2316
|
+
return value;
|
|
2317
|
+
}
|
|
2318
|
+
if (typeof value === "bigint") {
|
|
2319
|
+
return Number(value);
|
|
2320
|
+
}
|
|
2321
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2322
|
+
return Number(value);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return 0;
|
|
2326
|
+
}
|
|
2327
|
+
function readBigInt(...values) {
|
|
2328
|
+
for (const value of values) {
|
|
2329
|
+
if (typeof value === "bigint") {
|
|
2330
|
+
return value;
|
|
2331
|
+
}
|
|
2332
|
+
if (typeof value === "number") {
|
|
2333
|
+
return BigInt(value);
|
|
2334
|
+
}
|
|
2335
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2336
|
+
return BigInt(value);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return 0n;
|
|
2340
|
+
}
|
|
2341
|
+
function normalizeStakeType(...values) {
|
|
2342
|
+
const value = values.find((entry) => entry !== void 0);
|
|
2343
|
+
if (value === "agent" || value === 0 || value === "0") {
|
|
2344
|
+
return "agent";
|
|
2345
|
+
}
|
|
2346
|
+
if (value === "relay" || value === 1 || value === "1") {
|
|
2347
|
+
return "relay";
|
|
2348
|
+
}
|
|
2349
|
+
return void 0;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/index.ts
|
|
2353
|
+
async function startIndexerService() {
|
|
2354
|
+
const config = loadIndexerConfig();
|
|
2355
|
+
const logger2 = pino2({ name: "@hivemind-os/collective-indexer", level: "info" });
|
|
2356
|
+
await mkdir(dirname(config.sqlitePath), { recursive: true });
|
|
2357
|
+
const store = new IndexerStore(config.sqlitePath);
|
|
2358
|
+
const analytics = new AnalyticsEngine(store);
|
|
2359
|
+
const suiClient = new MeshSuiClient2({
|
|
2360
|
+
rpcUrl: config.rpcUrl,
|
|
2361
|
+
faucetUrl: "",
|
|
2362
|
+
packageId: config.packageId,
|
|
2363
|
+
registryId: ""
|
|
2364
|
+
});
|
|
2365
|
+
const indexer = new MeshIndexer({
|
|
2366
|
+
suiClient,
|
|
2367
|
+
store,
|
|
2368
|
+
packageId: config.packageId,
|
|
2369
|
+
pollIntervalMs: config.pollingIntervalMs,
|
|
2370
|
+
startCheckpoint: config.backfill.fromCheckpoint,
|
|
2371
|
+
logger: logger2
|
|
2372
|
+
});
|
|
2373
|
+
const graphql = createIndexerGraphQLServer({
|
|
2374
|
+
store,
|
|
2375
|
+
analytics,
|
|
2376
|
+
host: config.server.host,
|
|
2377
|
+
port: config.server.port,
|
|
2378
|
+
logger: logger2
|
|
2379
|
+
});
|
|
2380
|
+
await indexer.backfill(config.backfill.fromCheckpoint);
|
|
2381
|
+
indexer.start();
|
|
2382
|
+
const address = await graphql.start();
|
|
2383
|
+
const stop = async () => {
|
|
2384
|
+
await indexer.stop();
|
|
2385
|
+
await graphql.stop();
|
|
2386
|
+
store.close();
|
|
2387
|
+
};
|
|
2388
|
+
return {
|
|
2389
|
+
config,
|
|
2390
|
+
store,
|
|
2391
|
+
analytics,
|
|
2392
|
+
indexer,
|
|
2393
|
+
graphql,
|
|
2394
|
+
address,
|
|
2395
|
+
stop
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2399
|
+
try {
|
|
2400
|
+
const service = await startIndexerService();
|
|
2401
|
+
console.log(service.address);
|
|
2402
|
+
const shutdown = async () => {
|
|
2403
|
+
await service.stop();
|
|
2404
|
+
process.exit(0);
|
|
2405
|
+
};
|
|
2406
|
+
process.once("SIGINT", () => {
|
|
2407
|
+
void shutdown();
|
|
2408
|
+
});
|
|
2409
|
+
process.once("SIGTERM", () => {
|
|
2410
|
+
void shutdown();
|
|
2411
|
+
});
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
console.error(error);
|
|
2414
|
+
process.exitCode = 1;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
export {
|
|
2418
|
+
AnalyticsEngine,
|
|
2419
|
+
IndexerStore,
|
|
2420
|
+
MeshIndexer,
|
|
2421
|
+
SUPPORTED_EVENT_TYPES,
|
|
2422
|
+
createIndexerGraphQLServer,
|
|
2423
|
+
encodeCursor,
|
|
2424
|
+
getDefaultIndexerConfig,
|
|
2425
|
+
loadIndexerConfig,
|
|
2426
|
+
startIndexerService,
|
|
2427
|
+
validateIndexerConfig
|
|
2428
|
+
};
|
|
2429
|
+
//# sourceMappingURL=index.js.map
|