@seljs/runtime 1.0.0 → 1.0.1

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  3. package/dist/analysis/call-collector.cjs +233 -0
  4. package/dist/analysis/call-collector.mjs +232 -0
  5. package/dist/analysis/dependency-analyzer.cjs +68 -0
  6. package/dist/analysis/dependency-analyzer.mjs +68 -0
  7. package/dist/analysis/round-planner.cjs +65 -0
  8. package/dist/analysis/round-planner.mjs +65 -0
  9. package/dist/analysis/types.d.cts +115 -0
  10. package/dist/analysis/types.d.mts +115 -0
  11. package/dist/debug.cjs +17 -0
  12. package/dist/debug.mjs +15 -0
  13. package/dist/environment/context.cjs +11 -0
  14. package/dist/environment/context.mjs +11 -0
  15. package/dist/environment/contract-caller.cjs +81 -0
  16. package/dist/environment/contract-caller.mjs +77 -0
  17. package/dist/environment/environment.cjs +254 -0
  18. package/dist/environment/environment.d.cts +84 -0
  19. package/dist/environment/environment.d.mts +84 -0
  20. package/dist/environment/environment.mjs +252 -0
  21. package/dist/environment/error-wrapper.cjs +29 -0
  22. package/dist/environment/error-wrapper.mjs +27 -0
  23. package/dist/environment/index.cjs +1 -0
  24. package/dist/environment/index.d.mts +2 -0
  25. package/dist/environment/index.mjs +2 -0
  26. package/dist/environment/replay-cache.cjs +48 -0
  27. package/dist/environment/replay-cache.mjs +47 -0
  28. package/dist/environment/types.d.cts +60 -0
  29. package/dist/environment/types.d.mts +60 -0
  30. package/dist/errors/errors.cjs +68 -0
  31. package/dist/errors/errors.d.cts +64 -0
  32. package/dist/errors/errors.d.mts +64 -0
  33. package/dist/errors/errors.mjs +63 -0
  34. package/dist/errors/index.cjs +3 -0
  35. package/dist/errors/index.d.mts +1 -0
  36. package/dist/errors/index.mjs +2 -0
  37. package/dist/execution/multi-round-executor.cjs +45 -0
  38. package/dist/execution/multi-round-executor.mjs +44 -0
  39. package/dist/execution/multicall-batcher.cjs +51 -0
  40. package/dist/execution/multicall-batcher.mjs +50 -0
  41. package/dist/execution/multicall.cjs +39 -0
  42. package/dist/execution/multicall.mjs +38 -0
  43. package/dist/execution/result-cache.cjs +63 -0
  44. package/dist/execution/result-cache.mjs +63 -0
  45. package/dist/execution/round-executor.cjs +81 -0
  46. package/dist/execution/round-executor.mjs +80 -0
  47. package/dist/execution/types.d.cts +58 -0
  48. package/dist/execution/types.d.mts +58 -0
  49. package/dist/factory.cjs +6 -0
  50. package/dist/factory.d.cts +7 -0
  51. package/dist/factory.d.mts +6 -0
  52. package/dist/factory.mjs +6 -0
  53. package/dist/index.cjs +18 -0
  54. package/dist/index.d.cts +7 -0
  55. package/dist/index.d.mts +7 -0
  56. package/dist/index.mjs +6 -0
  57. package/package.json +26 -19
  58. package/dist/analysis/call-collector.d.ts +0 -20
  59. package/dist/analysis/call-collector.d.ts.map +0 -1
  60. package/dist/analysis/call-collector.js +0 -272
  61. package/dist/analysis/dependency-analyzer.d.ts +0 -14
  62. package/dist/analysis/dependency-analyzer.d.ts.map +0 -1
  63. package/dist/analysis/dependency-analyzer.js +0 -76
  64. package/dist/analysis/index.d.ts +0 -2
  65. package/dist/analysis/index.d.ts.map +0 -1
  66. package/dist/analysis/index.js +0 -1
  67. package/dist/analysis/round-planner.d.ts +0 -32
  68. package/dist/analysis/round-planner.d.ts.map +0 -1
  69. package/dist/analysis/round-planner.js +0 -69
  70. package/dist/analysis/types.d.ts +0 -113
  71. package/dist/analysis/types.d.ts.map +0 -1
  72. package/dist/analysis/types.js +0 -1
  73. package/dist/debug.d.ts +0 -13
  74. package/dist/debug.d.ts.map +0 -1
  75. package/dist/debug.js +0 -12
  76. package/dist/environment/context.d.ts +0 -3
  77. package/dist/environment/context.d.ts.map +0 -1
  78. package/dist/environment/context.js +0 -8
  79. package/dist/environment/contract-caller.d.ts +0 -25
  80. package/dist/environment/contract-caller.d.ts.map +0 -1
  81. package/dist/environment/contract-caller.js +0 -85
  82. package/dist/environment/environment.d.ts +0 -81
  83. package/dist/environment/environment.d.ts.map +0 -1
  84. package/dist/environment/environment.js +0 -279
  85. package/dist/environment/error-wrapper.d.ts +0 -11
  86. package/dist/environment/error-wrapper.d.ts.map +0 -1
  87. package/dist/environment/error-wrapper.js +0 -33
  88. package/dist/environment/index.d.ts +0 -3
  89. package/dist/environment/index.d.ts.map +0 -1
  90. package/dist/environment/index.js +0 -2
  91. package/dist/environment/replay-cache.d.ts +0 -23
  92. package/dist/environment/replay-cache.d.ts.map +0 -1
  93. package/dist/environment/replay-cache.js +0 -51
  94. package/dist/environment/types.d.ts +0 -57
  95. package/dist/environment/types.d.ts.map +0 -1
  96. package/dist/environment/types.js +0 -1
  97. package/dist/errors/errors.d.ts +0 -63
  98. package/dist/errors/errors.d.ts.map +0 -1
  99. package/dist/errors/errors.js +0 -63
  100. package/dist/errors/index.d.ts +0 -2
  101. package/dist/errors/index.d.ts.map +0 -1
  102. package/dist/errors/index.js +0 -1
  103. package/dist/execution/index.d.ts +0 -2
  104. package/dist/execution/index.d.ts.map +0 -1
  105. package/dist/execution/index.js +0 -1
  106. package/dist/execution/multi-round-executor.d.ts +0 -17
  107. package/dist/execution/multi-round-executor.d.ts.map +0 -1
  108. package/dist/execution/multi-round-executor.js +0 -47
  109. package/dist/execution/multicall-batcher.d.ts +0 -14
  110. package/dist/execution/multicall-batcher.d.ts.map +0 -1
  111. package/dist/execution/multicall-batcher.js +0 -53
  112. package/dist/execution/multicall.d.ts +0 -42
  113. package/dist/execution/multicall.d.ts.map +0 -1
  114. package/dist/execution/multicall.js +0 -29
  115. package/dist/execution/result-cache.d.ts +0 -47
  116. package/dist/execution/result-cache.d.ts.map +0 -1
  117. package/dist/execution/result-cache.js +0 -65
  118. package/dist/execution/round-executor.d.ts +0 -18
  119. package/dist/execution/round-executor.d.ts.map +0 -1
  120. package/dist/execution/round-executor.js +0 -95
  121. package/dist/execution/types.d.ts +0 -55
  122. package/dist/execution/types.d.ts.map +0 -1
  123. package/dist/execution/types.js +0 -1
  124. package/dist/factory.d.ts +0 -3
  125. package/dist/factory.d.ts.map +0 -1
  126. package/dist/factory.js +0 -2
  127. package/dist/index.d.ts +0 -10
  128. package/dist/index.d.ts.map +0 -1
  129. 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 };