@seljs/runtime 1.0.0 → 1.0.1-beta.9
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/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/analysis/call-collector.cjs +233 -0
- package/dist/analysis/call-collector.mjs +232 -0
- package/dist/analysis/dependency-analyzer.cjs +68 -0
- package/dist/analysis/dependency-analyzer.mjs +68 -0
- package/dist/analysis/round-planner.cjs +65 -0
- package/dist/analysis/round-planner.mjs +65 -0
- package/dist/analysis/types.d.cts +115 -0
- package/dist/analysis/types.d.mts +115 -0
- package/dist/debug.cjs +17 -0
- package/dist/debug.mjs +15 -0
- package/dist/environment/context.cjs +11 -0
- package/dist/environment/context.mjs +11 -0
- package/dist/environment/contract-caller.cjs +81 -0
- package/dist/environment/contract-caller.mjs +77 -0
- package/dist/environment/environment.cjs +254 -0
- package/dist/environment/environment.d.cts +84 -0
- package/dist/environment/environment.d.mts +84 -0
- package/dist/environment/environment.mjs +252 -0
- package/dist/environment/error-wrapper.cjs +29 -0
- package/dist/environment/error-wrapper.mjs +27 -0
- package/dist/environment/index.cjs +1 -0
- package/dist/environment/index.d.mts +2 -0
- package/dist/environment/index.mjs +2 -0
- package/dist/environment/replay-cache.cjs +48 -0
- package/dist/environment/replay-cache.mjs +47 -0
- package/dist/environment/types.d.cts +60 -0
- package/dist/environment/types.d.mts +60 -0
- package/dist/errors/errors.cjs +68 -0
- package/dist/errors/errors.d.cts +64 -0
- package/dist/errors/errors.d.mts +64 -0
- package/dist/errors/errors.mjs +63 -0
- package/dist/errors/index.cjs +3 -0
- package/dist/errors/index.d.mts +1 -0
- package/dist/errors/index.mjs +2 -0
- package/dist/execution/multi-round-executor.cjs +45 -0
- package/dist/execution/multi-round-executor.mjs +44 -0
- package/dist/execution/multicall-batcher.cjs +51 -0
- package/dist/execution/multicall-batcher.mjs +50 -0
- package/dist/execution/multicall.cjs +39 -0
- package/dist/execution/multicall.mjs +38 -0
- package/dist/execution/result-cache.cjs +63 -0
- package/dist/execution/result-cache.mjs +63 -0
- package/dist/execution/round-executor.cjs +81 -0
- package/dist/execution/round-executor.mjs +80 -0
- package/dist/execution/types.d.cts +58 -0
- package/dist/execution/types.d.mts +58 -0
- package/dist/factory.cjs +6 -0
- package/dist/factory.d.cts +7 -0
- package/dist/factory.d.mts +6 -0
- package/dist/factory.mjs +6 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +6 -0
- package/package.json +26 -19
- package/dist/analysis/call-collector.d.ts +0 -20
- package/dist/analysis/call-collector.d.ts.map +0 -1
- package/dist/analysis/call-collector.js +0 -272
- package/dist/analysis/dependency-analyzer.d.ts +0 -14
- package/dist/analysis/dependency-analyzer.d.ts.map +0 -1
- package/dist/analysis/dependency-analyzer.js +0 -76
- package/dist/analysis/index.d.ts +0 -2
- package/dist/analysis/index.d.ts.map +0 -1
- package/dist/analysis/index.js +0 -1
- package/dist/analysis/round-planner.d.ts +0 -32
- package/dist/analysis/round-planner.d.ts.map +0 -1
- package/dist/analysis/round-planner.js +0 -69
- package/dist/analysis/types.d.ts +0 -113
- package/dist/analysis/types.d.ts.map +0 -1
- package/dist/analysis/types.js +0 -1
- package/dist/debug.d.ts +0 -13
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js +0 -12
- package/dist/environment/context.d.ts +0 -3
- package/dist/environment/context.d.ts.map +0 -1
- package/dist/environment/context.js +0 -8
- package/dist/environment/contract-caller.d.ts +0 -25
- package/dist/environment/contract-caller.d.ts.map +0 -1
- package/dist/environment/contract-caller.js +0 -85
- package/dist/environment/environment.d.ts +0 -81
- package/dist/environment/environment.d.ts.map +0 -1
- package/dist/environment/environment.js +0 -279
- package/dist/environment/error-wrapper.d.ts +0 -11
- package/dist/environment/error-wrapper.d.ts.map +0 -1
- package/dist/environment/error-wrapper.js +0 -33
- package/dist/environment/index.d.ts +0 -3
- package/dist/environment/index.d.ts.map +0 -1
- package/dist/environment/index.js +0 -2
- package/dist/environment/replay-cache.d.ts +0 -23
- package/dist/environment/replay-cache.d.ts.map +0 -1
- package/dist/environment/replay-cache.js +0 -51
- package/dist/environment/types.d.ts +0 -57
- package/dist/environment/types.d.ts.map +0 -1
- package/dist/environment/types.js +0 -1
- package/dist/errors/errors.d.ts +0 -63
- package/dist/errors/errors.d.ts.map +0 -1
- package/dist/errors/errors.js +0 -63
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js +0 -1
- package/dist/execution/index.d.ts +0 -2
- package/dist/execution/index.d.ts.map +0 -1
- package/dist/execution/index.js +0 -1
- package/dist/execution/multi-round-executor.d.ts +0 -17
- package/dist/execution/multi-round-executor.d.ts.map +0 -1
- package/dist/execution/multi-round-executor.js +0 -47
- package/dist/execution/multicall-batcher.d.ts +0 -14
- package/dist/execution/multicall-batcher.d.ts.map +0 -1
- package/dist/execution/multicall-batcher.js +0 -53
- package/dist/execution/multicall.d.ts +0 -42
- package/dist/execution/multicall.d.ts.map +0 -1
- package/dist/execution/multicall.js +0 -29
- package/dist/execution/result-cache.d.ts +0 -47
- package/dist/execution/result-cache.d.ts.map +0 -1
- package/dist/execution/result-cache.js +0 -65
- package/dist/execution/round-executor.d.ts +0 -18
- package/dist/execution/round-executor.d.ts.map +0 -1
- package/dist/execution/round-executor.js +0 -95
- package/dist/execution/types.d.ts +0 -55
- package/dist/execution/types.d.ts.map +0 -1
- package/dist/execution/types.js +0 -1
- package/dist/factory.d.ts +0 -3
- package/dist/factory.d.ts.map +0 -1
- package/dist/factory.js +0 -2
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -7
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_context = require("./context.cjs");
|
|
3
|
+
const require_replay_cache = require("./replay-cache.cjs");
|
|
4
|
+
const require_debug = require("../debug.cjs");
|
|
5
|
+
const require_errors = require("../errors/errors.cjs");
|
|
6
|
+
require("../errors/index.cjs");
|
|
7
|
+
const require_contract_caller = require("./contract-caller.cjs");
|
|
8
|
+
const require_error_wrapper = require("./error-wrapper.cjs");
|
|
9
|
+
const require_call_collector = require("../analysis/call-collector.cjs");
|
|
10
|
+
const require_dependency_analyzer = require("../analysis/dependency-analyzer.cjs");
|
|
11
|
+
const require_round_planner = require("../analysis/round-planner.cjs");
|
|
12
|
+
const require_multi_round_executor = require("../execution/multi-round-executor.cjs");
|
|
13
|
+
let _seljs_checker = require("@seljs/checker");
|
|
14
|
+
let _seljs_common = require("@seljs/common");
|
|
15
|
+
//#region src/environment/environment.ts
|
|
16
|
+
/**
|
|
17
|
+
* Core SEL runtime that bridges CEL expression evaluation with EVM contract reads.
|
|
18
|
+
*
|
|
19
|
+
* Manages a CEL runtime extended with Solidity primitive types (uint256, address, bool, etc.),
|
|
20
|
+
* registered smart contracts whose view functions become callable in expressions, and typed
|
|
21
|
+
* variables that are supplied at evaluation time. Contract calls are automatically batched
|
|
22
|
+
* via multicall3 and executed in dependency-ordered rounds.
|
|
23
|
+
*/
|
|
24
|
+
const debug = require_debug.createLogger("environment");
|
|
25
|
+
var SELRuntime = class {
|
|
26
|
+
env;
|
|
27
|
+
client;
|
|
28
|
+
schema;
|
|
29
|
+
variableTypes = /* @__PURE__ */ new Map();
|
|
30
|
+
maxRounds;
|
|
31
|
+
maxCalls;
|
|
32
|
+
multicallOptions;
|
|
33
|
+
contractBindings;
|
|
34
|
+
codecRegistry;
|
|
35
|
+
/**
|
|
36
|
+
* Per-evaluation mutable state.
|
|
37
|
+
*
|
|
38
|
+
* These fields are set before CEL evaluation and cleared in `finally`.
|
|
39
|
+
* They exist because the contract call handler closure (created in the
|
|
40
|
+
* constructor) needs access to per-evaluation context, but CEL's
|
|
41
|
+
* `Environment.parse()` returns a function that doesn't accept extra
|
|
42
|
+
* parameters beyond the variable bindings.
|
|
43
|
+
*
|
|
44
|
+
* Thread safety: The `mutex` field serializes concurrent `evaluate()`
|
|
45
|
+
* calls, ensuring these fields are never accessed concurrently.
|
|
46
|
+
*
|
|
47
|
+
* TODO: Consider refactoring to pass an EvaluationContext object through
|
|
48
|
+
* the handler closure instead of mutating instance state. This would
|
|
49
|
+
* eliminate the need for the mutex and make the code more testable.
|
|
50
|
+
* See .omc/drafts/context-object-spike.md for preliminary analysis.
|
|
51
|
+
*/
|
|
52
|
+
currentCache;
|
|
53
|
+
currentClient;
|
|
54
|
+
currentCallCounter;
|
|
55
|
+
/** Mutex to serialize concurrent evaluate() calls (protects current* fields) */
|
|
56
|
+
mutex = Promise.resolve();
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new immutable SEL runtime with Solidity types pre-registered.
|
|
59
|
+
*
|
|
60
|
+
* Initializes the underlying CEL runtime, registers all Solidity primitive types,
|
|
61
|
+
* and processes any contracts and variables from the schema. After construction,
|
|
62
|
+
* the environment is fully configured and immutable.
|
|
63
|
+
*
|
|
64
|
+
* @param config - Configuration containing the SEL schema, optional viem client,
|
|
65
|
+
* multicall settings, and CEL/execution limits (maxRounds defaults to 10, maxCalls to 100)
|
|
66
|
+
*/
|
|
67
|
+
constructor(config) {
|
|
68
|
+
const limits = config.limits;
|
|
69
|
+
const celLimits = limits ? {
|
|
70
|
+
maxAstNodes: limits.maxAstNodes,
|
|
71
|
+
maxDepth: limits.maxDepth,
|
|
72
|
+
maxListElements: limits.maxListElements,
|
|
73
|
+
maxMapEntries: limits.maxMapEntries,
|
|
74
|
+
maxCallArguments: limits.maxCallArguments
|
|
75
|
+
} : void 0;
|
|
76
|
+
this.maxRounds = limits?.maxRounds ?? 10;
|
|
77
|
+
this.maxCalls = limits?.maxCalls ?? 100;
|
|
78
|
+
this.multicallOptions = config.multicall;
|
|
79
|
+
this.client = config.client;
|
|
80
|
+
this.schema = config.schema;
|
|
81
|
+
const handler = (contractName, methodName, args) => {
|
|
82
|
+
const contract = this.findContract(contractName);
|
|
83
|
+
if (!contract) throw new require_errors.SELContractError(`Unknown contract "${contractName}"`, { contractName });
|
|
84
|
+
const method = contract.methods.find((m) => m.name === methodName);
|
|
85
|
+
if (!method) throw new require_errors.SELContractError(`Unknown method "${contractName}.${methodName}"`, {
|
|
86
|
+
contractName,
|
|
87
|
+
methodName
|
|
88
|
+
});
|
|
89
|
+
return require_contract_caller.executeContractCall(contract, method, args, {
|
|
90
|
+
executionCache: this.currentCache,
|
|
91
|
+
client: this.currentClient ?? this.client,
|
|
92
|
+
codecRegistry: this.codecRegistry,
|
|
93
|
+
callCounter: this.currentCallCounter
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const { env, contractBindings, codecRegistry } = (0, _seljs_checker.createRuntimeEnvironment)(this.schema, handler, {
|
|
97
|
+
limits: celLimits,
|
|
98
|
+
unlistedVariablesAreDyn: true
|
|
99
|
+
});
|
|
100
|
+
this.env = env;
|
|
101
|
+
this.contractBindings = contractBindings;
|
|
102
|
+
this.codecRegistry = codecRegistry;
|
|
103
|
+
for (const v of this.schema.variables) this.variableTypes.set(v.name, v.type);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Type-checks an expression against registered variables and contract methods.
|
|
107
|
+
*
|
|
108
|
+
* @param expression - A CEL expression string to type-check
|
|
109
|
+
* @returns The type-check result containing validity, inferred type, and any errors
|
|
110
|
+
* @throws {@link SELTypeError} If the expression contains unrecoverable type errors
|
|
111
|
+
*/
|
|
112
|
+
check(expression) {
|
|
113
|
+
try {
|
|
114
|
+
return this.env.check(expression);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw require_error_wrapper.wrapError(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Evaluates a SEL expression, executing any embedded contract calls on-chain.
|
|
121
|
+
*
|
|
122
|
+
* When contract calls are present, the full pipeline runs:
|
|
123
|
+
* 1. Parse the expression and collect contract calls from the AST
|
|
124
|
+
* 2. Type-check against registered variables and contract methods
|
|
125
|
+
* 3. Build a dependency graph and plan execution rounds
|
|
126
|
+
* 4. Execute contract calls via multicall3 batching at a pinned block number
|
|
127
|
+
* 5. Evaluate the CEL expression with resolved contract results and context values
|
|
128
|
+
* 6. Unwrap Solidity wrapper types back to native JS values (BigInt, string, etc.)
|
|
129
|
+
*
|
|
130
|
+
* @param expression A CEL expression string
|
|
131
|
+
* @param context Variable bindings for evaluation
|
|
132
|
+
* @param options Optional client override
|
|
133
|
+
* @returns An {@link EvaluateResult} containing the value and optional execution metadata
|
|
134
|
+
* @throws {@link SELParseError} If the expression has invalid syntax
|
|
135
|
+
* @throws {@link SELTypeError} If type-checking fails
|
|
136
|
+
* @throws {@link SELContractError} If a contract call fails or no client is available
|
|
137
|
+
* @throws {@link SELEvaluationError} If CEL evaluation fails
|
|
138
|
+
*/
|
|
139
|
+
async evaluate(expression, context, options) {
|
|
140
|
+
let release;
|
|
141
|
+
const prev = this.mutex;
|
|
142
|
+
this.mutex = new Promise((resolve) => {
|
|
143
|
+
release = resolve;
|
|
144
|
+
});
|
|
145
|
+
await prev;
|
|
146
|
+
try {
|
|
147
|
+
return await this.doEvaluate(expression, context, options);
|
|
148
|
+
} finally {
|
|
149
|
+
release();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
planExecution(expression, evaluationContext) {
|
|
153
|
+
const parseResult = this.env.parse(expression);
|
|
154
|
+
const collectedCalls = require_call_collector.collectCalls(parseResult.ast, { get: (name) => this.findContract(name) });
|
|
155
|
+
debug("evaluate: collected %d calls", collectedCalls.length);
|
|
156
|
+
const normalizedContext = Object.keys(evaluationContext).length ? require_context.normalizeContextForEvaluation(evaluationContext, this.variableTypes, this.codecRegistry) : void 0;
|
|
157
|
+
const executionVariables = Object.keys(evaluationContext).length ? evaluationContext : {};
|
|
158
|
+
const typeCheckResult = parseResult.check();
|
|
159
|
+
if (!typeCheckResult.valid) throw new _seljs_common.SELTypeError(typeCheckResult.error?.message ?? "Type check failed", { cause: typeCheckResult.error });
|
|
160
|
+
return {
|
|
161
|
+
parseResult,
|
|
162
|
+
collectedCalls,
|
|
163
|
+
normalizedContext,
|
|
164
|
+
executionVariables,
|
|
165
|
+
typeCheckResult
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async executeContractCalls(collectedCalls, executionVariables, resolvedClient) {
|
|
169
|
+
debug("evaluate: calls to execute — %o", collectedCalls.map((c) => `${c.contract}.${c.method}`));
|
|
170
|
+
const plan = require_round_planner.planRounds(require_dependency_analyzer.analyzeDependencies(collectedCalls), {
|
|
171
|
+
maxRounds: this.maxRounds,
|
|
172
|
+
maxCalls: this.maxCalls
|
|
173
|
+
});
|
|
174
|
+
const executor = new require_multi_round_executor.MultiRoundExecutor(resolvedClient, require_contract_caller.buildContractInfoMap(this.schema.contracts), this.multicallOptions);
|
|
175
|
+
let executionResult;
|
|
176
|
+
try {
|
|
177
|
+
executionResult = await executor.execute(plan, executionVariables, await require_contract_caller.resolveExecutionBlockNumber(resolvedClient));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
let failedCall = collectedCalls[0];
|
|
180
|
+
if (error instanceof require_errors.MulticallBatchError) {
|
|
181
|
+
if (error.contractName && error.methodName) {
|
|
182
|
+
const contractName = error.contractName;
|
|
183
|
+
const methodName = error.methodName;
|
|
184
|
+
failedCall = {
|
|
185
|
+
...failedCall ?? {
|
|
186
|
+
id: "",
|
|
187
|
+
contract: contractName,
|
|
188
|
+
method: methodName,
|
|
189
|
+
args: [],
|
|
190
|
+
astNode: void 0
|
|
191
|
+
},
|
|
192
|
+
contract: contractName,
|
|
193
|
+
method: methodName
|
|
194
|
+
};
|
|
195
|
+
} else if (typeof error.failedCallIndex === "number") failedCall = collectedCalls[error.failedCallIndex] ?? failedCall;
|
|
196
|
+
}
|
|
197
|
+
if (!failedCall) throw error;
|
|
198
|
+
throw new require_errors.SELContractError(`Contract call failed: ${failedCall.contract}.${failedCall.method}`, {
|
|
199
|
+
cause: error,
|
|
200
|
+
contractName: failedCall.contract,
|
|
201
|
+
methodName: failedCall.method
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
executionMeta: executionResult.meta,
|
|
206
|
+
executionCache: require_replay_cache.buildExecutionReplayCache(collectedCalls, executionResult.results, executionVariables, this.codecRegistry, (contract, method) => {
|
|
207
|
+
return (this.findContract(contract)?.methods.find((m) => m.name === method))?.params.map((p) => p.type) ?? [];
|
|
208
|
+
})
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async doEvaluate(expression, context, options) {
|
|
212
|
+
debug("evaluate: %s", expression);
|
|
213
|
+
const resolvedClient = options?.client ?? this.client;
|
|
214
|
+
try {
|
|
215
|
+
const { parseResult, collectedCalls, normalizedContext, executionVariables, typeCheckResult } = this.planExecution(expression, context ?? {});
|
|
216
|
+
let executionMeta;
|
|
217
|
+
let executionCache;
|
|
218
|
+
if (collectedCalls.length > 0) {
|
|
219
|
+
if (!resolvedClient) throw new require_errors.SELContractError("No client provided for contract call. Provide a client in SELRuntime config or evaluate() options.", {
|
|
220
|
+
contractName: collectedCalls[0]?.contract,
|
|
221
|
+
methodName: collectedCalls[0]?.method
|
|
222
|
+
});
|
|
223
|
+
({executionMeta, executionCache} = await this.executeContractCalls(collectedCalls, executionVariables, resolvedClient));
|
|
224
|
+
}
|
|
225
|
+
this.currentCache = executionCache;
|
|
226
|
+
this.currentClient = resolvedClient;
|
|
227
|
+
this.currentCallCounter = new require_contract_caller.CallCounter(this.maxCalls, executionMeta?.totalCalls ?? 0);
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = await parseResult({
|
|
231
|
+
...normalizedContext,
|
|
232
|
+
...this.contractBindings
|
|
233
|
+
});
|
|
234
|
+
} finally {
|
|
235
|
+
this.currentCache = void 0;
|
|
236
|
+
this.currentClient = void 0;
|
|
237
|
+
this.currentCallCounter = void 0;
|
|
238
|
+
}
|
|
239
|
+
const value = typeCheckResult.type ? this.codecRegistry.encode(typeCheckResult.type, result) : result;
|
|
240
|
+
debug("evaluate: result type=%s", typeof value);
|
|
241
|
+
return executionMeta ? {
|
|
242
|
+
value,
|
|
243
|
+
meta: executionMeta
|
|
244
|
+
} : { value };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw require_error_wrapper.wrapError(error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
findContract(name) {
|
|
250
|
+
return this.schema.contracts.find((c) => c.name === name);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
exports.SELRuntime = SELRuntime;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SELRuntimeConfig } from "./types.cjs";
|
|
2
|
+
import { EvaluateOptions, EvaluateResult } from "../execution/types.cjs";
|
|
3
|
+
import { TypeCheckResult } from "@marcbachmann/cel-js";
|
|
4
|
+
|
|
5
|
+
//#region src/environment/environment.d.ts
|
|
6
|
+
declare class SELRuntime {
|
|
7
|
+
private readonly env;
|
|
8
|
+
private readonly client?;
|
|
9
|
+
private readonly schema;
|
|
10
|
+
private readonly variableTypes;
|
|
11
|
+
private readonly maxRounds;
|
|
12
|
+
private readonly maxCalls;
|
|
13
|
+
private readonly multicallOptions?;
|
|
14
|
+
private readonly contractBindings;
|
|
15
|
+
private readonly codecRegistry;
|
|
16
|
+
/**
|
|
17
|
+
* Per-evaluation mutable state.
|
|
18
|
+
*
|
|
19
|
+
* These fields are set before CEL evaluation and cleared in `finally`.
|
|
20
|
+
* They exist because the contract call handler closure (created in the
|
|
21
|
+
* constructor) needs access to per-evaluation context, but CEL's
|
|
22
|
+
* `Environment.parse()` returns a function that doesn't accept extra
|
|
23
|
+
* parameters beyond the variable bindings.
|
|
24
|
+
*
|
|
25
|
+
* Thread safety: The `mutex` field serializes concurrent `evaluate()`
|
|
26
|
+
* calls, ensuring these fields are never accessed concurrently.
|
|
27
|
+
*
|
|
28
|
+
* TODO: Consider refactoring to pass an EvaluationContext object through
|
|
29
|
+
* the handler closure instead of mutating instance state. This would
|
|
30
|
+
* eliminate the need for the mutex and make the code more testable.
|
|
31
|
+
* See .omc/drafts/context-object-spike.md for preliminary analysis.
|
|
32
|
+
*/
|
|
33
|
+
private currentCache?;
|
|
34
|
+
private currentClient?;
|
|
35
|
+
private currentCallCounter?;
|
|
36
|
+
/** Mutex to serialize concurrent evaluate() calls (protects current* fields) */
|
|
37
|
+
private mutex;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new immutable SEL runtime with Solidity types pre-registered.
|
|
40
|
+
*
|
|
41
|
+
* Initializes the underlying CEL runtime, registers all Solidity primitive types,
|
|
42
|
+
* and processes any contracts and variables from the schema. After construction,
|
|
43
|
+
* the environment is fully configured and immutable.
|
|
44
|
+
*
|
|
45
|
+
* @param config - Configuration containing the SEL schema, optional viem client,
|
|
46
|
+
* multicall settings, and CEL/execution limits (maxRounds defaults to 10, maxCalls to 100)
|
|
47
|
+
*/
|
|
48
|
+
constructor(config: SELRuntimeConfig);
|
|
49
|
+
/**
|
|
50
|
+
* Type-checks an expression against registered variables and contract methods.
|
|
51
|
+
*
|
|
52
|
+
* @param expression - A CEL expression string to type-check
|
|
53
|
+
* @returns The type-check result containing validity, inferred type, and any errors
|
|
54
|
+
* @throws {@link SELTypeError} If the expression contains unrecoverable type errors
|
|
55
|
+
*/
|
|
56
|
+
check(expression: string): TypeCheckResult;
|
|
57
|
+
/**
|
|
58
|
+
* Evaluates a SEL expression, executing any embedded contract calls on-chain.
|
|
59
|
+
*
|
|
60
|
+
* When contract calls are present, the full pipeline runs:
|
|
61
|
+
* 1. Parse the expression and collect contract calls from the AST
|
|
62
|
+
* 2. Type-check against registered variables and contract methods
|
|
63
|
+
* 3. Build a dependency graph and plan execution rounds
|
|
64
|
+
* 4. Execute contract calls via multicall3 batching at a pinned block number
|
|
65
|
+
* 5. Evaluate the CEL expression with resolved contract results and context values
|
|
66
|
+
* 6. Unwrap Solidity wrapper types back to native JS values (BigInt, string, etc.)
|
|
67
|
+
*
|
|
68
|
+
* @param expression A CEL expression string
|
|
69
|
+
* @param context Variable bindings for evaluation
|
|
70
|
+
* @param options Optional client override
|
|
71
|
+
* @returns An {@link EvaluateResult} containing the value and optional execution metadata
|
|
72
|
+
* @throws {@link SELParseError} If the expression has invalid syntax
|
|
73
|
+
* @throws {@link SELTypeError} If type-checking fails
|
|
74
|
+
* @throws {@link SELContractError} If a contract call fails or no client is available
|
|
75
|
+
* @throws {@link SELEvaluationError} If CEL evaluation fails
|
|
76
|
+
*/
|
|
77
|
+
evaluate<T = unknown>(expression: string, context?: Record<string, unknown>, options?: EvaluateOptions): Promise<EvaluateResult<T>>;
|
|
78
|
+
private planExecution;
|
|
79
|
+
private executeContractCalls;
|
|
80
|
+
private doEvaluate;
|
|
81
|
+
private findContract;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { SELRuntime };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SELRuntimeConfig } from "./types.mjs";
|
|
2
|
+
import { EvaluateOptions, EvaluateResult } from "../execution/types.mjs";
|
|
3
|
+
import { TypeCheckResult } from "@marcbachmann/cel-js";
|
|
4
|
+
|
|
5
|
+
//#region src/environment/environment.d.ts
|
|
6
|
+
declare class SELRuntime {
|
|
7
|
+
private readonly env;
|
|
8
|
+
private readonly client?;
|
|
9
|
+
private readonly schema;
|
|
10
|
+
private readonly variableTypes;
|
|
11
|
+
private readonly maxRounds;
|
|
12
|
+
private readonly maxCalls;
|
|
13
|
+
private readonly multicallOptions?;
|
|
14
|
+
private readonly contractBindings;
|
|
15
|
+
private readonly codecRegistry;
|
|
16
|
+
/**
|
|
17
|
+
* Per-evaluation mutable state.
|
|
18
|
+
*
|
|
19
|
+
* These fields are set before CEL evaluation and cleared in `finally`.
|
|
20
|
+
* They exist because the contract call handler closure (created in the
|
|
21
|
+
* constructor) needs access to per-evaluation context, but CEL's
|
|
22
|
+
* `Environment.parse()` returns a function that doesn't accept extra
|
|
23
|
+
* parameters beyond the variable bindings.
|
|
24
|
+
*
|
|
25
|
+
* Thread safety: The `mutex` field serializes concurrent `evaluate()`
|
|
26
|
+
* calls, ensuring these fields are never accessed concurrently.
|
|
27
|
+
*
|
|
28
|
+
* TODO: Consider refactoring to pass an EvaluationContext object through
|
|
29
|
+
* the handler closure instead of mutating instance state. This would
|
|
30
|
+
* eliminate the need for the mutex and make the code more testable.
|
|
31
|
+
* See .omc/drafts/context-object-spike.md for preliminary analysis.
|
|
32
|
+
*/
|
|
33
|
+
private currentCache?;
|
|
34
|
+
private currentClient?;
|
|
35
|
+
private currentCallCounter?;
|
|
36
|
+
/** Mutex to serialize concurrent evaluate() calls (protects current* fields) */
|
|
37
|
+
private mutex;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new immutable SEL runtime with Solidity types pre-registered.
|
|
40
|
+
*
|
|
41
|
+
* Initializes the underlying CEL runtime, registers all Solidity primitive types,
|
|
42
|
+
* and processes any contracts and variables from the schema. After construction,
|
|
43
|
+
* the environment is fully configured and immutable.
|
|
44
|
+
*
|
|
45
|
+
* @param config - Configuration containing the SEL schema, optional viem client,
|
|
46
|
+
* multicall settings, and CEL/execution limits (maxRounds defaults to 10, maxCalls to 100)
|
|
47
|
+
*/
|
|
48
|
+
constructor(config: SELRuntimeConfig);
|
|
49
|
+
/**
|
|
50
|
+
* Type-checks an expression against registered variables and contract methods.
|
|
51
|
+
*
|
|
52
|
+
* @param expression - A CEL expression string to type-check
|
|
53
|
+
* @returns The type-check result containing validity, inferred type, and any errors
|
|
54
|
+
* @throws {@link SELTypeError} If the expression contains unrecoverable type errors
|
|
55
|
+
*/
|
|
56
|
+
check(expression: string): TypeCheckResult;
|
|
57
|
+
/**
|
|
58
|
+
* Evaluates a SEL expression, executing any embedded contract calls on-chain.
|
|
59
|
+
*
|
|
60
|
+
* When contract calls are present, the full pipeline runs:
|
|
61
|
+
* 1. Parse the expression and collect contract calls from the AST
|
|
62
|
+
* 2. Type-check against registered variables and contract methods
|
|
63
|
+
* 3. Build a dependency graph and plan execution rounds
|
|
64
|
+
* 4. Execute contract calls via multicall3 batching at a pinned block number
|
|
65
|
+
* 5. Evaluate the CEL expression with resolved contract results and context values
|
|
66
|
+
* 6. Unwrap Solidity wrapper types back to native JS values (BigInt, string, etc.)
|
|
67
|
+
*
|
|
68
|
+
* @param expression A CEL expression string
|
|
69
|
+
* @param context Variable bindings for evaluation
|
|
70
|
+
* @param options Optional client override
|
|
71
|
+
* @returns An {@link EvaluateResult} containing the value and optional execution metadata
|
|
72
|
+
* @throws {@link SELParseError} If the expression has invalid syntax
|
|
73
|
+
* @throws {@link SELTypeError} If type-checking fails
|
|
74
|
+
* @throws {@link SELContractError} If a contract call fails or no client is available
|
|
75
|
+
* @throws {@link SELEvaluationError} If CEL evaluation fails
|
|
76
|
+
*/
|
|
77
|
+
evaluate<T = unknown>(expression: string, context?: Record<string, unknown>, options?: EvaluateOptions): Promise<EvaluateResult<T>>;
|
|
78
|
+
private planExecution;
|
|
79
|
+
private executeContractCalls;
|
|
80
|
+
private doEvaluate;
|
|
81
|
+
private findContract;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { SELRuntime };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { normalizeContextForEvaluation } from "./context.mjs";
|
|
2
|
+
import { buildExecutionReplayCache } from "./replay-cache.mjs";
|
|
3
|
+
import { createLogger } from "../debug.mjs";
|
|
4
|
+
import { MulticallBatchError, SELContractError, SELTypeError } from "../errors/errors.mjs";
|
|
5
|
+
import "../errors/index.mjs";
|
|
6
|
+
import { CallCounter, buildContractInfoMap, executeContractCall, resolveExecutionBlockNumber } from "./contract-caller.mjs";
|
|
7
|
+
import { wrapError } from "./error-wrapper.mjs";
|
|
8
|
+
import { collectCalls } from "../analysis/call-collector.mjs";
|
|
9
|
+
import { analyzeDependencies } from "../analysis/dependency-analyzer.mjs";
|
|
10
|
+
import { planRounds } from "../analysis/round-planner.mjs";
|
|
11
|
+
import { MultiRoundExecutor } from "../execution/multi-round-executor.mjs";
|
|
12
|
+
import { createRuntimeEnvironment } from "@seljs/checker";
|
|
13
|
+
//#region src/environment/environment.ts
|
|
14
|
+
/**
|
|
15
|
+
* Core SEL runtime that bridges CEL expression evaluation with EVM contract reads.
|
|
16
|
+
*
|
|
17
|
+
* Manages a CEL runtime extended with Solidity primitive types (uint256, address, bool, etc.),
|
|
18
|
+
* registered smart contracts whose view functions become callable in expressions, and typed
|
|
19
|
+
* variables that are supplied at evaluation time. Contract calls are automatically batched
|
|
20
|
+
* via multicall3 and executed in dependency-ordered rounds.
|
|
21
|
+
*/
|
|
22
|
+
const debug = createLogger("environment");
|
|
23
|
+
var SELRuntime = class {
|
|
24
|
+
env;
|
|
25
|
+
client;
|
|
26
|
+
schema;
|
|
27
|
+
variableTypes = /* @__PURE__ */ new Map();
|
|
28
|
+
maxRounds;
|
|
29
|
+
maxCalls;
|
|
30
|
+
multicallOptions;
|
|
31
|
+
contractBindings;
|
|
32
|
+
codecRegistry;
|
|
33
|
+
/**
|
|
34
|
+
* Per-evaluation mutable state.
|
|
35
|
+
*
|
|
36
|
+
* These fields are set before CEL evaluation and cleared in `finally`.
|
|
37
|
+
* They exist because the contract call handler closure (created in the
|
|
38
|
+
* constructor) needs access to per-evaluation context, but CEL's
|
|
39
|
+
* `Environment.parse()` returns a function that doesn't accept extra
|
|
40
|
+
* parameters beyond the variable bindings.
|
|
41
|
+
*
|
|
42
|
+
* Thread safety: The `mutex` field serializes concurrent `evaluate()`
|
|
43
|
+
* calls, ensuring these fields are never accessed concurrently.
|
|
44
|
+
*
|
|
45
|
+
* TODO: Consider refactoring to pass an EvaluationContext object through
|
|
46
|
+
* the handler closure instead of mutating instance state. This would
|
|
47
|
+
* eliminate the need for the mutex and make the code more testable.
|
|
48
|
+
* See .omc/drafts/context-object-spike.md for preliminary analysis.
|
|
49
|
+
*/
|
|
50
|
+
currentCache;
|
|
51
|
+
currentClient;
|
|
52
|
+
currentCallCounter;
|
|
53
|
+
/** Mutex to serialize concurrent evaluate() calls (protects current* fields) */
|
|
54
|
+
mutex = Promise.resolve();
|
|
55
|
+
/**
|
|
56
|
+
* Creates a new immutable SEL runtime with Solidity types pre-registered.
|
|
57
|
+
*
|
|
58
|
+
* Initializes the underlying CEL runtime, registers all Solidity primitive types,
|
|
59
|
+
* and processes any contracts and variables from the schema. After construction,
|
|
60
|
+
* the environment is fully configured and immutable.
|
|
61
|
+
*
|
|
62
|
+
* @param config - Configuration containing the SEL schema, optional viem client,
|
|
63
|
+
* multicall settings, and CEL/execution limits (maxRounds defaults to 10, maxCalls to 100)
|
|
64
|
+
*/
|
|
65
|
+
constructor(config) {
|
|
66
|
+
const limits = config.limits;
|
|
67
|
+
const celLimits = limits ? {
|
|
68
|
+
maxAstNodes: limits.maxAstNodes,
|
|
69
|
+
maxDepth: limits.maxDepth,
|
|
70
|
+
maxListElements: limits.maxListElements,
|
|
71
|
+
maxMapEntries: limits.maxMapEntries,
|
|
72
|
+
maxCallArguments: limits.maxCallArguments
|
|
73
|
+
} : void 0;
|
|
74
|
+
this.maxRounds = limits?.maxRounds ?? 10;
|
|
75
|
+
this.maxCalls = limits?.maxCalls ?? 100;
|
|
76
|
+
this.multicallOptions = config.multicall;
|
|
77
|
+
this.client = config.client;
|
|
78
|
+
this.schema = config.schema;
|
|
79
|
+
const handler = (contractName, methodName, args) => {
|
|
80
|
+
const contract = this.findContract(contractName);
|
|
81
|
+
if (!contract) throw new SELContractError(`Unknown contract "${contractName}"`, { contractName });
|
|
82
|
+
const method = contract.methods.find((m) => m.name === methodName);
|
|
83
|
+
if (!method) throw new SELContractError(`Unknown method "${contractName}.${methodName}"`, {
|
|
84
|
+
contractName,
|
|
85
|
+
methodName
|
|
86
|
+
});
|
|
87
|
+
return executeContractCall(contract, method, args, {
|
|
88
|
+
executionCache: this.currentCache,
|
|
89
|
+
client: this.currentClient ?? this.client,
|
|
90
|
+
codecRegistry: this.codecRegistry,
|
|
91
|
+
callCounter: this.currentCallCounter
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const { env, contractBindings, codecRegistry } = createRuntimeEnvironment(this.schema, handler, {
|
|
95
|
+
limits: celLimits,
|
|
96
|
+
unlistedVariablesAreDyn: true
|
|
97
|
+
});
|
|
98
|
+
this.env = env;
|
|
99
|
+
this.contractBindings = contractBindings;
|
|
100
|
+
this.codecRegistry = codecRegistry;
|
|
101
|
+
for (const v of this.schema.variables) this.variableTypes.set(v.name, v.type);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Type-checks an expression against registered variables and contract methods.
|
|
105
|
+
*
|
|
106
|
+
* @param expression - A CEL expression string to type-check
|
|
107
|
+
* @returns The type-check result containing validity, inferred type, and any errors
|
|
108
|
+
* @throws {@link SELTypeError} If the expression contains unrecoverable type errors
|
|
109
|
+
*/
|
|
110
|
+
check(expression) {
|
|
111
|
+
try {
|
|
112
|
+
return this.env.check(expression);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw wrapError(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Evaluates a SEL expression, executing any embedded contract calls on-chain.
|
|
119
|
+
*
|
|
120
|
+
* When contract calls are present, the full pipeline runs:
|
|
121
|
+
* 1. Parse the expression and collect contract calls from the AST
|
|
122
|
+
* 2. Type-check against registered variables and contract methods
|
|
123
|
+
* 3. Build a dependency graph and plan execution rounds
|
|
124
|
+
* 4. Execute contract calls via multicall3 batching at a pinned block number
|
|
125
|
+
* 5. Evaluate the CEL expression with resolved contract results and context values
|
|
126
|
+
* 6. Unwrap Solidity wrapper types back to native JS values (BigInt, string, etc.)
|
|
127
|
+
*
|
|
128
|
+
* @param expression A CEL expression string
|
|
129
|
+
* @param context Variable bindings for evaluation
|
|
130
|
+
* @param options Optional client override
|
|
131
|
+
* @returns An {@link EvaluateResult} containing the value and optional execution metadata
|
|
132
|
+
* @throws {@link SELParseError} If the expression has invalid syntax
|
|
133
|
+
* @throws {@link SELTypeError} If type-checking fails
|
|
134
|
+
* @throws {@link SELContractError} If a contract call fails or no client is available
|
|
135
|
+
* @throws {@link SELEvaluationError} If CEL evaluation fails
|
|
136
|
+
*/
|
|
137
|
+
async evaluate(expression, context, options) {
|
|
138
|
+
let release;
|
|
139
|
+
const prev = this.mutex;
|
|
140
|
+
this.mutex = new Promise((resolve) => {
|
|
141
|
+
release = resolve;
|
|
142
|
+
});
|
|
143
|
+
await prev;
|
|
144
|
+
try {
|
|
145
|
+
return await this.doEvaluate(expression, context, options);
|
|
146
|
+
} finally {
|
|
147
|
+
release();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
planExecution(expression, evaluationContext) {
|
|
151
|
+
const parseResult = this.env.parse(expression);
|
|
152
|
+
const collectedCalls = collectCalls(parseResult.ast, { get: (name) => this.findContract(name) });
|
|
153
|
+
debug("evaluate: collected %d calls", collectedCalls.length);
|
|
154
|
+
const normalizedContext = Object.keys(evaluationContext).length ? normalizeContextForEvaluation(evaluationContext, this.variableTypes, this.codecRegistry) : void 0;
|
|
155
|
+
const executionVariables = Object.keys(evaluationContext).length ? evaluationContext : {};
|
|
156
|
+
const typeCheckResult = parseResult.check();
|
|
157
|
+
if (!typeCheckResult.valid) throw new SELTypeError(typeCheckResult.error?.message ?? "Type check failed", { cause: typeCheckResult.error });
|
|
158
|
+
return {
|
|
159
|
+
parseResult,
|
|
160
|
+
collectedCalls,
|
|
161
|
+
normalizedContext,
|
|
162
|
+
executionVariables,
|
|
163
|
+
typeCheckResult
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async executeContractCalls(collectedCalls, executionVariables, resolvedClient) {
|
|
167
|
+
debug("evaluate: calls to execute — %o", collectedCalls.map((c) => `${c.contract}.${c.method}`));
|
|
168
|
+
const plan = planRounds(analyzeDependencies(collectedCalls), {
|
|
169
|
+
maxRounds: this.maxRounds,
|
|
170
|
+
maxCalls: this.maxCalls
|
|
171
|
+
});
|
|
172
|
+
const executor = new MultiRoundExecutor(resolvedClient, buildContractInfoMap(this.schema.contracts), this.multicallOptions);
|
|
173
|
+
let executionResult;
|
|
174
|
+
try {
|
|
175
|
+
executionResult = await executor.execute(plan, executionVariables, await resolveExecutionBlockNumber(resolvedClient));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
let failedCall = collectedCalls[0];
|
|
178
|
+
if (error instanceof MulticallBatchError) {
|
|
179
|
+
if (error.contractName && error.methodName) {
|
|
180
|
+
const contractName = error.contractName;
|
|
181
|
+
const methodName = error.methodName;
|
|
182
|
+
failedCall = {
|
|
183
|
+
...failedCall ?? {
|
|
184
|
+
id: "",
|
|
185
|
+
contract: contractName,
|
|
186
|
+
method: methodName,
|
|
187
|
+
args: [],
|
|
188
|
+
astNode: void 0
|
|
189
|
+
},
|
|
190
|
+
contract: contractName,
|
|
191
|
+
method: methodName
|
|
192
|
+
};
|
|
193
|
+
} else if (typeof error.failedCallIndex === "number") failedCall = collectedCalls[error.failedCallIndex] ?? failedCall;
|
|
194
|
+
}
|
|
195
|
+
if (!failedCall) throw error;
|
|
196
|
+
throw new SELContractError(`Contract call failed: ${failedCall.contract}.${failedCall.method}`, {
|
|
197
|
+
cause: error,
|
|
198
|
+
contractName: failedCall.contract,
|
|
199
|
+
methodName: failedCall.method
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
executionMeta: executionResult.meta,
|
|
204
|
+
executionCache: buildExecutionReplayCache(collectedCalls, executionResult.results, executionVariables, this.codecRegistry, (contract, method) => {
|
|
205
|
+
return (this.findContract(contract)?.methods.find((m) => m.name === method))?.params.map((p) => p.type) ?? [];
|
|
206
|
+
})
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async doEvaluate(expression, context, options) {
|
|
210
|
+
debug("evaluate: %s", expression);
|
|
211
|
+
const resolvedClient = options?.client ?? this.client;
|
|
212
|
+
try {
|
|
213
|
+
const { parseResult, collectedCalls, normalizedContext, executionVariables, typeCheckResult } = this.planExecution(expression, context ?? {});
|
|
214
|
+
let executionMeta;
|
|
215
|
+
let executionCache;
|
|
216
|
+
if (collectedCalls.length > 0) {
|
|
217
|
+
if (!resolvedClient) throw new SELContractError("No client provided for contract call. Provide a client in SELRuntime config or evaluate() options.", {
|
|
218
|
+
contractName: collectedCalls[0]?.contract,
|
|
219
|
+
methodName: collectedCalls[0]?.method
|
|
220
|
+
});
|
|
221
|
+
({executionMeta, executionCache} = await this.executeContractCalls(collectedCalls, executionVariables, resolvedClient));
|
|
222
|
+
}
|
|
223
|
+
this.currentCache = executionCache;
|
|
224
|
+
this.currentClient = resolvedClient;
|
|
225
|
+
this.currentCallCounter = new CallCounter(this.maxCalls, executionMeta?.totalCalls ?? 0);
|
|
226
|
+
let result;
|
|
227
|
+
try {
|
|
228
|
+
result = await parseResult({
|
|
229
|
+
...normalizedContext,
|
|
230
|
+
...this.contractBindings
|
|
231
|
+
});
|
|
232
|
+
} finally {
|
|
233
|
+
this.currentCache = void 0;
|
|
234
|
+
this.currentClient = void 0;
|
|
235
|
+
this.currentCallCounter = void 0;
|
|
236
|
+
}
|
|
237
|
+
const value = typeCheckResult.type ? this.codecRegistry.encode(typeCheckResult.type, result) : result;
|
|
238
|
+
debug("evaluate: result type=%s", typeof value);
|
|
239
|
+
return executionMeta ? {
|
|
240
|
+
value,
|
|
241
|
+
meta: executionMeta
|
|
242
|
+
} : { value };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw wrapError(error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
findContract(name) {
|
|
248
|
+
return this.schema.contracts.find((c) => c.name === name);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
//#endregion
|
|
252
|
+
export { SELRuntime };
|