@taifoon/jev 0.2.15 → 0.2.17
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/README.md +41 -7
- package/bin/jev.mjs +25 -7
- package/dist/addresses.d.ts +11 -0
- package/dist/addresses.js +8 -0
- package/dist/chains.d.ts +18 -0
- package/dist/chains.js +31 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +5 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/rpc-fallback.d.ts +4 -0
- package/dist/rpc-fallback.js +36 -0
- package/dist/rpc.d.ts +17 -0
- package/dist/rpc.js +87 -0
- package/dist/verify.d.ts +25 -12
- package/dist/verify.js +55 -52
- package/docs/ON_CHAIN.md +2 -2
- package/docs/PIPELINE.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
delivery, and code turns the answers into complete, reject or needs review, with a receipt anyone can recompute.
|
|
7
7
|
- **Why:** agents are paid on chain for work nobody reads. A grade with its full probabilities, a published rubric and
|
|
8
8
|
an appeal path is something a buyer, a seller and a contract can all rely on.
|
|
9
|
-
- **How:** replay a
|
|
9
|
+
- **How:** replay a Base job, graded by Jev, offline and with no key:
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
npx @taifoon/jev run --demo
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-

|
|
16
16
|
|
|
17
17
|
Then grade your own with your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai). Installing it also
|
|
18
18
|
installs Taifoon's other packages (@taifoon/jev-wilson, @taifoon/n8n-nodes-typesafe). No key inside, and it signs nothing. Recording a grade on chain is optional.
|
|
@@ -54,11 +54,11 @@ claims the evidence cannot support. Grade one with your key:
|
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
TYPESAFE_KEY=… npx @taifoon/jev run --job-file examples/jobs/invoice-extraction.json
|
|
57
|
-
npx @taifoon/jev run --demo # offline, no key: replays the
|
|
57
|
+
npx @taifoon/jev run --demo # offline, no key: replays the Base job below and checks it against the chain
|
|
58
58
|
npx @taifoon/jev run # a live Base job from the coordination layer's queue (needs TYPESAFE_KEY)
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
## A
|
|
61
|
+
## A Base job, graded
|
|
62
62
|
|
|
63
63
|
[BitAgent](https://basescan.org/address/0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7) job 7287 on Base: a buyer paid
|
|
64
64
|
1.5 USDC for `equity_research where ticker is 'AAPL'`. The seller submitted within seconds, but only a 32-byte digest
|
|
@@ -109,7 +109,7 @@ Safe by default: your own TypeSafe key only, nothing recorded, nothing signed or
|
|
|
109
109
|
|
|
110
110
|
| Flag | Default | What it does |
|
|
111
111
|
|---|---|---|
|
|
112
|
-
| `--demo` | off | Replays the
|
|
112
|
+
| `--demo` | off | Replays the Base job above, offline, no key |
|
|
113
113
|
| `--job <chain>:<id>` | first ready job in the layer's queue | Grade one live job, e.g. `8453:bitagent:8453:7287` |
|
|
114
114
|
| `--job-file job.json` | | Grade a job you describe (see `examples/jobs/`) |
|
|
115
115
|
| `--evidence pack.json` | | Grade your own evidence pack, no layer |
|
|
@@ -146,7 +146,7 @@ code checks reject is still graded). Exit codes: `0` done, `1` a step failed, `2
|
|
|
146
146
|
| `facts({ delivered, checks, priceUsdc? })` | Runs the checks your protocol supplies |
|
|
147
147
|
| `record(receipt, { network, send? })` | Returns the unsigned `JevAnswerLog.record` and `JevDecisionLog.record` calls |
|
|
148
148
|
| `evaluatorCall(protocol, jobId, verdict, digest)` | Returns the one unsigned call that ends the job; `null` for needs_review |
|
|
149
|
-
| `verify(receiptOrDigest)` | Recomputes every digest and finds the records on chain |
|
|
149
|
+
| `verify(receiptOrDigest, { network? })` | Recomputes every digest and finds the records on chain (the devnet unless you name a chain; see Chains) |
|
|
150
150
|
|
|
151
151
|
`verifyDecision(record, { answers? })` does the same for a decision as the coordination layer serves it at
|
|
152
152
|
`/v1/judge/decisions/{id}`: the input fingerprint, the decision digest, the answers digest and the verdict RUBRIC_v1
|
|
@@ -162,7 +162,7 @@ Each is tested against calldata a mined transaction carried.
|
|
|
162
162
|
- [Selling through Taifoon: readiness, the signed enrich route, A2A and x402 shapes, a first graded job, Monad](docs/SELLERS.md)
|
|
163
163
|
- [API: the six calls, where Jev is reached, your own rubric](docs/API.md)
|
|
164
164
|
- [Evaluator seats, with the transactions they are tested against](docs/EVALUATORS.md)
|
|
165
|
-
- [On chain: contract addresses on Base and devnet, and one
|
|
165
|
+
- [On chain: contract addresses on Base and devnet, and one grade end to end](docs/ON_CHAIN.md)
|
|
166
166
|
- [The records: receipt, decision.v2, jev.answer.v1](docs/RECORDS.md)
|
|
167
167
|
- [n8n workflows, and the TypeSafe node's Jev Options](docs/WORKFLOWS.md)
|
|
168
168
|
- [Develop](docs/DEVELOP.md)
|
|
@@ -173,6 +173,40 @@ npx @taifoon/jev verify <decision id> --network base # recompute the served re
|
|
|
173
173
|
npx @taifoon/jev workflows export ./jev-workflows
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
## Chains
|
|
177
|
+
|
|
178
|
+
`verify` reads the Jev logs on every chain they are deployed to: the Taifoon devnet (36927), Base (8453), Arbitrum One
|
|
179
|
+
(42161), Arc (5042) and Monad (143). Robinhood Chain (4663), Taifoon mainnet (3692781) and Ethereum (1) are known
|
|
180
|
+
chains with no Jev log yet; `verify` says so instead of reading them.
|
|
181
|
+
|
|
182
|
+
| Flag | What it does |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `--chain <id or name>` | Read one chain: `devnet`, `base`, `arbitrum`, `arc`, `monad`, `robinhood`, `taifoon`, or a chain id |
|
|
185
|
+
| `--network devnet\|base\|any` | The earlier flag; still works. `any` is the same as naming no chain |
|
|
186
|
+
| `--rpc <url[,url]>` | Read through these endpoints instead of the rotation below |
|
|
187
|
+
|
|
188
|
+
With no chain named, a decision id is read on the chain its record names (its anchor); a bare digest is looked up on
|
|
189
|
+
the devnet first, then Base, then the other chains, and the first chain that holds it answers. Nothing reads a mainnet
|
|
190
|
+
unless the record or you name one: the tests and examples use the devnet.
|
|
191
|
+
|
|
192
|
+
Every chain read goes through the warmbed rotation (`https://warmbed.taifoon.dev`), which lists the endpoints it
|
|
193
|
+
measured to serve log windows for that chain. A rate limit, a server error or a timeout moves the read to the next
|
|
194
|
+
endpoint, two passes over the list; a revert does not. When warmbed does not answer, the package uses the static list
|
|
195
|
+
it ships (`src/rpc-fallback.ts`, generated from the RPC registry).
|
|
196
|
+
|
|
197
|
+
`onChain.status` in the output tells the outcomes apart:
|
|
198
|
+
|
|
199
|
+
| `status` | Meaning |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `recorded` | The answer row is on chain (and the decision row, when there is one: `checks.decisionOnChain`) |
|
|
202
|
+
| `not_recorded` | The chain answered and holds no row for this digest |
|
|
203
|
+
| `rpc_unavailable` | No endpoint answered; the row may exist. Try again later, or pass `--rpc` |
|
|
204
|
+
| `not_deployed` | No Jev log on that chain |
|
|
205
|
+
| `skipped` | `verify(x, { chain: false })`: digests recomputed, no chain read |
|
|
206
|
+
|
|
207
|
+
In code: `verify(digest, { network: 'arbitrum' })` (a name or a chain id), `{ rpc: 'https://…' }`, and `JevChainError`
|
|
208
|
+
(`code`: `rpc_unavailable`, `not_deployed`, `reverted`, `bad_input`) from the lower-level calls.
|
|
209
|
+
|
|
176
210
|
## Licence
|
|
177
211
|
|
|
178
212
|
Independent project. Jev and TypeSafe are products of TypeSafe AI, Inc., which does not endorse this package.
|
package/bin/jev.mjs
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// jev workflows list [--version X] the bundled n8n workflows (and which nodes and credentials each needs)
|
|
4
4
|
// jev workflows export <dir> [--version X] write them, their schemas, manifest and README into <dir>
|
|
5
5
|
// jev run [--job 8453:81100] [--evidence pack.json] [--network …] [--yes] [--json] one job through the pipeline, step by step
|
|
6
|
-
// jev verify <answers-digest | decision id> [--
|
|
6
|
+
// jev verify <answers-digest | decision id> [--chain <id|name>] [--rpc url[,url]] (a decision id: recompute its record offline, then) find the digest's
|
|
7
|
+
// JevAnswered / Decided rows: on --chain, else the chain the decision record names, else every chain with Jev logs (devnet,
|
|
8
|
+
// Base, then the rest). Chain reads go through warmbed's rotation, then the registry's static list; --rpc overrides both.
|
|
9
|
+
// --network devnet|base|any still works (any = no chain named).
|
|
7
10
|
import { cpSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
8
11
|
import { dirname, join, resolve } from 'node:path';
|
|
9
12
|
import { fileURLToPath } from 'node:url';
|
|
@@ -16,6 +19,9 @@ const flag = (n) => { const i = args.indexOf(n); return i >= 0 ? args.splice(i,
|
|
|
16
19
|
const BUNDLES = readdirSync(join(ROOT, 'workflows')).filter((d) => /^\d+\.\d+\.\d+$/.test(d)).sort((x, y) => x.localeCompare(y, undefined, { numeric: true }));
|
|
17
20
|
const version = flag('--version') ?? BUNDLES.at(-1) ?? PKG.version;
|
|
18
21
|
const network = flag('--network') ?? 'any';
|
|
22
|
+
// --rpc <url[,url…]>: read through these endpoints instead of warmbed's rotation (the named or anchored chain; else Base)
|
|
23
|
+
const rpcFlag = flag('--rpc');
|
|
24
|
+
const chainFlag = flag('--chain');
|
|
19
25
|
const bundle = join(ROOT, 'workflows', version);
|
|
20
26
|
const die = (m) => { console.error(m); process.exit(1); };
|
|
21
27
|
const [cmd, sub, target] = args;
|
|
@@ -34,9 +40,12 @@ if (cmd === 'workflows') {
|
|
|
34
40
|
} else if (cmd === 'run') {
|
|
35
41
|
await import('./run.mjs').then((m) => m.run(process.argv.slice(3)));
|
|
36
42
|
} else if (cmd === 'verify' && sub) {
|
|
37
|
-
const { verify, verifyDecision, LAYER } = await import('../dist/index.js');
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const { verify, verifyDecision, LAYER, chainIdOf, JEV_CHAINS } = await import('../dist/index.js');
|
|
44
|
+
// which chain(s) to read: --chain <id|name> (or --network devnet|base|<chain>) names one; otherwise the chain the decision
|
|
45
|
+
// record names (its anchor); otherwise every chain with Jev logs in turn (devnet, Base, then the rest) until one holds it
|
|
46
|
+
let named = null;
|
|
47
|
+
try { named = chainFlag ? chainIdOf(chainFlag) : network !== 'any' ? chainIdOf(network) : null; } catch (e) { die(e.message); }
|
|
48
|
+
let digest = sub; let offline = null; let anchor = null;
|
|
40
49
|
// a decision id (or its /v1/judge/decisions URL): read the served record and its answers, recompute both here, then
|
|
41
50
|
// find the answers digest on chain like any other
|
|
42
51
|
const m = /(decision-\d+-[0-9a-f]{10})/.exec(sub);
|
|
@@ -48,11 +57,20 @@ if (cmd === 'workflows') {
|
|
|
48
57
|
offline = verifyDecision(rec, ans?.record ? { answers: ans.record } : {});
|
|
49
58
|
if (!ad) { console.log(JSON.stringify({ offline }, null, 1)); process.exit(offline.ok ? 0 : 2); }
|
|
50
59
|
digest = ad;
|
|
60
|
+
const a = rec?.decision?.anchor;
|
|
61
|
+
if (a && a.status === 'ok' && Number.isInteger(Number(a.chain))) anchor = { chain: Number(a.chain), block: Number(a.block) || undefined };
|
|
62
|
+
}
|
|
63
|
+
const chains = named ? [named] : anchor ? [anchor.chain] : JEV_CHAINS;
|
|
64
|
+
// --rpc reads one chain: the named or anchored one, else Base (the endpoints a reader passes are almost always Base's)
|
|
65
|
+
const rpcFor = (c) => (rpcFlag && (chains.length === 1 || c === 8453) ? { rpc: rpcFlag } : {});
|
|
66
|
+
let v = null;
|
|
67
|
+
for (const c of chains) {
|
|
68
|
+
const r = await verify(digest, { network: c, ...rpcFor(c), ...(anchor && anchor.chain === c && anchor.block ? { decisionBlock: anchor.block } : {}) });
|
|
69
|
+
if (!v || r.checks.answersOnChain || (c === 8453 && !v.ok)) v = r;
|
|
70
|
+
if (r.checks.answersOnChain) break;
|
|
51
71
|
}
|
|
52
|
-
let v = await verify(digest, { network: network === 'base' ? 'base' : 'devnet' });
|
|
53
|
-
if (network === 'any' && !v.checks.answersOnChain) { const b = await verify(sub, { network: 'base' }); if (b.checks.answersOnChain || !v.ok) v = b; }
|
|
54
72
|
console.log(JSON.stringify(offline ? { offline, ...v, ok: v.ok && offline.ok } : v, null, 1));
|
|
55
73
|
process.exit(v.ok && (!offline || offline.ok) ? 0 : 2);
|
|
56
74
|
} else {
|
|
57
|
-
console.log(`jev ${PKG.version}\n jev workflows list [--version X]\n jev workflows export <dir> [--version X]\n jev run [--job <chain>:<id>] [--evidence pack.json] [--network none|devnet|base|both] [--yes] [--json]\n jev verify <answers-digest | decision id> [--
|
|
75
|
+
console.log(`jev ${PKG.version}\n jev workflows list [--version X]\n jev workflows export <dir> [--version X]\n jev run [--job <chain>:<id>] [--evidence pack.json] [--network none|devnet|base|both] [--yes] [--json]\n jev verify <answers-digest | decision id> [--chain <id|name>] [--rpc url[,url]]`);
|
|
58
76
|
}
|
package/dist/addresses.d.ts
CHANGED
|
@@ -60,3 +60,14 @@ export declare const REGISTRY_ADDRESSES: {
|
|
|
60
60
|
readonly block: 48395723;
|
|
61
61
|
};
|
|
62
62
|
};
|
|
63
|
+
/** The Jev logs on every chain the registry deploys them to (JevAnswerLog + JevDecisionLog, from their deploy block). */
|
|
64
|
+
export declare const JEV_LOGS: Readonly<Record<number, {
|
|
65
|
+
answerLog: {
|
|
66
|
+
address: string;
|
|
67
|
+
fromBlock: number;
|
|
68
|
+
};
|
|
69
|
+
decisionLog: {
|
|
70
|
+
address: string;
|
|
71
|
+
fromBlock: number;
|
|
72
|
+
};
|
|
73
|
+
}>>;
|
package/dist/addresses.js
CHANGED
|
@@ -22,3 +22,11 @@ export const REGISTRY_ADDRESSES = {
|
|
|
22
22
|
/** standards-erc8183 · BitAgent ERC-8183 escrow (live) */
|
|
23
23
|
bitagentErc8183: { chainId: 8453, address: '0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7', block: 48395723 },
|
|
24
24
|
};
|
|
25
|
+
/** The Jev logs on every chain the registry deploys them to (JevAnswerLog + JevDecisionLog, from their deploy block). */
|
|
26
|
+
export const JEV_LOGS = {
|
|
27
|
+
143: { answerLog: { address: '0x29fEEc116e0CAbbaf38e9EBF2BBf65d82D672104', fromBlock: 110216088 }, decisionLog: { address: '0x1aC6266b9926cB3E206D5dc0c9d1B7c873aa150a', fromBlock: 110216093 } },
|
|
28
|
+
5042: { answerLog: { address: '0x45214361A8cADb8aEe6302cd5653E119E5a89839', fromBlock: 24071071 }, decisionLog: { address: '0x863db7dAc69CC614a98822082cbe884F1f3b55cA', fromBlock: 24071074 } },
|
|
29
|
+
8453: { answerLog: { address: '0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d', fromBlock: 51863008 }, decisionLog: { address: '0x209490d6A0FFC5368A42b0c2208BDCda853f6a92', fromBlock: 51856200 } },
|
|
30
|
+
36927: { answerLog: { address: '0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3', fromBlock: 1106754 }, decisionLog: { address: '0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05', fromBlock: 981099 } },
|
|
31
|
+
42161: { answerLog: { address: '0x29fEEc116e0CAbbaf38e9EBF2BBf65d82D672104', fromBlock: 511340178 }, decisionLog: { address: '0x1aC6266b9926cB3E206D5dc0c9d1B7c873aa150a', fromBlock: 511340188 } },
|
|
32
|
+
};
|
package/dist/chains.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const DEVNET = 36927;
|
|
2
|
+
export declare const CHAIN_NAMES: Readonly<Record<number, string>>;
|
|
3
|
+
export declare const chainName: (id: number) => string;
|
|
4
|
+
/** A chain id from an id ("8453"), a name ("base", "Arbitrum One") or a number. Throws JevChainError bad_input. */
|
|
5
|
+
export declare function chainIdOf(x: string | number): number;
|
|
6
|
+
/** The Jev logs of a chain, or null when none is deployed there. */
|
|
7
|
+
export declare const jevLogsOn: (chainId: number) => {
|
|
8
|
+
answerLog: {
|
|
9
|
+
address: string;
|
|
10
|
+
fromBlock: number;
|
|
11
|
+
};
|
|
12
|
+
decisionLog: {
|
|
13
|
+
address: string;
|
|
14
|
+
fromBlock: number;
|
|
15
|
+
};
|
|
16
|
+
} | null;
|
|
17
|
+
/** Every chain that carries Jev logs, the devnet first, then Base, then the rest by id. */
|
|
18
|
+
export declare const JEV_CHAINS: readonly number[];
|
package/dist/chains.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The chains verify() knows: every chain the coordination layer reads, by id or name, and the Jev logs on each
|
|
2
|
+
// (vendored from the address registry: src/addresses.ts JEV_LOGS). A chain without Jev logs is known but has nothing to verify.
|
|
3
|
+
import { JEV_LOGS } from './addresses.js';
|
|
4
|
+
import { JevChainError } from './errors.js';
|
|
5
|
+
export const DEVNET = 36927;
|
|
6
|
+
export const CHAIN_NAMES = {
|
|
7
|
+
36927: 'Taifoon devnet', 8453: 'Base', 42161: 'Arbitrum One', 5042: 'Arc', 4663: 'Robinhood Chain', 143: 'Monad',
|
|
8
|
+
3692781: 'Taifoon mainnet', 1: 'Ethereum',
|
|
9
|
+
};
|
|
10
|
+
const ALIASES = {
|
|
11
|
+
devnet: 36927, 'taifoon-devnet': 36927, base: 8453, arbitrum: 42161, 'arbitrum-one': 42161, arb: 42161, arc: 5042,
|
|
12
|
+
robinhood: 4663, 'robinhood-chain': 4663, monad: 143, taifoon: 3692781, 'taifoon-mainnet': 3692781, ethereum: 1, eth: 1,
|
|
13
|
+
};
|
|
14
|
+
export const chainName = (id) => `${CHAIN_NAMES[id] ?? 'chain'} (${id})`;
|
|
15
|
+
/** A chain id from an id ("8453"), a name ("base", "Arbitrum One") or a number. Throws JevChainError bad_input. */
|
|
16
|
+
export function chainIdOf(x) {
|
|
17
|
+
if (typeof x === 'number' && Number.isInteger(x) && x > 0)
|
|
18
|
+
return x;
|
|
19
|
+
const s = String(x).trim().toLowerCase().replace(/\s+/g, '-');
|
|
20
|
+
if (/^\d+$/.test(s))
|
|
21
|
+
return Number(s);
|
|
22
|
+
const id = ALIASES[s];
|
|
23
|
+
if (!id)
|
|
24
|
+
throw new JevChainError('bad_input', `unknown chain "${x}"; use an id or one of: ${Object.keys(ALIASES).join(', ')}`);
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
/** The Jev logs of a chain, or null when none is deployed there. */
|
|
28
|
+
export const jevLogsOn = (chainId) => JEV_LOGS[chainId] ?? null;
|
|
29
|
+
/** Every chain that carries Jev logs, the devnet first, then Base, then the rest by id. */
|
|
30
|
+
export const JEV_CHAINS = Object.keys(JEV_LOGS).map(Number)
|
|
31
|
+
.sort((a, b) => (a === DEVNET ? -1 : b === DEVNET ? 1 : a === 8453 ? -1 : b === 8453 ? 1 : a - b));
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a chain read did not give an answer. `rpc_unavailable`: every endpoint failed (rate limit, 5xx, timeout); the record
|
|
3
|
+
* may well exist, try again or pass --rpc. `not_deployed`: no Jev log on that chain. `reverted`: the call reverted.
|
|
4
|
+
* `bad_input`: an argument verify() cannot use. "Not recorded" is not an error: verify() reports it in onChain.status.
|
|
5
|
+
*/
|
|
6
|
+
export type JevChainErrorCode = 'rpc_unavailable' | 'not_deployed' | 'reverted' | 'bad_input';
|
|
7
|
+
export declare class JevChainError extends Error {
|
|
8
|
+
readonly code: JevChainErrorCode;
|
|
9
|
+
readonly chainId?: number;
|
|
10
|
+
constructor(code: JevChainErrorCode, message: string, chainId?: number);
|
|
11
|
+
}
|
package/dist/errors.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export { prepareJob, runCheck } from './job.js';
|
|
|
20
20
|
export { record } from './record.js';
|
|
21
21
|
export { evaluatorCall } from './evaluator/index.js';
|
|
22
22
|
export { verify } from './verify.js';
|
|
23
|
+
export { JevChainError, type JevChainErrorCode } from './errors.js';
|
|
24
|
+
export { CHAIN_NAMES, JEV_CHAINS, chainIdOf, chainName, jevLogsOn } from './chains.js';
|
|
25
|
+
export { rpcRotation, WARMBED } from './rpc.js';
|
|
23
26
|
export { verifyDecision } from './decision.js';
|
|
24
27
|
export { pipeline, STEPS, LAYER, factsFromEvidence, protocolFor, parseJob, localQuote } from './pipeline.js';
|
|
25
28
|
export { RUBRIC_v1 } from './rubric.js';
|
|
@@ -28,7 +31,7 @@ export type { GradeInput, Receipt } from './grade.js';
|
|
|
28
31
|
export type { FactsInput, Check } from './facts.js';
|
|
29
32
|
export type { JobSpec, JobCheck, PreparedJob } from './job.js';
|
|
30
33
|
export type { Recorded, UnsignedCall, Network } from './record.js';
|
|
31
|
-
export type { Verification, AnswerEvent, DecisionEvent } from './verify.js';
|
|
34
|
+
export type { Verification, AnswerEvent, DecisionEvent, ChainStatus, VerifyOptions, VerifyNetwork } from './verify.js';
|
|
32
35
|
export type { DecisionRecord, DecisionCheck } from './decision.js';
|
|
33
36
|
export type { Protocol, Adapter, AdapterOpts, EvaluatorCall } from './evaluator/index.js';
|
|
34
37
|
export type { JevError } from './ask.js';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@ export { prepareJob, runCheck } from './job.js';
|
|
|
20
20
|
export { record } from './record.js';
|
|
21
21
|
export { evaluatorCall } from './evaluator/index.js';
|
|
22
22
|
export { verify } from './verify.js';
|
|
23
|
+
export { JevChainError } from './errors.js';
|
|
24
|
+
export { CHAIN_NAMES, JEV_CHAINS, chainIdOf, chainName, jevLogsOn } from './chains.js';
|
|
25
|
+
export { rpcRotation, WARMBED } from './rpc.js';
|
|
23
26
|
export { verifyDecision } from './decision.js';
|
|
24
27
|
export { pipeline, STEPS, LAYER, factsFromEvidence, protocolFor, parseJob, localQuote } from './pipeline.js';
|
|
25
28
|
export { RUBRIC_v1 } from './rubric.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// GENERATED by scripts/vendor-rpc.mjs from packages/rpc/fallback.json (measured 2026-10-08). Do not edit.
|
|
2
|
+
// The fallback only: verify() asks warmbed's rotation first and uses these when warmbed does not answer.
|
|
3
|
+
export const RPC_FALLBACK = {
|
|
4
|
+
36927: {
|
|
5
|
+
logs: ["https://rpc.taifoon.dev"],
|
|
6
|
+
call: ["https://rpc.taifoon.dev"],
|
|
7
|
+
},
|
|
8
|
+
8453: {
|
|
9
|
+
logs: ["https://base.rpc.blxrbdn.com", "https://base.public.blockpi.network/v1/rpc/public", "https://base-rpc.publicnode.com", "https://gateway.tenderly.co/public/base", "https://base.gateway.tenderly.co"],
|
|
10
|
+
call: ["https://base.rpc.blxrbdn.com", "https://base.public.blockpi.network/v1/rpc/public", "https://gateway.tenderly.co/public/base", "https://base.gateway.tenderly.co", "https://base-rpc.publicnode.com", "https://developer-access-mainnet.base.org", "https://mainnet.base.org", "https://base.drpc.org", "https://base-mainnet.public.blastapi.io", "https://api.zan.top/base-mainnet", "https://base.meowrpc.com", "https://base-public.nodies.app"],
|
|
11
|
+
},
|
|
12
|
+
42161: {
|
|
13
|
+
logs: ["https://arb1.arbitrum.io/rpc", "https://gateway.tenderly.co/public/arbitrum", "https://arbitrum.gateway.tenderly.co"],
|
|
14
|
+
call: ["https://arb1.arbitrum.io/rpc", "https://gateway.tenderly.co/public/arbitrum", "https://arbitrum.gateway.tenderly.co", "https://arbitrum.drpc.org", "https://arbitrum-one-rpc.publicnode.com", "https://arbitrum-one.public.blastapi.io"],
|
|
15
|
+
},
|
|
16
|
+
5042: {
|
|
17
|
+
logs: ["https://rpc.mainnet.arc.io", "https://rpc.quicknode.mainnet.arc.io", "https://rpc.blockdaemon.mainnet.arc.io", "https://arc.gateway.tenderly.co"],
|
|
18
|
+
call: ["https://rpc.mainnet.arc.io", "https://rpc.quicknode.mainnet.arc.io", "https://rpc.blockdaemon.mainnet.arc.io", "https://arc.gateway.tenderly.co", "https://rpc.drpc.mainnet.arc.io", "https://arc.drpc.org", "https://arc-rpc.publicnode.com"],
|
|
19
|
+
},
|
|
20
|
+
4663: {
|
|
21
|
+
logs: ["https://rpc.mainnet.chain.robinhood.com", "https://rpc.ordofi.network"],
|
|
22
|
+
call: ["https://rpc.mainnet.chain.robinhood.com", "https://rpc.ordofi.network", "https://robinhood.drpc.org", "https://robinhood-rpc.publicnode.com"],
|
|
23
|
+
},
|
|
24
|
+
143: {
|
|
25
|
+
logs: ["https://rpc1.monad.xyz", "https://infra.originstake.com/monad/evm"],
|
|
26
|
+
call: ["https://rpc1.monad.xyz", "https://infra.originstake.com/monad/evm", "https://rpc.monad.xyz", "https://rpc3.monad.xyz", "https://rpc-mainnet.monadinfra.com", "https://monad.gateway.tenderly.co", "https://rpc.ankr.com/monad_mainnet", "https://rpc4.monad.xyz"],
|
|
27
|
+
},
|
|
28
|
+
3692781: {
|
|
29
|
+
logs: ["https://mainnet.taifoon.dev"],
|
|
30
|
+
call: ["https://mainnet.taifoon.dev"],
|
|
31
|
+
},
|
|
32
|
+
1: {
|
|
33
|
+
logs: ["https://gateway.tenderly.co/public/mainnet", "https://rpc.mevblocker.io", "https://mainnet.gateway.tenderly.co", "https://ethereum.publicnode.com", "https://rpc.flashbots.net"],
|
|
34
|
+
call: ["https://gateway.tenderly.co/public/mainnet", "https://mainnet.gateway.tenderly.co", "https://rpc.mevblocker.io", "https://ethereum-rpc.publicnode.com", "https://ethereum.publicnode.com", "https://rpc.flashbots.net", "https://eth.drpc.org"],
|
|
35
|
+
},
|
|
36
|
+
};
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const WARMBED = "https://warmbed.taifoon.dev";
|
|
2
|
+
export declare const RPC_TIMEOUT_MS = 30000;
|
|
3
|
+
/** Forget the cached rotations (tests; a long-running process never needs it: entries expire after 60 s). */
|
|
4
|
+
export declare function clearRpcCache(): void;
|
|
5
|
+
/** warmbed's rotation of a chain for `need`, then the static fallback (deduplicated). Never throws. */
|
|
6
|
+
export declare function rpcRotation(chainId: number, f?: typeof fetch, need?: 'logs' | 'call'): Promise<string[]>;
|
|
7
|
+
/** The cap an endpoint names when it refuses a range ("eth_getLogs is limited to a 500 range"), or null. */
|
|
8
|
+
export declare function rangeCapOf(message: string): number | null;
|
|
9
|
+
/**
|
|
10
|
+
* One JSON-RPC call over a list of endpoints (or one URL): each endpoint in turn, two passes with a short pause, each
|
|
11
|
+
* attempt bounded by RPC_TIMEOUT_MS. Throws JevChainError reverted / rpc_unavailable, or the range refusal as is.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rpcCall<T>(rpc: string | readonly string[], f: typeof fetch, method: string, params: unknown[], o?: {
|
|
14
|
+
chainId?: number;
|
|
15
|
+
passes?: number;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}): Promise<T>;
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The RPCs the package reads through: warmbed's rotation (https://warmbed.taifoon.dev, endpoints it measured to serve
|
|
2
|
+
// eth_getLogs windows), then the static list vendored from the RPC registry (src/rpc-fallback.ts). A rate limit, a 5xx,
|
|
3
|
+
// a timeout or a refusal moves a call to the next endpoint; a revert is thrown at once (JevChainError reverted), and so is a
|
|
4
|
+
// range refusal that names its cap (the caller narrows the window). When every endpoint failed: JevChainError rpc_unavailable.
|
|
5
|
+
import { JevChainError } from './errors.js';
|
|
6
|
+
import { RPC_FALLBACK } from './rpc-fallback.js';
|
|
7
|
+
export const WARMBED = 'https://warmbed.taifoon.dev';
|
|
8
|
+
export const RPC_TIMEOUT_MS = 30_000;
|
|
9
|
+
const WARMBED_TIMEOUT_MS = 2_500;
|
|
10
|
+
const TTL_MS = 60_000;
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
/** Forget the cached rotations (tests; a long-running process never needs it: entries expire after 60 s). */
|
|
13
|
+
export function clearRpcCache() { cache.clear(); }
|
|
14
|
+
/** warmbed's rotation of a chain for `need`, then the static fallback (deduplicated). Never throws. */
|
|
15
|
+
export async function rpcRotation(chainId, f = fetch, need = 'logs') {
|
|
16
|
+
const k = `${chainId}:${need}`;
|
|
17
|
+
const hit = cache.get(k);
|
|
18
|
+
if (hit && Date.now() - hit.at < TTL_MS)
|
|
19
|
+
return hit.urls;
|
|
20
|
+
let warm = [];
|
|
21
|
+
try {
|
|
22
|
+
const r = await f(`${WARMBED}/warmbed/chain/${chainId}/rotation?need=${need}${need === 'logs' ? '&min_span=500' : ''}&n=8`, { signal: AbortSignal.timeout(WARMBED_TIMEOUT_MS) });
|
|
23
|
+
const body = r && r.ok ? (await r.json()) : null;
|
|
24
|
+
if (Array.isArray(body))
|
|
25
|
+
warm = body.map((e) => (e && typeof e === 'object' ? String(e.url ?? '') : '')).filter((u) => /^https:\/\//.test(u));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
warm = [];
|
|
29
|
+
}
|
|
30
|
+
const urls = [...new Set([...warm, ...(RPC_FALLBACK[chainId]?.[need] ?? []), ...(RPC_FALLBACK[chainId]?.call ?? [])])];
|
|
31
|
+
if (warm.length)
|
|
32
|
+
cache.set(k, { at: Date.now(), urls });
|
|
33
|
+
return urls;
|
|
34
|
+
}
|
|
35
|
+
const REVERT = /revert|invalid opcode|out of gas/i;
|
|
36
|
+
/** The cap an endpoint names when it refuses a range ("eth_getLogs is limited to a 500 range"), or null. */
|
|
37
|
+
export function rangeCapOf(message) {
|
|
38
|
+
const m = /limited to (?:a )?(\d+)(?: block)? range/i.exec(message) ?? /range (?:is )?(?:limited|capped) (?:to|at) (\d+)/i.exec(message);
|
|
39
|
+
return m ? Number(m[1]) : null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One JSON-RPC call over a list of endpoints (or one URL): each endpoint in turn, two passes with a short pause, each
|
|
43
|
+
* attempt bounded by RPC_TIMEOUT_MS. Throws JevChainError reverted / rpc_unavailable, or the range refusal as is.
|
|
44
|
+
*/
|
|
45
|
+
export async function rpcCall(rpc, f, method, params, o = {}) {
|
|
46
|
+
const urls = typeof rpc === 'string' ? [rpc] : rpc;
|
|
47
|
+
if (!urls.length)
|
|
48
|
+
throw new JevChainError('rpc_unavailable', `${method}: no RPC endpoint`, o.chainId);
|
|
49
|
+
const passes = o.passes ?? 2;
|
|
50
|
+
let last = '';
|
|
51
|
+
for (let p = 0; p < passes; p++) {
|
|
52
|
+
for (const u of urls) {
|
|
53
|
+
try {
|
|
54
|
+
const r = await f(u, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(o.timeoutMs ?? RPC_TIMEOUT_MS) });
|
|
55
|
+
if (typeof r.status === 'number' && r.status >= 400) {
|
|
56
|
+
last = `${hostOf(u)}: http ${r.status}`;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const j = (await r.json());
|
|
60
|
+
if (j.error) {
|
|
61
|
+
const msg = String(j.error.message ?? j.error.code);
|
|
62
|
+
if (j.error.code === 3 || REVERT.test(msg))
|
|
63
|
+
throw new JevChainError('reverted', `${method}: ${msg}`, o.chainId);
|
|
64
|
+
if (rangeCapOf(msg))
|
|
65
|
+
throw Object.assign(new Error(`${method}: ${msg}`), { final: true });
|
|
66
|
+
last = `${hostOf(u)}: ${msg.slice(0, 160)}`;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
return j.result;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e instanceof JevChainError || e.final)
|
|
73
|
+
throw e;
|
|
74
|
+
last = `${hostOf(u)}: ${e instanceof Error ? e.message : String(e)}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (p + 1 < passes)
|
|
78
|
+
await new Promise((r) => setTimeout(r, 400 * (p + 1)));
|
|
79
|
+
}
|
|
80
|
+
throw new JevChainError('rpc_unavailable', `${method}: every RPC endpoint failed (last: ${last})`, o.chainId);
|
|
81
|
+
}
|
|
82
|
+
function hostOf(u) { try {
|
|
83
|
+
return new URL(u).host;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return u;
|
|
87
|
+
} }
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rangeCapOf } from './rpc.js';
|
|
1
2
|
import { type Hex } from './hash.js';
|
|
2
3
|
import type { Receipt } from './receipt.js';
|
|
3
4
|
import { type Rubric, type RubricInput } from './rubric.js';
|
|
@@ -25,36 +26,48 @@ export type DecisionEvent = {
|
|
|
25
26
|
export type Verification = {
|
|
26
27
|
ok: boolean;
|
|
27
28
|
checks: Record<string, boolean>;
|
|
29
|
+
/** status: recorded (the answer row is on chain) · not_recorded (the chain answered, no row) · rpc_unavailable (no endpoint
|
|
30
|
+
* answered: the row may exist) · not_deployed (no Jev log on that chain) · skipped (chain: false) */
|
|
28
31
|
onChain: {
|
|
29
32
|
chainId: number;
|
|
30
33
|
answers: AnswerEvent[];
|
|
31
34
|
decisions: DecisionEvent[];
|
|
35
|
+
status?: ChainStatus;
|
|
32
36
|
};
|
|
33
37
|
problems: string[];
|
|
34
38
|
};
|
|
39
|
+
export type ChainStatus = 'recorded' | 'not_recorded' | 'rpc_unavailable' | 'not_deployed' | 'skipped';
|
|
35
40
|
export type Log = {
|
|
36
41
|
transactionHash: string;
|
|
37
42
|
blockNumber: string;
|
|
38
43
|
topics: string[];
|
|
39
44
|
data: Hex;
|
|
40
45
|
};
|
|
41
|
-
|
|
42
|
-
export
|
|
46
|
+
type Rpc = string | readonly string[];
|
|
47
|
+
export { rangeCapOf };
|
|
43
48
|
/**
|
|
44
49
|
* eth_getLogs over [from, to], nearest to `center` first, in windows of at most `step` blocks (public Base endpoints
|
|
45
50
|
* refuse wide ranges: mainnet.base.org caps a call at 500 blocks). A refusal that names a smaller cap shrinks the
|
|
46
51
|
* window and retries. Stops at the first window where `hit` matches, so a record next to its answer costs one call.
|
|
47
52
|
*/
|
|
48
|
-
export declare function logsAround(rpc:
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
export declare function verify(x: Receipt | string, opts?: {
|
|
55
|
-
rpc?: string;
|
|
53
|
+
export declare function logsAround(rpc: Rpc, f: typeof fetch, address: string, topics: Array<string | null>, center: number, from: number, to: number, hit: (l: Log) => boolean, step?: number, chainId?: number): Promise<Log[]>;
|
|
54
|
+
/** Which chain verify() reads: 'devnet' (default, 36927), 'base', a chain id or a chain name ("arbitrum", "arc", "monad" …). */
|
|
55
|
+
export type VerifyNetwork = 'devnet' | 'base' | number | string;
|
|
56
|
+
export type VerifyOptions = {
|
|
57
|
+
/** one URL, a comma list or a list: read through these instead of warmbed's rotation */
|
|
58
|
+
rpc?: string | readonly string[];
|
|
56
59
|
fetch?: typeof fetch;
|
|
57
60
|
rubric?: Rubric | RubricInput;
|
|
61
|
+
/** false: recompute the digests only, read no chain */
|
|
58
62
|
chain?: boolean;
|
|
59
|
-
network?:
|
|
60
|
-
|
|
63
|
+
network?: VerifyNetwork;
|
|
64
|
+
/** a block near the decision row (a decision record's anchor.block): its window is read first */
|
|
65
|
+
decisionBlock?: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Recompute a receipt (or take a bare answers digest), then find its rows on one chain. Every chain but the devnet is
|
|
69
|
+
* read through JevAnswerLog.recordedAt (trusted records only) and the decision row in windows around it, because public
|
|
70
|
+
* endpoints refuse wide log ranges; the devnet is read in one range. The chain is read through warmbed's rotation, then
|
|
71
|
+
* the registry's static list; `rpc` overrides both.
|
|
72
|
+
*/
|
|
73
|
+
export declare function verify(x: Receipt | string, opts?: VerifyOptions): Promise<Verification>;
|
package/dist/verify.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// verify(): recompute every digest a receipt carries, re-compose its verdict from its own answers, then find the chain
|
|
2
2
|
// events that hold those digests. Given a bare answers digest, it finds the JevAnswered log for it.
|
|
3
3
|
import { decode } from './abi.js';
|
|
4
|
-
import {
|
|
4
|
+
import { chainIdOf, chainName, DEVNET, jevLogsOn } from './chains.js';
|
|
5
|
+
import { JevChainError } from './errors.js';
|
|
6
|
+
import { rangeCapOf, rpcCall, rpcRotation } from './rpc.js';
|
|
5
7
|
import { keccakHex, sha256Hex } from './hash.js';
|
|
6
8
|
import { answersDigestOf, confidenceBpsOf, decisionAnswersOf, decisionDigest, subjectIdOf } from './records.js';
|
|
7
9
|
import { defineRubric, receiptBody, receiptHashOf, RUBRIC_v1 } from './rubric.js';
|
|
@@ -10,34 +12,23 @@ const TOPIC_DECIDED = keccakHex('Decided(bytes32,bytes32,address,uint256,bytes32
|
|
|
10
12
|
const ANSWERED_DATA = ['address', 'bytes32', 'bytes32', 'string', 'string', 'uint256', 'bool'];
|
|
11
13
|
const DECIDED_DATA = ['uint256', 'bytes32', 'uint16', 'string', 'string'];
|
|
12
14
|
const hexN = (n) => '0x' + n.toString(16);
|
|
13
|
-
async function
|
|
14
|
-
|
|
15
|
-
const j = (await r.json());
|
|
16
|
-
if (j.error)
|
|
17
|
-
throw new Error(`${method}: ${j.error.message}`);
|
|
18
|
-
return j.result;
|
|
19
|
-
}
|
|
20
|
-
async function logs(rpc, f, address, topics, fromBlock, toBlock) {
|
|
21
|
-
return (await rpcCall(rpc, f, 'eth_getLogs', [{ address, topics, fromBlock: hexN(fromBlock), toBlock: toBlock == null ? 'latest' : hexN(toBlock) }])) ?? [];
|
|
15
|
+
async function logs(rpc, f, address, topics, fromBlock, toBlock, chainId) {
|
|
16
|
+
return (await rpcCall(rpc, f, 'eth_getLogs', [{ address, topics, fromBlock: hexN(fromBlock), toBlock: toBlock == null ? 'latest' : hexN(toBlock) }], { chainId })) ?? [];
|
|
22
17
|
}
|
|
23
18
|
const SEL_RECORDED_AT = keccakHex('recordedAt(bytes32)').slice(0, 10);
|
|
24
19
|
/** JevAnswerLog.recordedAt(digest): the block of the first trusted record, 0 when none. */
|
|
25
|
-
async function recordedAt(rpc, f, address, digest) {
|
|
26
|
-
const out = await rpcCall(rpc, f, 'eth_call', [{ to: address, data: SEL_RECORDED_AT + digest.slice(2) }, 'latest']);
|
|
20
|
+
async function recordedAt(rpc, f, address, digest, chainId) {
|
|
21
|
+
const out = await rpcCall(rpc, f, 'eth_call', [{ to: address, data: SEL_RECORDED_AT + digest.slice(2) }, 'latest'], { chainId });
|
|
27
22
|
return out && out !== '0x' ? Number(BigInt(out)) : 0;
|
|
28
23
|
}
|
|
29
|
-
|
|
30
|
-
export function rangeCapOf(message) {
|
|
31
|
-
const m = /limited to (?:a )?(\d+)(?: block)? range/i.exec(message) ?? /range (?:is )?(?:limited|capped) (?:to|at) (\d+)/i.exec(message);
|
|
32
|
-
return m ? Number(m[1]) : null;
|
|
33
|
-
}
|
|
24
|
+
export { rangeCapOf };
|
|
34
25
|
/**
|
|
35
26
|
* eth_getLogs over [from, to], nearest to `center` first, in windows of at most `step` blocks (public Base endpoints
|
|
36
27
|
* refuse wide ranges: mainnet.base.org caps a call at 500 blocks). A refusal that names a smaller cap shrinks the
|
|
37
28
|
* window and retries. Stops at the first window where `hit` matches, so a record next to its answer costs one call.
|
|
38
29
|
*/
|
|
39
|
-
export async function logsAround(rpc, f, address, topics, center, from, to, hit, step = 500) {
|
|
40
|
-
const head = Number(BigInt(await rpcCall(rpc, f, 'eth_blockNumber', [])));
|
|
30
|
+
export async function logsAround(rpc, f, address, topics, center, from, to, hit, step = 500, chainId) {
|
|
31
|
+
const head = Number(BigInt(await rpcCall(rpc, f, 'eth_blockNumber', [], { chainId })));
|
|
41
32
|
to = Math.min(to, head); // endpoints refuse a range past the head
|
|
42
33
|
center = Math.min(Math.max(center, from), to);
|
|
43
34
|
const out = [];
|
|
@@ -46,7 +37,7 @@ export async function logsAround(rpc, f, address, topics, center, from, to, hit,
|
|
|
46
37
|
try {
|
|
47
38
|
const got = [];
|
|
48
39
|
for (let x = a; x <= b; x += step)
|
|
49
|
-
got.push(...(await logs(rpc, f, address, topics, x, Math.min(b, x + step - 1))));
|
|
40
|
+
got.push(...(await logs(rpc, f, address, topics, x, Math.min(b, x + step - 1), chainId)));
|
|
50
41
|
return got;
|
|
51
42
|
}
|
|
52
43
|
catch (e) {
|
|
@@ -79,20 +70,18 @@ export async function logsAround(rpc, f, address, topics, center, from, to, hit,
|
|
|
79
70
|
const answerEvent = (l) => { const [recorder, inputDigest, decisionDigest, model, uri, , trusted] = decode(ANSWERED_DATA, l.data); return { tx: l.transactionHash, block: parseInt(l.blockNumber, 16), recorder: String(recorder), trusted: Boolean(trusted), subject: l.topics[2], inputDigest: inputDigest, decisionDigest: decisionDigest, model: String(model), uri: String(uri) }; };
|
|
80
71
|
const decisionEvent = (l) => { const [index, digest, bps, model, uri] = decode(DECIDED_DATA, l.data); return { tx: l.transactionHash, block: parseInt(l.blockNumber, 16), recorder: `0x${String(l.topics[3]).slice(26)}`, index: Number(index), digest: digest, confidenceBps: Number(bps), model: String(model), uri: String(uri) }; };
|
|
81
72
|
/**
|
|
82
|
-
*
|
|
83
|
-
* JevAnswerLog.recordedAt (trusted records only) and the decision row in
|
|
84
|
-
* endpoints refuse wide log ranges.
|
|
73
|
+
* Recompute a receipt (or take a bare answers digest), then find its rows on one chain. Every chain but the devnet is
|
|
74
|
+
* read through JevAnswerLog.recordedAt (trusted records only) and the decision row in windows around it, because public
|
|
75
|
+
* endpoints refuse wide log ranges; the devnet is read in one range. The chain is read through warmbed's rotation, then
|
|
76
|
+
* the registry's static list; `rpc` overrides both.
|
|
85
77
|
*/
|
|
86
78
|
export async function verify(x, opts = {}) {
|
|
87
|
-
const
|
|
79
|
+
const chainId = chainIdOf(opts.network ?? 'devnet');
|
|
88
80
|
const f = opts.fetch ?? fetch;
|
|
89
|
-
const
|
|
90
|
-
const dev = network === 'base'
|
|
91
|
-
? { chainId: CONTRACTS.base.chainId, answerLog: CONTRACTS.base.answerLog, decisionLog: CONTRACTS.base.decisionLog }
|
|
92
|
-
: { chainId: CONTRACTS.devnet.chainId, answerLog: CONTRACTS.devnet.answerLog, decisionLog: CONTRACTS.devnet.decisionLog };
|
|
81
|
+
const dev = { chainId, ...(jevLogsOn(chainId) ?? { answerLog: null, decisionLog: null }) };
|
|
93
82
|
const checks = {};
|
|
94
83
|
const problems = [];
|
|
95
|
-
const onChain = { chainId: dev.chainId, answers: [], decisions: [] };
|
|
84
|
+
const onChain = { chainId: dev.chainId, answers: [], decisions: [], status: 'skipped' };
|
|
96
85
|
let answersDigest;
|
|
97
86
|
let subjectId = null;
|
|
98
87
|
let decision = null;
|
|
@@ -125,32 +114,46 @@ export async function verify(x, opts = {}) {
|
|
|
125
114
|
decision = x.decision?.digest ?? null;
|
|
126
115
|
}
|
|
127
116
|
if (opts.chain !== false && answersDigest) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
117
|
+
const where = chainName(chainId);
|
|
118
|
+
if (!dev.answerLog || !dev.decisionLog) {
|
|
119
|
+
onChain.status = 'not_deployed';
|
|
120
|
+
problems.push(`no Jev log is deployed on ${where}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
try {
|
|
124
|
+
// an explicit rpc (one URL, a comma list or a list) wins; otherwise warmbed's rotation, then the static fallback
|
|
125
|
+
const given = typeof opts.rpc === 'string' ? opts.rpc.split(',').map((u) => u.trim()).filter(Boolean) : opts.rpc;
|
|
126
|
+
const rpc = given?.length ? given : await rpcRotation(chainId, f);
|
|
127
|
+
if (chainId !== DEVNET) {
|
|
128
|
+
const at = await recordedAt(rpc, f, dev.answerLog.address, answersDigest, chainId);
|
|
129
|
+
if (at > 0)
|
|
130
|
+
onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], at, at, chainId)).map(answerEvent);
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], dev.answerLog.fromBlock, undefined, chainId)).map(answerEvent);
|
|
134
|
+
// a bare digest: the answer row names its subject and decision, so the decision row can be found too
|
|
135
|
+
const first = onChain.answers[0];
|
|
136
|
+
if (!subjectId && first && !/^0x0{64}$/.test(first.decisionDigest)) {
|
|
137
|
+
subjectId = first.subject;
|
|
138
|
+
decision = first.decisionDigest;
|
|
139
|
+
}
|
|
140
|
+
if (subjectId && decision) {
|
|
141
|
+
const center = opts.decisionBlock ?? first?.block;
|
|
142
|
+
const found = chainId !== DEVNET && center
|
|
143
|
+
? await logsAround(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], center, Math.max(dev.decisionLog.fromBlock, center - 4000), center + 4000, (l) => decisionEvent(l).digest.toLowerCase() === decision.toLowerCase(), 500, chainId)
|
|
144
|
+
: await logs(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], dev.decisionLog.fromBlock, undefined, chainId);
|
|
145
|
+
onChain.decisions = found.map(decisionEvent).filter((d) => d.digest.toLowerCase() === decision.toLowerCase());
|
|
146
|
+
}
|
|
147
|
+
onChain.status = onChain.answers.length ? 'recorded' : 'not_recorded';
|
|
148
|
+
if (!onChain.answers.length)
|
|
149
|
+
problems.push(`not recorded on ${where}`);
|
|
143
150
|
}
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
onChain.decisions = found.map(decisionEvent).filter((d) => d.digest.toLowerCase() === decision.toLowerCase());
|
|
151
|
+
catch (e) {
|
|
152
|
+
const unavailable = !(e instanceof JevChainError) || e.code === 'rpc_unavailable';
|
|
153
|
+
onChain.status = unavailable ? 'rpc_unavailable' : onChain.answers.length ? 'recorded' : 'not_recorded';
|
|
154
|
+
problems.push(`${unavailable ? `RPC unavailable on ${where}` : `chain read failed on ${where}`}: ${e instanceof Error ? e.message : String(e)}`);
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
|
-
catch (e) {
|
|
152
|
-
problems.push(`chain read failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
153
|
-
}
|
|
154
157
|
checks.answersOnChain = onChain.answers.length > 0;
|
|
155
158
|
if (decision)
|
|
156
159
|
checks.decisionOnChain = onChain.decisions.length > 0;
|
package/docs/ON_CHAIN.md
CHANGED
|
@@ -19,7 +19,7 @@ On Base (8453), since 0.2.0:
|
|
|
19
19
|
`npx @taifoon/jev verify <digest> --network base` read them (the CLI's default `any` tries the devnet, then Base).
|
|
20
20
|
0.1.0 carried no Base addresses. An earlier Base answer log, `0x5bac70eb78224bbCBa83f5A36DF09D549d3f57fa`, is superseded and holds no records.
|
|
21
21
|
|
|
22
|
-
Here is one
|
|
22
|
+
Here is one grade, end to end: n8n execution 86, a proof-verification job graded with RUBRIC_v1.
|
|
23
23
|
|
|
24
24
|
- Answers recorded in devnet tx
|
|
25
25
|
[`0x9a38cf55…a61b`](https://www.taifoon.io/scan/36927/tx/0x9a38cf5541bb82a8a6e2e5cf1a96ed25fdc46921c351506da83023f5dec1a61b).
|
|
@@ -29,7 +29,7 @@ Here is one real grade, end to end: n8n execution 86, a proof-verification job g
|
|
|
29
29
|
Decision digest `0x781b0fce…3667`, confidence 2,700 bps.
|
|
30
30
|
- Jev answered spec_met `{ yes: 0.36, no: 0.64 }`, so the composed verdict is **reject** (spec_met ≤ 0.40).
|
|
31
31
|
|
|
32
|
-
`verify('0x6c7e6d29…f52d')` finds both rows. This is
|
|
32
|
+
`verify('0x6c7e6d29…f52d')` finds both rows. This is its output:
|
|
33
33
|
|
|
34
34
|
```
|
|
35
35
|
{"ok":true,"checks":{"answersOnChain":true,"decisionOnChain":true},
|
package/docs/PIPELINE.md
CHANGED
|
@@ -71,6 +71,6 @@ Steps 1 to 4 are required: saying no at one stops the run. Keys come from the en
|
|
|
71
71
|
TypeSafe key; without it the Jev step stops and says where to get one) and `TAIFOON_RELAYER_KEY`, and are never printed. Nothing is signed. In code it is `pipeline()`,
|
|
72
72
|
with a `before` gate and an `after` hook per step.
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
A run (2026-09-27): BitAgent job 7287 on Base asked for "equity_research where ticker is 'AAPL'" and
|
|
75
75
|
got a 32-byte digest. Facts passed (delivered, before the deadline, 1.5 USDC). Jev answered spec_met 0.03, so the
|
|
76
76
|
verdict is **reject**; the receipt re-derives; the seller's quote is 0.0%–39.0% (6 settled, thin record).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taifoon/jev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "Grade AI agent jobs with TypeSafe's Jev and record the verdict on chain: code checks the facts, Jev answers a published rubric, and you get a receipt, the on-chain record and the evaluator call for ERC-8183 and Virtuals ACP. No runtime dependencies, no key inside.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jev",
|