@callcium/sdk 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,600 @@
1
+ import { hexToBytes, bytesToHex, readU16, readU32, bigintToHex } from "./bytes";
2
+ import { loadWord, readPointer } from "./calldata-reader";
3
+ import {
4
+ PolicyFormat as PF,
5
+ Scope,
6
+ MAX_CONTEXT_PROPERTY_ID,
7
+ Limits,
8
+ TypeCode,
9
+ classifyTypeCode,
10
+ lookupContextProperty,
11
+ } from "./constants";
12
+ import { CallciumError, PolicyViolationError } from "./errors";
13
+ import { applyOperator, toBigInt, isLengthOp, isLengthValidType, canonicalize } from "./operators";
14
+ import { decodePolicy } from "./policy-coder";
15
+
16
+ import type { ReadResult } from "./calldata-reader";
17
+ import type {
18
+ Context,
19
+ DecodedRule,
20
+ EnforceResult,
21
+ Hex,
22
+ NavigationViolationCode,
23
+ Span,
24
+ Violation,
25
+ ViolationCode,
26
+ } from "./types";
27
+
28
+ ///////////////////////////////////////////////////////////////////////////
29
+ // Helpers
30
+ ///////////////////////////////////////////////////////////////////////////
31
+
32
+ /** Violations that abort evaluation instead of failing only their group (spec §9.3). */
33
+ const ABORT_VIOLATION_CODES: ReadonlySet<ViolationCode> = new Set<ViolationCode>([
34
+ "CALLDATA_OUT_OF_BOUNDS",
35
+ "ARRAY_INDEX_OUT_OF_BOUNDS",
36
+ "NON_CANONICAL_VALUE",
37
+ "QUANTIFIER_LIMIT_EXCEEDED",
38
+ ]);
39
+
40
+ /** Convert a hex address to a 256-bit bigint (zero-padded to 32 bytes). */
41
+ function addressToBigInt(hex: string): bigint {
42
+ const bytes = hexToBytes(hex);
43
+ const padded = new Uint8Array(32);
44
+ padded.set(bytes, 32 - bytes.length);
45
+ return toBigInt(padded, 0);
46
+ }
47
+
48
+ ///////////////////////////////////////////////////////////////////////////
49
+ // Public API
50
+ ///////////////////////////////////////////////////////////////////////////
51
+
52
+ /**
53
+ * Check a policy against ABI-encoded call data without throwing on violations.
54
+ * @param policy - Binary policy blob as 0x-prefixed hex string.
55
+ * @param callData - ABI-encoded call data as 0x-prefixed hex string.
56
+ * @param context - Optional execution context for context-scoped rules.
57
+ * @returns Pass with matched group index, or fail with one violation per evaluated failing group.
58
+ * @throws {CallciumError} If the policy blob is structurally malformed.
59
+ */
60
+ function check(policy: Hex, callData: Hex, context?: Context): EnforceResult {
61
+ const { policy: decoded, data: policyBytes } = decodePolicy(policy);
62
+ const callDataBytes = hexToBytes(callData);
63
+
64
+ // Selector check.
65
+ if (!decoded.isSelectorless) {
66
+ if (callDataBytes.length < 4) {
67
+ return {
68
+ ok: false,
69
+ violations: [{ code: "MISSING_SELECTOR" }],
70
+ };
71
+ }
72
+ const expectedSelector = decoded.selector.value;
73
+ const actualSelector = bytesToHex(callDataBytes.subarray(0, 4));
74
+ if (actualSelector !== expectedSelector) {
75
+ return {
76
+ ok: false,
77
+ violations: [
78
+ {
79
+ code: "SELECTOR_MISMATCH",
80
+ resolvedValue: actualSelector,
81
+ expectedValue: expectedSelector,
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ }
87
+
88
+ const baseOffset = decoded.isSelectorless ? 0 : PF.SELECTOR_SIZE;
89
+
90
+ // Evaluate groups with OR semantics.
91
+ const allViolations: Violation[] = [];
92
+
93
+ for (let groupIndex = 0; groupIndex < decoded.groups.length; groupIndex++) {
94
+ const group = decoded.groups[groupIndex]!;
95
+ let groupFailed = false;
96
+
97
+ for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex++) {
98
+ const rule = group.rules[ruleIndex]!;
99
+ const violation = evaluateRule(rule, policyBytes, callDataBytes, baseOffset, groupIndex, ruleIndex, context);
100
+ if (violation !== null) {
101
+ allViolations.push(violation);
102
+ if (ABORT_VIOLATION_CODES.has(violation.code)) {
103
+ // Abort violations reject the policy outright; later groups are not consulted (spec §9.3).
104
+ return { ok: false, violations: allViolations };
105
+ }
106
+ groupFailed = true;
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (!groupFailed) {
112
+ return { ok: true, matchedGroup: groupIndex };
113
+ }
114
+ }
115
+
116
+ return { ok: false, violations: allViolations };
117
+ }
118
+
119
+ /**
120
+ * Enforce a policy against ABI-encoded call data, throwing on violation.
121
+ * @param policy - Binary policy blob as 0x-prefixed hex string.
122
+ * @param callData - ABI-encoded call data as 0x-prefixed hex string.
123
+ * @param context - Optional execution context for context-scoped rules.
124
+ * @throws {PolicyViolationError} If the policy rejects the call data.
125
+ * @throws {CallciumError} If the policy blob is structurally malformed.
126
+ */
127
+ function enforce(policy: Hex, callData: Hex, context?: Context): void {
128
+ const result = check(policy, callData, context);
129
+ if (!result.ok) {
130
+ throw new PolicyViolationError(result.violations);
131
+ }
132
+ }
133
+
134
+ ///////////////////////////////////////////////////////////////////////////
135
+ // Rule evaluation
136
+ ///////////////////////////////////////////////////////////////////////////
137
+
138
+ /** Read the blob bytes a decoded field spans. */
139
+ function fieldBytes(policyBytes: Uint8Array, field: { span: Span }): Uint8Array {
140
+ return policyBytes.subarray(field.span.start, field.span.end);
141
+ }
142
+
143
+ /** Evaluate a single rule against calldata or context, returning a violation or null on pass. */
144
+ function evaluateRule(
145
+ rule: DecodedRule,
146
+ policyBytes: Uint8Array,
147
+ callDataBytes: Uint8Array,
148
+ baseOffset: number,
149
+ groupIndex: number,
150
+ ruleIndex: number,
151
+ context?: Context,
152
+ ): Violation | null {
153
+ const opCode = rule.opCode.value;
154
+ const operandData = fieldBytes(policyBytes, rule.data);
155
+
156
+ const scope = rule.scope.value;
157
+ if (scope === Scope.CONTEXT) {
158
+ const pathBytes = fieldBytes(policyBytes, rule.path);
159
+ return evaluateContextRule(pathBytes, opCode, operandData, groupIndex, ruleIndex, rule.path.value, context);
160
+ }
161
+
162
+ // The hint addresses the target on its own; path bytes carry no evaluation role.
163
+ if (rule.hint === undefined) {
164
+ throw new CallciumError("MALFORMED_HINT", "Calldata rule carries no hint block");
165
+ }
166
+ return evaluateCalldataRule(
167
+ fieldBytes(policyBytes, rule.hint),
168
+ callDataBytes,
169
+ baseOffset,
170
+ opCode,
171
+ operandData,
172
+ groupIndex,
173
+ ruleIndex,
174
+ rule.path.value,
175
+ );
176
+ }
177
+
178
+ ///////////////////////////////////////////////////////////////////////////
179
+ // Hint chain resolution
180
+ ///////////////////////////////////////////////////////////////////////////
181
+
182
+ /** Fields of a hint's target block. */
183
+ type TargetBlock = {
184
+ targetDelta: number;
185
+ targetMeta: number;
186
+ typeCode: number;
187
+ };
188
+
189
+ /** Read the target block at `targetOffset` in the hint. */
190
+ function readTargetBlock(hint: Uint8Array, targetOffset: number): TargetBlock {
191
+ return {
192
+ targetDelta: readU32(hint, targetOffset),
193
+ targetMeta: readU16(hint, targetOffset + PF.HINT_TARGET_META_OFFSET),
194
+ typeCode: hint[targetOffset + PF.HINT_TARGET_TYPECODE_OFFSET]!,
195
+ };
196
+ }
197
+
198
+ /** Advance `from` by the offset word at `at`. */
199
+ function follow(callData: Uint8Array, from: number, at: number): ReadResult<number> {
200
+ const word = readPointer(callData, at);
201
+ if (typeof word !== "number") return word;
202
+ return from + word;
203
+ }
204
+
205
+ /** Follow `hopCount` hop entries from `base` and return the calldata offset they reach. */
206
+ function chainResolve(
207
+ hint: Uint8Array,
208
+ callData: Uint8Array,
209
+ base: number,
210
+ hopsOffset: number,
211
+ hopCount: number,
212
+ ): ReadResult<number> {
213
+ for (let i = 0; i < hopCount; i++) {
214
+ const hop = hopsOffset + i * PF.HINT_HOP_SIZE;
215
+ const index = readU16(hint, hop + PF.HINT_HOP_INDEX_OFFSET);
216
+
217
+ if (index === PF.HINT_NO_INDEX) {
218
+ const next = follow(callData, base, base + readU32(hint, hop));
219
+ if (typeof next !== "number") return next;
220
+ base = next;
221
+ continue;
222
+ }
223
+
224
+ const meta = readU16(hint, hop + PF.HINT_HOP_META_OFFSET);
225
+ let elems = base;
226
+ if ((meta & PF.HINT_META_DYNAMIC_ARRAY) !== 0) {
227
+ const length = readPointer(callData, elems);
228
+ if (typeof length !== "number") return length;
229
+ if (index >= length) return { code: "ARRAY_INDEX_OUT_OF_BOUNDS" };
230
+ elems += 32;
231
+ }
232
+
233
+ const slot = elems + index * (meta & PF.HINT_META_STRIDE_MASK) * 32;
234
+ if ((meta & PF.HINT_META_ELEM_DYNAMIC) !== 0) {
235
+ const next = follow(callData, elems, slot);
236
+ if (typeof next !== "number") return next;
237
+ base = next;
238
+ } else {
239
+ base = slot;
240
+ }
241
+ }
242
+ return base;
243
+ }
244
+
245
+ ///////////////////////////////////////////////////////////////////////////
246
+ // Target evaluation
247
+ ///////////////////////////////////////////////////////////////////////////
248
+
249
+ type TargetResult =
250
+ | { passed: boolean; value: bigint }
251
+ | { error: NavigationViolationCode }
252
+ | { error: "NON_CANONICAL_VALUE"; value: bigint };
253
+
254
+ /**
255
+ * Load the value at a resolved target offset and apply the operator.
256
+ *
257
+ * `value` is normalised at the decision point: length operators yield the byte/element
258
+ * count; scalar operators preserve the full 32-byte ABI word, keeping left-aligned `bytesN`
259
+ * decodable. A passing target allocates no string.
260
+ */
261
+ function evalTarget(
262
+ callData: Uint8Array,
263
+ target: number,
264
+ block: TargetBlock,
265
+ opCode: number,
266
+ operandData: Uint8Array,
267
+ ): TargetResult {
268
+ const typeCode = block.typeCode;
269
+
270
+ // A dynamic target's chain ends at its payload, so the word there is the declared length.
271
+ if (isLengthValidType(typeCode)) {
272
+ if (!isLengthOp(opCode)) {
273
+ throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Value operator on a target without a scalar word.");
274
+ }
275
+ const length = readPointer(callData, target);
276
+ if (typeof length !== "number") return { error: length.code };
277
+
278
+ // The declared payload of `length` items must lie within calldata past the length word.
279
+ const stride = typeCode === TypeCode.DYNAMIC_ARRAY ? (block.targetMeta & PF.HINT_META_STRIDE_MASK) * 32 : 1;
280
+ const room = callData.length - (target + 32);
281
+ if (stride !== 0 && length > Math.floor(room / stride)) return { error: "CALLDATA_OUT_OF_BOUNDS" };
282
+
283
+ const passed = applyOperator(opCode, 0n, length, operandData, typeCode);
284
+ return { passed, value: BigInt(length) };
285
+ }
286
+
287
+ if (classifyTypeCode(typeCode).typeClass !== "elementary") {
288
+ throw new CallciumError("OPERATOR_TARGET_MISMATCH", "Operator target does not carry a scalar word.");
289
+ }
290
+ const word = loadWord(callData, target);
291
+ if (!(word instanceof Uint8Array)) return { error: word.code };
292
+
293
+ const value = toBigInt(word, 0);
294
+ if (canonicalize(value, typeCode) !== value) {
295
+ return { error: "NON_CANONICAL_VALUE", value };
296
+ }
297
+
298
+ const passed = applyOperator(opCode, value, 32, operandData, typeCode);
299
+ return { passed, value };
300
+ }
301
+
302
+ ///////////////////////////////////////////////////////////////////////////
303
+ // Calldata rule evaluation
304
+ ///////////////////////////////////////////////////////////////////////////
305
+
306
+ /** Coordinates and rule metadata shared by every violation a calldata rule can produce. */
307
+ type RuleFrame = {
308
+ group: number;
309
+ rule: number;
310
+ path: Hex;
311
+ opCode: number;
312
+ operandData: Uint8Array;
313
+ typeCode: number;
314
+ };
315
+
316
+ /** Build a navigation violation for a read the enforcer cannot perform. */
317
+ function navigationViolation(frame: RuleFrame, code: NavigationViolationCode, elementIndex?: number): Violation {
318
+ return {
319
+ group: frame.group,
320
+ rule: frame.rule,
321
+ code,
322
+ scope: Scope.CALLDATA,
323
+ path: frame.path,
324
+ opCode: frame.opCode,
325
+ operandData: bytesToHex(frame.operandData),
326
+ typeCode: frame.typeCode,
327
+ ...(elementIndex !== undefined && { elementIndex }),
328
+ };
329
+ }
330
+
331
+ /** Convert a failed or erroring target result into its violation. Passing results map to null. */
332
+ function targetViolation(frame: RuleFrame, result: TargetResult, elementIndex?: number): Violation | null {
333
+ if ("error" in result) {
334
+ if (result.error === "NON_CANONICAL_VALUE") {
335
+ return {
336
+ group: frame.group,
337
+ rule: frame.rule,
338
+ code: result.error,
339
+ scope: Scope.CALLDATA,
340
+ path: frame.path,
341
+ opCode: frame.opCode,
342
+ operandData: bytesToHex(frame.operandData),
343
+ typeCode: frame.typeCode,
344
+ resolvedValue: bigintToHex(result.value),
345
+ ...(elementIndex !== undefined && { elementIndex }),
346
+ };
347
+ }
348
+ return navigationViolation(frame, result.error, elementIndex);
349
+ }
350
+
351
+ if (result.passed) return null;
352
+ return {
353
+ group: frame.group,
354
+ rule: frame.rule,
355
+ code: "VALUE_MISMATCH",
356
+ scope: Scope.CALLDATA,
357
+ path: frame.path,
358
+ opCode: frame.opCode,
359
+ operandData: bytesToHex(frame.operandData),
360
+ typeCode: frame.typeCode,
361
+ resolvedValue: bigintToHex(result.value),
362
+ ...(elementIndex !== undefined && { elementIndex }),
363
+ };
364
+ }
365
+
366
+ /** Evaluate a calldata rule by resolving its target through the hint block. */
367
+ function evaluateCalldataRule(
368
+ hint: Uint8Array,
369
+ callDataBytes: Uint8Array,
370
+ baseOffset: number,
371
+ opCode: number,
372
+ operandData: Uint8Array,
373
+ groupIndex: number,
374
+ ruleIndex: number,
375
+ pathHex: Hex,
376
+ ): Violation | null {
377
+ const header = hint[0]!;
378
+ const hopCount = header & PF.HINT_HOP_COUNT_MASK;
379
+ const kind = header >> PF.HINT_KIND_SHIFT;
380
+ const hopsOffset = PF.HINT_HEADER_SIZE;
381
+
382
+ if (kind !== PF.HINT_KIND_NONE) {
383
+ return evaluateQuantified(
384
+ hint,
385
+ callDataBytes,
386
+ baseOffset,
387
+ kind,
388
+ hopsOffset,
389
+ hopCount,
390
+ opCode,
391
+ operandData,
392
+ groupIndex,
393
+ ruleIndex,
394
+ pathHex,
395
+ );
396
+ }
397
+
398
+ const block = readTargetBlock(hint, hopsOffset + hopCount * PF.HINT_HOP_SIZE);
399
+ const frame: RuleFrame = {
400
+ group: groupIndex,
401
+ rule: ruleIndex,
402
+ path: pathHex,
403
+ opCode,
404
+ operandData,
405
+ typeCode: block.typeCode,
406
+ };
407
+
408
+ const base = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
409
+ if (typeof base !== "number") return navigationViolation(frame, base.code);
410
+
411
+ return targetViolation(frame, evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData));
412
+ }
413
+
414
+ ///////////////////////////////////////////////////////////////////////////
415
+ // Quantifier evaluation
416
+ ///////////////////////////////////////////////////////////////////////////
417
+
418
+ /** Evaluate a quantified rule by iterating over array elements with ALL/ANY semantics. */
419
+ function evaluateQuantified(
420
+ hint: Uint8Array,
421
+ callDataBytes: Uint8Array,
422
+ baseOffset: number,
423
+ kind: number,
424
+ hopsOffset: number,
425
+ hopCount: number,
426
+ opCode: number,
427
+ operandData: Uint8Array,
428
+ groupIndex: number,
429
+ ruleIndex: number,
430
+ pathHex: Hex,
431
+ ): Violation | null {
432
+ const frameOffset = hopsOffset + hopCount * PF.HINT_HOP_SIZE;
433
+ const suffixHeaderOffset = frameOffset + PF.HINT_FRAME_PREFIX_SIZE;
434
+ const suffixHopCount = hint[suffixHeaderOffset]! & PF.HINT_HOP_COUNT_MASK;
435
+ const suffixHopsOffset = suffixHeaderOffset + PF.HINT_HEADER_SIZE;
436
+
437
+ const block = readTargetBlock(hint, suffixHopsOffset + suffixHopCount * PF.HINT_HOP_SIZE);
438
+ const frame: RuleFrame = {
439
+ group: groupIndex,
440
+ rule: ruleIndex,
441
+ path: pathHex,
442
+ opCode,
443
+ operandData,
444
+ typeCode: block.typeCode,
445
+ };
446
+
447
+ const chained = chainResolve(hint, callDataBytes, baseOffset, hopsOffset, hopCount);
448
+ if (typeof chained !== "number") return navigationViolation(frame, chained.code);
449
+
450
+ const arrayDelta = readU32(hint, frameOffset);
451
+ let elems = chained + arrayDelta;
452
+
453
+ // A frame declaring no count spans a dynamic array, whose length word precedes its elements.
454
+ let count = readU16(hint, frameOffset + PF.HINT_FRAME_COUNT_OFFSET);
455
+ if (count === 0) {
456
+ const length = readPointer(callDataBytes, elems);
457
+ if (typeof length !== "number") return navigationViolation(frame, length.code);
458
+ count = length;
459
+ elems += 32;
460
+ }
461
+
462
+ if (count > Limits.MAX_QUANTIFIED_ARRAY_LENGTH) {
463
+ return {
464
+ group: groupIndex,
465
+ rule: ruleIndex,
466
+ code: "QUANTIFIER_LIMIT_EXCEEDED",
467
+ scope: Scope.CALLDATA,
468
+ path: pathHex,
469
+ resolvedValue: bigintToHex(BigInt(count)),
470
+ };
471
+ }
472
+
473
+ const isUniversal = kind === PF.HINT_KIND_ALL;
474
+ if (count === 0) {
475
+ if (isUniversal) return null;
476
+ return {
477
+ group: groupIndex,
478
+ rule: ruleIndex,
479
+ code: "QUANTIFIER_EMPTY_ARRAY",
480
+ scope: Scope.CALLDATA,
481
+ path: pathHex,
482
+ };
483
+ }
484
+
485
+ const meta = readU16(hint, frameOffset + PF.HINT_FRAME_META_OFFSET);
486
+ const elemStride = (meta & PF.HINT_META_STRIDE_MASK) * 32;
487
+ const elemIsDynamic = (meta & PF.HINT_META_ELEM_DYNAMIC) !== 0;
488
+
489
+ for (let elemIndex = 0; elemIndex < count; elemIndex++) {
490
+ const slot = elems + elemIndex * elemStride;
491
+ let elem: number = slot;
492
+ if (elemIsDynamic) {
493
+ const followed = follow(callDataBytes, elems, slot);
494
+ // Navigation failures abort under every quantifier: calldata the enforcer cannot read is
495
+ // not an element that merely fails the operator (spec §9.3).
496
+ if (typeof followed !== "number") return navigationViolation(frame, followed.code, elemIndex);
497
+ elem = followed;
498
+ }
499
+
500
+ let base: number = elem;
501
+ if (suffixHopCount > 0) {
502
+ const chainedElem = chainResolve(hint, callDataBytes, elem, suffixHopsOffset, suffixHopCount);
503
+ if (typeof chainedElem !== "number") return navigationViolation(frame, chainedElem.code, elemIndex);
504
+ base = chainedElem;
505
+ }
506
+
507
+ const applied = evalTarget(callDataBytes, base + block.targetDelta, block, opCode, operandData);
508
+ if ("error" in applied) {
509
+ // An abort-effect violation ends evaluation whatever the quantifier; a later element cannot
510
+ // rescue calldata the enforcer cannot read (spec §9.3).
511
+ return targetViolation(frame, applied, elemIndex);
512
+ }
513
+
514
+ if (applied.passed) {
515
+ // Existential quantification passes on the first passing element.
516
+ if (!isUniversal) return null;
517
+ } else if (isUniversal) {
518
+ return targetViolation(frame, applied, elemIndex);
519
+ }
520
+ }
521
+
522
+ if (isUniversal) return null;
523
+ // Existential (ANY) failure: every element rejected the constraint.
524
+ return {
525
+ group: groupIndex,
526
+ rule: ruleIndex,
527
+ code: "VALUE_MISMATCH",
528
+ scope: Scope.CALLDATA,
529
+ path: pathHex,
530
+ opCode,
531
+ operandData: bytesToHex(operandData),
532
+ typeCode: block.typeCode,
533
+ };
534
+ }
535
+
536
+ ///////////////////////////////////////////////////////////////////////////
537
+ // Context rule evaluation
538
+ ///////////////////////////////////////////////////////////////////////////
539
+
540
+ /** Evaluate a context-scoped rule by resolving the property from the execution context. */
541
+ function evaluateContextRule(
542
+ pathBytes: Uint8Array,
543
+ opCode: number,
544
+ operandData: Uint8Array,
545
+ groupIndex: number,
546
+ ruleIndex: number,
547
+ pathHex: Hex,
548
+ context?: Context,
549
+ ): Violation | null {
550
+ const propertyId = readU16(pathBytes, 0);
551
+
552
+ if (propertyId > MAX_CONTEXT_PROPERTY_ID) {
553
+ throw new CallciumError(
554
+ "UNKNOWN_CONTEXT_PROPERTY",
555
+ `Unknown context property ID 0x${propertyId.toString(16).padStart(4, "0")}`,
556
+ );
557
+ }
558
+
559
+ const propInfo = lookupContextProperty(propertyId);
560
+ const contextValue = context?.[propInfo.contextKey];
561
+
562
+ if (contextValue === undefined) {
563
+ return {
564
+ group: groupIndex,
565
+ rule: ruleIndex,
566
+ code: "MISSING_CONTEXT",
567
+ scope: Scope.CONTEXT,
568
+ path: pathHex,
569
+ typeCode: propInfo.typeCode,
570
+ };
571
+ }
572
+
573
+ let value: bigint;
574
+ if (typeof contextValue === "string") {
575
+ value = addressToBigInt(contextValue);
576
+ } else {
577
+ value = contextValue;
578
+ }
579
+
580
+ const result = applyOperator(opCode, value, 32, operandData, propInfo.typeCode);
581
+
582
+ if (!result) {
583
+ return {
584
+ group: groupIndex,
585
+ rule: ruleIndex,
586
+ code: "VALUE_MISMATCH",
587
+ scope: Scope.CONTEXT,
588
+ path: pathHex,
589
+ opCode,
590
+ operandData: bytesToHex(operandData),
591
+ typeCode: propInfo.typeCode,
592
+ resolvedValue: bigintToHex(value),
593
+ };
594
+ }
595
+
596
+ return null;
597
+ }
598
+
599
+ /** Check and enforce policies against ABI-encoded call data. */
600
+ export const PolicyEnforcer = { check, enforce };