@finchip.ai/foga 0.0.0-stage → 0.6.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +216 -2
- package/artifacts/bundle.json +74250 -0
- package/artifacts/bundle.sha256 +1 -0
- package/bin/foga.js +14 -0
- package/build/known-interface-ids.json +18 -0
- package/build/profiles.json +120 -0
- package/dist/build/bundle.js +417 -0
- package/dist/build/vendor.js +74 -0
- package/dist/bundle/load.js +24 -0
- package/dist/bundle/types.js +1 -0
- package/dist/cli-cell.js +395 -0
- package/dist/cli.js +505 -0
- package/dist/core/acdf.js +162 -0
- package/dist/core/adrf.js +273 -0
- package/dist/core/aid.js +83 -0
- package/dist/core/ard.js +146 -0
- package/dist/core/asf.js +157 -0
- package/dist/core/canonical.js +79 -0
- package/dist/core/erc8004.js +52 -0
- package/dist/core/kya.js +225 -0
- package/dist/core/measure.js +96 -0
- package/dist/core/pack/cbor.js +130 -0
- package/dist/core/pack/skillpack.js +142 -0
- package/dist/core/skills.js +40 -0
- package/dist/core/swarm.js +206 -0
- package/dist/core/tenders.js +121 -0
- package/dist/core/units.js +122 -0
- package/dist/core/world.js +104 -0
- package/dist/demo/demander.js +76 -0
- package/dist/demo/evidence.js +1 -0
- package/dist/demo/loop-d.js +449 -0
- package/dist/doctor/checks.js +51 -0
- package/dist/doctor/doctor.js +385 -0
- package/dist/doctor/loop-d.js +103 -0
- package/dist/doctor/probe.js +97 -0
- package/dist/doctor/semantic.js +83 -0
- package/dist/genesis/extract.js +64 -0
- package/dist/genesis/funding.js +126 -0
- package/dist/genesis/operator-scope.js +90 -0
- package/dist/genesis/preflight.js +54 -0
- package/dist/genesis/probe.js +164 -0
- package/dist/genesis/roles.js +96 -0
- package/dist/genesis/run.js +71 -0
- package/dist/genesis/scenario.js +765 -0
- package/dist/genesis/skill-runner.js +103 -0
- package/dist/genesis/skill-worker.js +41 -0
- package/dist/genesis/t1-zk.js +62 -0
- package/dist/genesis/t3-zk.js +105 -0
- package/dist/genesis/t4.js +259 -0
- package/dist/genesis/t5.js +168 -0
- package/dist/genesis/t6.js +222 -0
- package/dist/genesis/t7.js +198 -0
- package/dist/genesis/t8.js +189 -0
- package/dist/genesis/t9.js +188 -0
- package/dist/genesis/zk.js +128 -0
- package/dist/http/server.js +276 -0
- package/dist/kernel/cell.js +104 -0
- package/dist/kernel/errors.js +78 -0
- package/dist/kernel/kernel.js +251 -0
- package/dist/kernel/provider.js +125 -0
- package/dist/kernel/store.js +470 -0
- package/dist/kernel/tx.js +385 -0
- package/dist/mcp/server.js +170 -0
- package/dist/node/config.js +195 -0
- package/dist/node/init.js +200 -0
- package/dist/node/keys.js +95 -0
- package/dist/node/migrate.js +173 -0
- package/dist/node/model/anthropic.js +97 -0
- package/dist/node/model/stub.js +129 -0
- package/dist/node/model/types.js +9 -0
- package/dist/node/node.js +198 -0
- package/dist/node/policy.js +95 -0
- package/dist/node/runner.js +256 -0
- package/dist/node/watchers.js +88 -0
- package/dist/tools/admin.js +184 -0
- package/dist/tools/catalog.js +261 -0
- package/dist/tools/common.js +143 -0
- package/dist/tools/identity.js +107 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/read.js +471 -0
- package/dist/tools/tenders.js +400 -0
- package/dist/tools/types.js +4 -0
- package/dist/util/bytecode.js +123 -0
- package/dist/util/fs.js +58 -0
- package/dist/util/hash.js +4 -0
- package/dist/util/log.js +11 -0
- package/dist/util/version.js +12 -0
- package/dist/world/deploy.js +277 -0
- package/dist/world/plan.js +93 -0
- package/dist/world/types.js +1 -0
- package/docs/build.md +45 -0
- package/docs/cell-account.md +58 -0
- package/docs/compatibility-matrix.md +38 -0
- package/docs/design-memo.md +113 -0
- package/docs/doctor.md +53 -0
- package/docs/examples/README.md +11 -0
- package/docs/examples/claude-code-mcp.json +13 -0
- package/docs/examples/claude_desktop_config.json +12 -0
- package/docs/examples/cursor-mcp.json +11 -0
- package/docs/genesis.md +312 -0
- package/docs/kernel.md +101 -0
- package/docs/loop-d.md +90 -0
- package/docs/mcp.md +105 -0
- package/docs/node.md +66 -0
- package/docs/version-lock.md +67 -0
- package/docs/world.md +50 -0
- package/docs/zk.md +48 -0
- package/package.json +82 -4
- package/prover/README.md +26 -0
- package/prover/build-predicate.mjs +77 -0
- package/prover/package-lock.json +1837 -0
- package/prover/package.json +15 -0
- package/prover/run.cjs +199 -0
- package/scenarios/fixtures/sample-filing.txt +16 -0
- package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
- package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
- package/scenarios/skills/finch-extract-verify/fields.json +56 -0
- package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
- package/scenarios/skills/foga-probe/LICENSE +2 -0
- package/scenarios/skills/foga-probe/SKILL.md +35 -0
- package/scenarios/skills/foga-probe/manifest.json +16 -0
- package/scenarios/skills/foga-probe/probe.schema.json +40 -0
- package/server.json +79 -0
- package/vendor/LOCK.json +107 -0
- package/vendor/README.md +13 -0
- package/worlds/local.json +34 -0
- package/worlds/sepolia.json +314 -0
- package/zk/build/verification_key.json +174 -0
- package/zk/circuits/kya_predicate_count_v1.circom +231 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { ensureDir } from "../util/fs.js";
|
|
4
|
+
/** Thrown when a key is claimed again with a different request than the one it was claimed with. */
|
|
5
|
+
export class OperationConflict extends Error {
|
|
6
|
+
key;
|
|
7
|
+
storedHash;
|
|
8
|
+
requestHash;
|
|
9
|
+
constructor(key, storedHash, requestHash) {
|
|
10
|
+
super(`operation ${key} was claimed with request ${storedHash} and is now asked for with request ${requestHash}: the same key must carry the same request (use another key for another request)`);
|
|
11
|
+
this.key = key;
|
|
12
|
+
this.storedHash = storedHash;
|
|
13
|
+
this.requestHash = requestHash;
|
|
14
|
+
this.name = "OperationConflict";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* node:sqlite is unflagged since Node 22.13 but Node 22 still tags it experimental and prints a one-line notice
|
|
19
|
+
* on first load. The notice is silenced for that one load only; every other warning is left alone.
|
|
20
|
+
*/
|
|
21
|
+
function loadSqlite() {
|
|
22
|
+
const original = process.emitWarning;
|
|
23
|
+
process.emitWarning = function (warning, ...args) {
|
|
24
|
+
const text = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
25
|
+
const type = typeof args[0] === "string" ? args[0] : args[0]?.type;
|
|
26
|
+
if (type === "ExperimentalWarning" && /sqlite/i.test(text))
|
|
27
|
+
return;
|
|
28
|
+
return original.call(process, warning, ...args);
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
return createRequire(import.meta.url)("node:sqlite");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
process.emitWarning = original;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const SCHEMA = `
|
|
38
|
+
CREATE TABLE IF NOT EXISTS ops (
|
|
39
|
+
key TEXT PRIMARY KEY,
|
|
40
|
+
kind TEXT NOT NULL,
|
|
41
|
+
signer TEXT NOT NULL,
|
|
42
|
+
status TEXT NOT NULL,
|
|
43
|
+
nonce INTEGER,
|
|
44
|
+
tx_hash TEXT,
|
|
45
|
+
block INTEGER,
|
|
46
|
+
result TEXT,
|
|
47
|
+
error TEXT,
|
|
48
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
created_at TEXT NOT NULL,
|
|
50
|
+
updated_at TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value TEXT NOT NULL,
|
|
55
|
+
updated_at TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS journal (
|
|
58
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
at TEXT NOT NULL,
|
|
60
|
+
op_key TEXT,
|
|
61
|
+
event TEXT NOT NULL,
|
|
62
|
+
detail TEXT
|
|
63
|
+
);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
65
|
+
key TEXT PRIMARY KEY,
|
|
66
|
+
value TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
kind TEXT NOT NULL,
|
|
71
|
+
status TEXT NOT NULL,
|
|
72
|
+
caller TEXT,
|
|
73
|
+
cell TEXT,
|
|
74
|
+
input_hash TEXT NOT NULL,
|
|
75
|
+
input TEXT NOT NULL,
|
|
76
|
+
result TEXT,
|
|
77
|
+
error TEXT,
|
|
78
|
+
operations TEXT NOT NULL DEFAULT '[]',
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
updated_at TEXT NOT NULL
|
|
81
|
+
);
|
|
82
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
at TEXT NOT NULL,
|
|
85
|
+
entrance TEXT NOT NULL,
|
|
86
|
+
caller TEXT NOT NULL,
|
|
87
|
+
capability TEXT NOT NULL,
|
|
88
|
+
mode TEXT NOT NULL,
|
|
89
|
+
input_hash TEXT NOT NULL,
|
|
90
|
+
verdict TEXT NOT NULL,
|
|
91
|
+
gate TEXT NOT NULL,
|
|
92
|
+
reason TEXT NOT NULL,
|
|
93
|
+
job_id TEXT,
|
|
94
|
+
operation_id TEXT,
|
|
95
|
+
model TEXT
|
|
96
|
+
);
|
|
97
|
+
`;
|
|
98
|
+
/** Columns added after 0.5.0; an older store gains them on open (SQLite's ADD COLUMN is cheap and keeps the rows). */
|
|
99
|
+
const OPS_COLUMNS = [
|
|
100
|
+
["replacements", "INTEGER NOT NULL DEFAULT 0"],
|
|
101
|
+
["request_hash", "TEXT"],
|
|
102
|
+
["capability", "TEXT"],
|
|
103
|
+
["caller", "TEXT"],
|
|
104
|
+
["cell", "TEXT"],
|
|
105
|
+
["value_wei", "TEXT NOT NULL DEFAULT '0'"],
|
|
106
|
+
["fee_cap_wei", "TEXT NOT NULL DEFAULT '0'"],
|
|
107
|
+
["gas_cost_wei", "TEXT"],
|
|
108
|
+
];
|
|
109
|
+
function now() {
|
|
110
|
+
return new Date().toISOString();
|
|
111
|
+
}
|
|
112
|
+
/** JSON with bigints as decimal strings, so facts and results round-trip through SQLite text. */
|
|
113
|
+
export function toJson(value) {
|
|
114
|
+
return JSON.stringify(value, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
|
|
115
|
+
}
|
|
116
|
+
export class OperationStore {
|
|
117
|
+
db;
|
|
118
|
+
path;
|
|
119
|
+
constructor(db, path) {
|
|
120
|
+
this.db = db;
|
|
121
|
+
this.path = path;
|
|
122
|
+
}
|
|
123
|
+
static open(path) {
|
|
124
|
+
if (path !== ":memory:")
|
|
125
|
+
ensureDir(dirname(path));
|
|
126
|
+
const { DatabaseSync } = loadSqlite();
|
|
127
|
+
const db = new DatabaseSync(path);
|
|
128
|
+
// WAL so a one-shot command (a guardian's suspension, a key minted) can write while the node runs; a busy timeout so
|
|
129
|
+
// the two writers wait for each other instead of failing on SQLITE_BUSY
|
|
130
|
+
if (path !== ":memory:")
|
|
131
|
+
db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA busy_timeout = 5000;");
|
|
132
|
+
db.exec(SCHEMA);
|
|
133
|
+
const have = new Set(db.prepare("PRAGMA table_info(ops)").all().map((r) => String(r.name)));
|
|
134
|
+
for (const [name, decl] of OPS_COLUMNS)
|
|
135
|
+
if (!have.has(name))
|
|
136
|
+
db.exec(`ALTER TABLE ops ADD COLUMN ${name} ${decl}`);
|
|
137
|
+
const jobColumns = new Set(db.prepare("PRAGMA table_info(jobs)").all().map((r) => String(r.name)));
|
|
138
|
+
if (!jobColumns.has("value_wei"))
|
|
139
|
+
db.exec("ALTER TABLE jobs ADD COLUMN value_wei TEXT NOT NULL DEFAULT '0'");
|
|
140
|
+
return new OperationStore(db, path);
|
|
141
|
+
}
|
|
142
|
+
depth = 0;
|
|
143
|
+
/** Run `fn` inside one SQLite transaction (a claim and its reservation land together or not at all); nested calls join the outer one. */
|
|
144
|
+
transaction(fn) {
|
|
145
|
+
if (this.depth > 0)
|
|
146
|
+
return fn();
|
|
147
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
148
|
+
this.depth++;
|
|
149
|
+
try {
|
|
150
|
+
const out = fn();
|
|
151
|
+
this.db.exec("COMMIT");
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
this.db.exec("ROLLBACK");
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
this.depth--;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
close() {
|
|
163
|
+
this.db.close();
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------- meta
|
|
166
|
+
getMeta(key) {
|
|
167
|
+
return this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key)?.value;
|
|
168
|
+
}
|
|
169
|
+
setMeta(key, value) {
|
|
170
|
+
this.db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------- operations
|
|
173
|
+
get(key) {
|
|
174
|
+
const row = this.db.prepare("SELECT * FROM ops WHERE key = ?").get(key);
|
|
175
|
+
return row ? rowToOp(row) : undefined;
|
|
176
|
+
}
|
|
177
|
+
all() {
|
|
178
|
+
return this.db.prepare("SELECT * FROM ops ORDER BY created_at, key").all().map(rowToOp);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Claim a key: record the intent to send, bound to the request when the caller binds it. Idempotent for the same
|
|
182
|
+
* request: an existing row is returned unchanged (and gains the binding if it had none). Another request under
|
|
183
|
+
* the same key is refused with OperationConflict. The claim and its reservation are one transaction.
|
|
184
|
+
*/
|
|
185
|
+
prepare(key, kind, signer, binding = {}) {
|
|
186
|
+
return this.transaction(() => {
|
|
187
|
+
const existing = this.get(key);
|
|
188
|
+
if (existing) {
|
|
189
|
+
if (binding.requestHash && existing.requestHash && existing.requestHash !== binding.requestHash) {
|
|
190
|
+
throw new OperationConflict(key, existing.requestHash, binding.requestHash);
|
|
191
|
+
}
|
|
192
|
+
if (binding.requestHash && !existing.requestHash) {
|
|
193
|
+
this.db
|
|
194
|
+
.prepare("UPDATE ops SET request_hash = ?, capability = COALESCE(capability, ?), caller = COALESCE(caller, ?), cell = COALESCE(cell, ?), updated_at = ? WHERE key = ?")
|
|
195
|
+
.run(binding.requestHash, binding.capability ?? null, binding.caller ?? null, binding.cell ?? null, now(), key);
|
|
196
|
+
return this.get(key);
|
|
197
|
+
}
|
|
198
|
+
return existing;
|
|
199
|
+
}
|
|
200
|
+
const t = now();
|
|
201
|
+
this.db
|
|
202
|
+
.prepare("INSERT INTO ops (key, kind, signer, status, attempts, replacements, request_hash, capability, caller, cell, value_wei, fee_cap_wei, created_at, updated_at) VALUES (?, ?, ?, 'prepared', 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
203
|
+
.run(key, kind, signer, binding.requestHash ?? null, binding.capability ?? null, binding.caller ?? null, binding.cell ?? null, (binding.valueWei ?? 0n).toString(), (binding.feeCapWei ?? 0n).toString(), t, t);
|
|
204
|
+
return this.get(key);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The transaction is signed and its hash known: written before the broadcast. The value it carries replaces the
|
|
209
|
+
* reservation made at the claim (the reservation was an upper bound; from here on the row says what will move).
|
|
210
|
+
*/
|
|
211
|
+
markSent(key, txHash, nonce, valueWei) {
|
|
212
|
+
this.db
|
|
213
|
+
.prepare("UPDATE ops SET status = 'sent', tx_hash = ?, nonce = ?, error = NULL, attempts = attempts + 1, value_wei = COALESCE(?, value_wei), updated_at = ? WHERE key = ?")
|
|
214
|
+
.run(txHash, nonce, valueWei === undefined ? null : valueWei.toString(), now(), key);
|
|
215
|
+
}
|
|
216
|
+
/** A fee replacement was signed for the operation: its new hash, and one more replacement on the count. */
|
|
217
|
+
markReplaced(key, txHash, nonce) {
|
|
218
|
+
this.db.prepare("UPDATE ops SET status = 'sent', tx_hash = ?, nonce = ?, error = NULL, replacements = replacements + 1, updated_at = ? WHERE key = ?").run(txHash, nonce, now(), key);
|
|
219
|
+
}
|
|
220
|
+
/** Fees are cumulative over the operation's attempts: a retried operation's reverted first attempt was paid for too. */
|
|
221
|
+
addedGasCost(key, gasCostWei) {
|
|
222
|
+
if (gasCostWei === undefined)
|
|
223
|
+
return null;
|
|
224
|
+
return ((this.get(key)?.gasCostWei ?? 0n) + gasCostWei).toString();
|
|
225
|
+
}
|
|
226
|
+
markConfirmed(key, txHash, block, result, gasCostWei) {
|
|
227
|
+
this.transaction(() => {
|
|
228
|
+
this.db
|
|
229
|
+
.prepare("UPDATE ops SET status = 'confirmed', tx_hash = ?, block = ?, result = ?, error = NULL, gas_cost_wei = COALESCE(?, gas_cost_wei), updated_at = ? WHERE key = ?")
|
|
230
|
+
.run(txHash, block, toJson(result), this.addedGasCost(key, gasCostWei), now(), key);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
markFailed(key, error, gasCostWei) {
|
|
234
|
+
this.transaction(() => {
|
|
235
|
+
this.db
|
|
236
|
+
.prepare("UPDATE ops SET status = 'failed', error = ?, gas_cost_wei = COALESCE(?, gas_cost_wei), updated_at = ? WHERE key = ?")
|
|
237
|
+
.run(error, this.addedGasCost(key, gasCostWei), now(), key);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/** A failed operation the caller explicitly retries: back to prepared, the failure kept in the journal. */
|
|
241
|
+
markRetry(key) {
|
|
242
|
+
this.db.prepare("UPDATE ops SET status = 'prepared', tx_hash = NULL, nonce = NULL, block = NULL, error = NULL, updated_at = ? WHERE key = ?").run(now(), key);
|
|
243
|
+
}
|
|
244
|
+
/** Operations of a signer that are claimed or in flight (their reservations are still open). */
|
|
245
|
+
openOps(signer) {
|
|
246
|
+
return this.db.prepare("SELECT * FROM ops WHERE signer = ? AND status IN ('prepared', 'sent') ORDER BY created_at, key").all(signer).map(rowToOp);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* What a signer's operations spent since `sinceIso` (value moved plus fees paid, confirmed and reverted alike) and
|
|
250
|
+
* what its open operations still reserve: the two numbers a budget is checked against.
|
|
251
|
+
*/
|
|
252
|
+
budgetUse(signer, sinceIso) {
|
|
253
|
+
let spent = 0n;
|
|
254
|
+
for (const r of this.db.prepare("SELECT value_wei, gas_cost_wei, status FROM ops WHERE signer = ? AND updated_at >= ? AND status IN ('confirmed', 'failed')").all(signer, sinceIso)) {
|
|
255
|
+
if (r.status === "confirmed")
|
|
256
|
+
spent += BigInt(r.value_wei ?? "0");
|
|
257
|
+
spent += BigInt(r.gas_cost_wei ?? "0");
|
|
258
|
+
}
|
|
259
|
+
let reserved = 0n;
|
|
260
|
+
for (const r of this.db.prepare("SELECT value_wei, fee_cap_wei FROM ops WHERE signer = ? AND status IN ('prepared', 'sent')").all(signer)) {
|
|
261
|
+
reserved += BigInt(r.value_wei ?? "0") + BigInt(r.fee_cap_wei ?? "0");
|
|
262
|
+
}
|
|
263
|
+
return { spentWei: spent, reservedWei: reserved };
|
|
264
|
+
}
|
|
265
|
+
/** The same figures kept apart: value moved and fees paid since `sinceIso`, and what open operations reserve of each. */
|
|
266
|
+
budgetBooks(signer, sinceIso) {
|
|
267
|
+
let valueWei = 0n;
|
|
268
|
+
let gasWei = 0n;
|
|
269
|
+
for (const r of this.db.prepare("SELECT value_wei, gas_cost_wei, status FROM ops WHERE signer = ? AND updated_at >= ? AND status IN ('confirmed', 'failed')").all(signer, sinceIso)) {
|
|
270
|
+
if (r.status === "confirmed")
|
|
271
|
+
valueWei += BigInt(r.value_wei ?? "0");
|
|
272
|
+
gasWei += BigInt(r.gas_cost_wei ?? "0");
|
|
273
|
+
}
|
|
274
|
+
let reservedValueWei = 0n;
|
|
275
|
+
let reservedFeeWei = 0n;
|
|
276
|
+
for (const r of this.db.prepare("SELECT value_wei, fee_cap_wei FROM ops WHERE signer = ? AND status IN ('prepared', 'sent')").all(signer)) {
|
|
277
|
+
reservedValueWei += BigInt(r.value_wei ?? "0");
|
|
278
|
+
reservedFeeWei += BigInt(r.fee_cap_wei ?? "0");
|
|
279
|
+
}
|
|
280
|
+
// a job that was allowed but whose operations are not prepared yet reserves what its plan moves (less what its operations already carry)
|
|
281
|
+
for (const j of this.db.prepare("SELECT id, value_wei FROM jobs WHERE status IN ('accepted', 'running') AND value_wei <> '0'").all()) {
|
|
282
|
+
let carried = 0n;
|
|
283
|
+
for (const o of this.db.prepare("SELECT value_wei FROM ops WHERE key LIKE ? AND signer = ?").all(`job:${j.id}:%`, signer))
|
|
284
|
+
carried += BigInt(o.value_wei ?? "0");
|
|
285
|
+
const planned = BigInt(j.value_wei ?? "0");
|
|
286
|
+
if (planned > carried)
|
|
287
|
+
reservedValueWei += planned - carried;
|
|
288
|
+
}
|
|
289
|
+
return { valueWei, gasWei, reservedValueWei, reservedFeeWei };
|
|
290
|
+
}
|
|
291
|
+
/** Jobs of a kind in given states (the running commitments of a cell, for instance). */
|
|
292
|
+
countJobs(kind, statuses) {
|
|
293
|
+
if (!statuses.length)
|
|
294
|
+
return 0;
|
|
295
|
+
const marks = statuses.map(() => "?").join(", ");
|
|
296
|
+
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM jobs WHERE kind = ? AND status IN (${marks})`).get(kind, ...statuses).n);
|
|
297
|
+
}
|
|
298
|
+
// ---------------------------------------------------------------------------- jobs
|
|
299
|
+
/** Create a job, or return the existing one for the id; the same id with another input is a conflict. */
|
|
300
|
+
createJob(p) {
|
|
301
|
+
return this.transaction(() => {
|
|
302
|
+
const existing = this.getJob(p.id);
|
|
303
|
+
if (existing) {
|
|
304
|
+
if (existing.inputHash !== p.inputHash)
|
|
305
|
+
throw new OperationConflict(p.id, existing.inputHash, p.inputHash);
|
|
306
|
+
return existing;
|
|
307
|
+
}
|
|
308
|
+
const t = now();
|
|
309
|
+
this.db
|
|
310
|
+
.prepare("INSERT INTO jobs (id, kind, status, caller, cell, input_hash, input, operations, value_wei, created_at, updated_at) VALUES (?, ?, 'accepted', ?, ?, ?, ?, '[]', ?, ?, ?)")
|
|
311
|
+
.run(p.id, p.kind, p.caller ?? null, p.cell ?? null, p.inputHash, toJson(p.input), (p.valueWei ?? 0n).toString(), t, t);
|
|
312
|
+
return this.getJob(p.id);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
getJob(id) {
|
|
316
|
+
const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(id);
|
|
317
|
+
return row ? rowToJob(row) : undefined;
|
|
318
|
+
}
|
|
319
|
+
updateJob(id, patch) {
|
|
320
|
+
return this.transaction(() => {
|
|
321
|
+
const job = this.getJob(id);
|
|
322
|
+
if (!job)
|
|
323
|
+
throw new Error(`no job ${id}`);
|
|
324
|
+
const operations = patch.addOperation && !job.operations.includes(patch.addOperation) ? [...job.operations, patch.addOperation] : job.operations;
|
|
325
|
+
this.db
|
|
326
|
+
.prepare("UPDATE jobs SET status = ?, result = ?, error = ?, operations = ?, updated_at = ? WHERE id = ?")
|
|
327
|
+
.run(patch.status ?? job.status, patch.result === undefined ? (job.result === null ? null : toJson(job.result)) : toJson(patch.result), patch.error === undefined ? job.error : patch.error, JSON.stringify(operations), now(), id);
|
|
328
|
+
return this.getJob(id);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/** Jobs visible to a caller (its own), newest first; `caller` null lists every job (the operator's view). */
|
|
332
|
+
jobs(caller, limit = 50) {
|
|
333
|
+
const rows = caller === null
|
|
334
|
+
? this.db.prepare("SELECT * FROM jobs ORDER BY created_at DESC LIMIT ?").all(limit)
|
|
335
|
+
: this.db.prepare("SELECT * FROM jobs WHERE caller = ? ORDER BY created_at DESC LIMIT ?").all(caller, limit);
|
|
336
|
+
return rows.map(rowToJob);
|
|
337
|
+
}
|
|
338
|
+
// ---------------------------------------------------------------------------- decisions
|
|
339
|
+
/** Write a decision record; returns its id. */
|
|
340
|
+
decide(d) {
|
|
341
|
+
this.db
|
|
342
|
+
.prepare("INSERT INTO decisions (at, entrance, caller, capability, mode, input_hash, verdict, gate, reason, job_id, operation_id, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
343
|
+
.run(now(), d.entrance, d.caller, d.capability, d.mode, d.inputHash, d.verdict, d.gate, d.reason, d.jobId ?? null, d.operationId ?? null, d.model ? toJson(d.model) : null);
|
|
344
|
+
const row = this.db.prepare("SELECT * FROM decisions ORDER BY id DESC LIMIT 1").get();
|
|
345
|
+
return rowToDecision(row);
|
|
346
|
+
}
|
|
347
|
+
/** Decision records, oldest first; `caller` restricts to one caller's own (a caller sees its own attempts, the operator every one). */
|
|
348
|
+
decisions(filter = {}) {
|
|
349
|
+
const where = [];
|
|
350
|
+
const args = [];
|
|
351
|
+
if (filter.caller) {
|
|
352
|
+
where.push("caller = ?");
|
|
353
|
+
args.push(filter.caller);
|
|
354
|
+
}
|
|
355
|
+
if (filter.capability) {
|
|
356
|
+
where.push("capability = ?");
|
|
357
|
+
args.push(filter.capability);
|
|
358
|
+
}
|
|
359
|
+
if (filter.verdict) {
|
|
360
|
+
where.push("verdict = ?");
|
|
361
|
+
args.push(filter.verdict);
|
|
362
|
+
}
|
|
363
|
+
if (filter.since) {
|
|
364
|
+
where.push("at >= ?");
|
|
365
|
+
args.push(filter.since);
|
|
366
|
+
}
|
|
367
|
+
const sql = `SELECT * FROM decisions${where.length ? " WHERE " + where.join(" AND ") : ""} ORDER BY id LIMIT ?`;
|
|
368
|
+
return this.db.prepare(sql).all(...args, filter.limit ?? 1000).map(rowToDecision);
|
|
369
|
+
}
|
|
370
|
+
// ---------------------------------------------------------------------------- facts
|
|
371
|
+
/** Scenario-level values (ids, addresses, hashes) that later steps and a resumed run read back. */
|
|
372
|
+
setFact(key, value) {
|
|
373
|
+
this.db.prepare("INSERT INTO facts (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at").run(key, toJson(value), now());
|
|
374
|
+
}
|
|
375
|
+
getFact(key) {
|
|
376
|
+
const row = this.db.prepare("SELECT value FROM facts WHERE key = ?").get(key);
|
|
377
|
+
return row ? JSON.parse(row.value) : undefined;
|
|
378
|
+
}
|
|
379
|
+
requireFact(key) {
|
|
380
|
+
const v = this.getFact(key);
|
|
381
|
+
if (v === undefined)
|
|
382
|
+
throw new Error(`operation store has no fact ${key}`);
|
|
383
|
+
return v;
|
|
384
|
+
}
|
|
385
|
+
facts(prefix = "") {
|
|
386
|
+
const out = {};
|
|
387
|
+
for (const row of this.db.prepare("SELECT key, value FROM facts WHERE key LIKE ? ORDER BY key").all(prefix + "%"))
|
|
388
|
+
out[row.key] = JSON.parse(row.value);
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
// ---------------------------------------------------------------------------- journal
|
|
392
|
+
journal(event, detail, opKey) {
|
|
393
|
+
this.db.prepare("INSERT INTO journal (at, op_key, event, detail) VALUES (?, ?, ?, ?)").run(now(), opKey ?? null, event, detail ? toJson(detail) : null);
|
|
394
|
+
return Number(this.db.prepare("SELECT seq FROM journal ORDER BY seq DESC LIMIT 1").get().seq);
|
|
395
|
+
}
|
|
396
|
+
journalEntries() {
|
|
397
|
+
return this.db.prepare("SELECT * FROM journal ORDER BY seq").all().map((r) => ({ seq: Number(r.seq), at: r.at, opKey: r.op_key ?? null, event: r.event, detail: r.detail ? JSON.parse(r.detail) : null }));
|
|
398
|
+
}
|
|
399
|
+
// ---------------------------------------------------------------------------- summary
|
|
400
|
+
summary() {
|
|
401
|
+
const ops = { prepared: 0, sent: 0, confirmed: 0, failed: 0 };
|
|
402
|
+
for (const r of this.db.prepare("SELECT status, COUNT(*) AS n FROM ops GROUP BY status").all())
|
|
403
|
+
ops[r.status] = Number(r.n);
|
|
404
|
+
const facts = Number(this.db.prepare("SELECT COUNT(*) AS n FROM facts").get().n);
|
|
405
|
+
const journal = Number(this.db.prepare("SELECT COUNT(*) AS n FROM journal").get().n);
|
|
406
|
+
const jobs = Number(this.db.prepare("SELECT COUNT(*) AS n FROM jobs").get().n);
|
|
407
|
+
const decisions = { allowed: 0, refused: 0 };
|
|
408
|
+
for (const r of this.db.prepare("SELECT verdict, COUNT(*) AS n FROM decisions GROUP BY verdict").all())
|
|
409
|
+
decisions[r.verdict] = Number(r.n);
|
|
410
|
+
return { ops, facts, journal, jobs, decisions };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function rowToOp(row) {
|
|
414
|
+
return {
|
|
415
|
+
key: row.key,
|
|
416
|
+
kind: row.kind,
|
|
417
|
+
signer: row.signer,
|
|
418
|
+
status: row.status,
|
|
419
|
+
nonce: row.nonce === null || row.nonce === undefined ? null : Number(row.nonce),
|
|
420
|
+
txHash: row.tx_hash ?? null,
|
|
421
|
+
block: row.block === null || row.block === undefined ? null : Number(row.block),
|
|
422
|
+
result: row.result ? JSON.parse(row.result) : null,
|
|
423
|
+
error: row.error ?? null,
|
|
424
|
+
attempts: Number(row.attempts ?? 0),
|
|
425
|
+
replacements: Number(row.replacements ?? 0),
|
|
426
|
+
requestHash: row.request_hash ?? null,
|
|
427
|
+
capability: row.capability ?? null,
|
|
428
|
+
caller: row.caller ?? null,
|
|
429
|
+
cell: row.cell ?? null,
|
|
430
|
+
valueWei: BigInt(row.value_wei ?? "0"),
|
|
431
|
+
feeCapWei: BigInt(row.fee_cap_wei ?? "0"),
|
|
432
|
+
gasCostWei: row.gas_cost_wei === null || row.gas_cost_wei === undefined ? null : BigInt(row.gas_cost_wei),
|
|
433
|
+
createdAt: row.created_at,
|
|
434
|
+
updatedAt: row.updated_at,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function rowToJob(row) {
|
|
438
|
+
return {
|
|
439
|
+
id: row.id,
|
|
440
|
+
kind: row.kind,
|
|
441
|
+
status: row.status,
|
|
442
|
+
caller: row.caller ?? null,
|
|
443
|
+
cell: row.cell ?? null,
|
|
444
|
+
inputHash: row.input_hash,
|
|
445
|
+
input: row.input ? JSON.parse(row.input) : null,
|
|
446
|
+
result: row.result ? JSON.parse(row.result) : null,
|
|
447
|
+
error: row.error ?? null,
|
|
448
|
+
operations: row.operations ? JSON.parse(row.operations) : [],
|
|
449
|
+
valueWei: BigInt(row.value_wei ?? "0"),
|
|
450
|
+
createdAt: row.created_at,
|
|
451
|
+
updatedAt: row.updated_at,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function rowToDecision(row) {
|
|
455
|
+
return {
|
|
456
|
+
id: Number(row.id),
|
|
457
|
+
at: row.at,
|
|
458
|
+
entrance: row.entrance,
|
|
459
|
+
caller: row.caller,
|
|
460
|
+
capability: row.capability,
|
|
461
|
+
mode: row.mode,
|
|
462
|
+
inputHash: row.input_hash,
|
|
463
|
+
verdict: row.verdict,
|
|
464
|
+
gate: row.gate,
|
|
465
|
+
reason: row.reason,
|
|
466
|
+
jobId: row.job_id ?? null,
|
|
467
|
+
operationId: row.operation_id ?? null,
|
|
468
|
+
model: row.model ? JSON.parse(row.model) : null,
|
|
469
|
+
};
|
|
470
|
+
}
|