@callcium/sdk 0.11.1 → 0.13.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.
package/src/constants.ts CHANGED
@@ -7,6 +7,7 @@ import { CallciumError } from "./errors";
7
7
  /** Binary layout constants for the Callcium descriptor format. */
8
8
  export const DescriptorFormat = {
9
9
  VERSION: 0x02,
10
+ HEADER_PARAMCOUNT_OFFSET: 1,
10
11
  HEADER_SIZE: 2,
11
12
  TYPECODE_SIZE: 1,
12
13
  COMPOSITE_META_SIZE: 3,
@@ -28,27 +29,23 @@ export const DescriptorFormat = {
28
29
  // Policy format
29
30
  ///////////////////////////////////////////////////////////////////////////
30
31
 
31
- /** Byte width of a rule's data length field. */
32
- const RULE_DATALENGTH_SIZE = 2;
33
-
34
- /** Byte width of one path step. */
35
- const PATH_STEP_SIZE = 2;
36
-
37
- /** Byte overhead of a rule header, excluding the path and the operator payload. */
38
- const RULE_FIXED_OVERHEAD = 7;
39
-
40
- /** Highest value a big-endian field of `byteWidth` bytes holds. */
32
+ /** Highest value a big-endian field of `byteWidth` bytes can represent. */
41
33
  export function fieldMax(byteWidth: number): number {
42
34
  return 256 ** byteWidth - 1;
43
35
  }
44
36
 
37
+ /** Lowest minor version, the minor of a blob that uses no extension. */
38
+ const MINOR = 0;
39
+
45
40
  /** Binary layout constants and normative limits for the Callcium policy format. */
46
41
  export const PolicyFormat = {
47
- VERSION: 0x02,
48
- VERSION_MASK: 0x0f,
49
- FLAG_NO_SELECTOR: 0x10,
50
- RESERVED_MASK: 0xe0,
51
- HEADER_SIZE: 1,
42
+ MAJOR: 3,
43
+ MINOR,
44
+ SUPPORTED_MINOR: MINOR,
45
+ MAJOR_SHIFT: 5,
46
+ MINOR_SHIFT: 1,
47
+ MINOR_MASK: 0x0f,
48
+ FLAG_NO_SELECTOR: 0x01,
52
49
  SELECTOR_OFFSET: 1,
53
50
  SELECTOR_SIZE: 4,
54
51
  DESC_LENGTH_OFFSET: 5,
@@ -56,18 +53,20 @@ export const PolicyFormat = {
56
53
  DESC_OFFSET: 7,
57
54
  GROUP_COUNT_SIZE: 1,
58
55
  GROUP_RULECOUNT_SIZE: 2,
59
- GROUP_SIZE_SIZE: 4,
60
56
  GROUP_HEADER_SIZE: 6,
61
57
  RULE_SIZE_SIZE: 2,
62
58
  RULE_SCOPE_OFFSET: 2,
59
+ RULE_SCOPE_SIZE: 1,
63
60
  RULE_DEPTH_OFFSET: 3,
61
+ RULE_DEPTH_SIZE: 1,
64
62
  RULE_PATH_OFFSET: 4,
65
- PATH_STEP_SIZE,
66
- PATH_STEP_MAX: fieldMax(PATH_STEP_SIZE),
63
+ PATH_STEP_SIZE: 2,
64
+ PROPERTY_ID_SIZE: 2,
65
+ RULE_QUANTIFIER_OFFSET: 3,
66
+ RULE_QUANTIFIER_SIZE: 1,
67
+ LOOP_DEPTH_OFFSET: 4,
67
68
  RULE_OPCODE_SIZE: 1,
68
- RULE_DATALENGTH_SIZE,
69
- RULE_FIXED_OVERHEAD,
70
- RULE_MIN_SIZE: RULE_FIXED_OVERHEAD + PATH_STEP_SIZE,
69
+ RULE_DATALENGTH_SIZE: 2,
71
70
  HINT_HEADER_SIZE: 1,
72
71
  HINT_HOP_SIZE: 8,
73
72
  HINT_HOP_INDEX_OFFSET: 4,
@@ -78,15 +77,10 @@ export const PolicyFormat = {
78
77
  HINT_TARGET_SIZE: 7,
79
78
  HINT_TARGET_META_OFFSET: 4,
80
79
  HINT_TARGET_TYPECODE_OFFSET: 6,
81
- HINT_KIND_NONE: 0x0,
82
- HINT_KIND_ALL: 0x1,
83
- HINT_KIND_ANY: 0x2,
84
- HINT_KIND_MAX: 0x2,
85
- HINT_KIND_SHIFT: 6,
86
80
  HINT_HOP_COUNT_MASK: 0x3f,
87
- HINT_SUFFIX_RESERVED_MASK: 0xc0,
81
+ HINT_HEADER_RESERVED_MASK: 0xc0,
88
82
  HINT_NO_INDEX: 0xffff,
89
- HINT_INDEX_RESERVED: 0xfffe,
83
+ PATH_MARKER_BASE: 0xff00,
90
84
  HINT_META_ELEM_DYNAMIC: 0x8000,
91
85
  HINT_META_DYNAMIC_ARRAY: 0x4000,
92
86
  HINT_META_RESERVED_MASK: 0x3000,
@@ -94,7 +88,6 @@ export const PolicyFormat = {
94
88
  // Normative limits.
95
89
  MAX_PATH_DEPTH: 32,
96
90
  MAX_QUANTIFIED_ARRAY_LENGTH: 256,
97
- MAX_SET_MEMBERS: Math.floor(fieldMax(RULE_DATALENGTH_SIZE) / 32),
98
91
  } as const satisfies Record<string, number>;
99
92
 
100
93
  ///////////////////////////////////////////////////////////////////////////
@@ -113,7 +106,7 @@ type CodeMap<T extends readonly { readonly key: string; readonly code: number }[
113
106
  * Render a wire code as fixed-width hex.
114
107
  * @param code - Code value.
115
108
  * @param byteWidth - Byte width of the field the code occupies.
116
- * @returns The code as `0x`-prefixed hex, or its decimal form when no such field holds it.
109
+ * @returns The code as `0x`-prefixed hex, or its decimal form when no such field can represent it.
117
110
  */
118
111
  export function formatCode(code: number, byteWidth: number): string {
119
112
  if (!Number.isInteger(code) || code < 0 || code > fieldMax(byteWidth)) return String(code);
@@ -128,37 +121,6 @@ export function buildCodeMap<T extends readonly { readonly key: string; readonly
128
121
  return Object.fromEntries(table.map((entry) => [entry.key, entry.code])) as CodeMap<T>;
129
122
  }
130
123
 
131
- ///////////////////////////////////////////////////////////////////////////
132
- // Scope codes
133
- ///////////////////////////////////////////////////////////////////////////
134
-
135
- const SCOPE_TABLE = [
136
- { key: "CONTEXT", code: 0x00, label: "context" },
137
- { key: "CALLDATA", code: 0x01, label: "calldata" },
138
- ] as const;
139
-
140
- /** Rule scope discriminant: context (EVM environment) vs. calldata (ABI payload). */
141
- export const Scope = buildCodeMap(SCOPE_TABLE);
142
-
143
- /** Display metadata for a scope code. */
144
- export type ScopeInfo = { label: string };
145
-
146
- const scopeByCode: ReadonlyMap<number, ScopeInfo> = new Map<number, ScopeInfo>(
147
- SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]),
148
- );
149
-
150
- /**
151
- * Map a scope code to its display label.
152
- * @param code - Scope byte value.
153
- * @returns Display metadata for the scope.
154
- * @throws {CallciumError} If the code is not a recognised scope.
155
- */
156
- export function lookupScope(code: number): ScopeInfo {
157
- const info = scopeByCode.get(code);
158
- if (!info) throw new CallciumError("INVALID_SCOPE", `Unknown scope value ${code}`);
159
- return info;
160
- }
161
-
162
124
  ///////////////////////////////////////////////////////////////////////////
163
125
  // Type code ranges
164
126
  ///////////////////////////////////////////////////////////////////////////
@@ -233,17 +195,96 @@ export function lookupContextProperty(code: number): ContextPropertyInfo {
233
195
  if (!info) {
234
196
  throw new CallciumError(
235
197
  "UNKNOWN_CONTEXT_PROPERTY",
236
- `Unknown context property ID ${formatCode(code, PATH_STEP_SIZE)}`,
198
+ `Unknown context property ID ${formatCode(code, PolicyFormat.PROPERTY_ID_SIZE)}`,
237
199
  );
238
200
  }
239
201
  return info;
240
202
  }
241
203
 
204
+ ///////////////////////////////////////////////////////////////////////////
205
+ // Scope codes
206
+ ///////////////////////////////////////////////////////////////////////////
207
+
208
+ const SCOPE_TABLE = [
209
+ { key: "CONTEXT", code: 0x00, label: "context" },
210
+ { key: "CALLDATA", code: 0x01, label: "calldata" },
211
+ { key: "LOOP", code: 0x02, label: "loop" },
212
+ ] as const;
213
+
214
+ /** Rule scope discriminant: context (EVM environment), calldata (ABI payload) or loop (array elements). */
215
+ export const Scope = buildCodeMap(SCOPE_TABLE);
216
+
217
+ /** A defined scope code. */
218
+ export type ScopeCode = (typeof SCOPE_TABLE)[number]["code"];
219
+
220
+ /** Display metadata for a scope code. */
221
+ export type ScopeInfo = { label: string };
222
+
223
+ const scopeByCode: ReadonlyMap<number, ScopeInfo> = new Map<number, ScopeInfo>(
224
+ SCOPE_TABLE.map((entry) => [entry.code, { label: entry.label }]),
225
+ );
226
+
227
+ /** Check whether a scope byte is a defined scope code. */
228
+ export function isScopeCode(code: number): code is ScopeCode {
229
+ return scopeByCode.has(code);
230
+ }
231
+
232
+ /**
233
+ * Map a scope code to its display label.
234
+ * @param code - Scope byte value.
235
+ * @returns Display metadata for the scope.
236
+ * @throws {CallciumError} If the code is not a recognised scope.
237
+ */
238
+ export function lookupScope(code: number): ScopeInfo {
239
+ const info = scopeByCode.get(code);
240
+ if (!info) throw new CallciumError("UNKNOWN_SCOPE", `Unknown scope value ${code}`);
241
+ return info;
242
+ }
243
+
244
+ ///////////////////////////////////////////////////////////////////////////
245
+ // Quantifier codes
246
+ ///////////////////////////////////////////////////////////////////////////
247
+
248
+ const QUANTIFIER_TABLE = [
249
+ { key: "ALL", code: 0x01, label: "all" },
250
+ { key: "ANY", code: 0x02, label: "any" },
251
+ ] as const;
252
+
253
+ /** Loop quantifier: the body holds for every element (`ALL`) or for some element (`ANY`). */
254
+ export const LoopQuantifier = buildCodeMap(QUANTIFIER_TABLE);
255
+
256
+ /** A defined loop quantifier code. */
257
+ export type QuantifierCode = (typeof QUANTIFIER_TABLE)[number]["code"];
258
+
259
+ /** Display metadata for a loop quantifier code. */
260
+ export type QuantifierInfo = { label: string };
261
+
262
+ const quantifierByCode: ReadonlyMap<number, QuantifierInfo> = new Map<number, QuantifierInfo>(
263
+ QUANTIFIER_TABLE.map((entry) => [entry.code, { label: entry.label }]),
264
+ );
265
+
266
+ /** Check whether a quantifier byte is a defined quantifier code. */
267
+ export function isQuantifierCode(code: number): code is QuantifierCode {
268
+ return quantifierByCode.has(code);
269
+ }
270
+
271
+ /**
272
+ * Map a loop quantifier code to its display label.
273
+ * @param code - Quantifier byte value.
274
+ * @returns Display metadata for the quantifier.
275
+ * @throws {CallciumError} If the code is not a recognised quantifier.
276
+ */
277
+ export function lookupQuantifier(code: number): QuantifierInfo {
278
+ const info = quantifierByCode.get(code);
279
+ if (!info) throw new CallciumError("UNKNOWN_QUANTIFIER", `Unknown quantifier value ${code}`);
280
+ return info;
281
+ }
282
+
242
283
  ///////////////////////////////////////////////////////////////////////////
243
284
  // Operator codes
244
285
  ///////////////////////////////////////////////////////////////////////////
245
286
 
246
- /** Operand count category for operator data validation. */
287
+ /** Operand count category of an operator. */
247
288
  export type Operands = "single" | "range" | "variadic";
248
289
 
249
290
  /** @internal */
@@ -255,20 +296,33 @@ const OP_TABLE = [
255
296
  { key: "LTE", code: 0x05, label: "<=", operands: "single" },
256
297
  { key: "BETWEEN", code: 0x06, label: "between", operands: "range" },
257
298
  { key: "IN", code: 0x07, label: "in", operands: "variadic" },
258
- { key: "EQ_CTX", code: 0x08, label: "== ctx", operands: "single" },
259
299
  { key: "BITMASK_ALL", code: 0x10, label: "bitmask all", operands: "single" },
260
300
  { key: "BITMASK_ANY", code: 0x11, label: "bitmask any", operands: "single" },
261
301
  { key: "BITMASK_NONE", code: 0x12, label: "bitmask none", operands: "single" },
262
- { key: "LENGTH_EQ", code: 0x20, label: "length ==", operands: "single" },
263
- { key: "LENGTH_GT", code: 0x21, label: "length >", operands: "single" },
264
- { key: "LENGTH_LT", code: 0x22, label: "length <", operands: "single" },
265
- { key: "LENGTH_GTE", code: 0x23, label: "length >=", operands: "single" },
266
- { key: "LENGTH_LTE", code: 0x24, label: "length <=", operands: "single" },
267
- { key: "LENGTH_BETWEEN", code: 0x25, label: "length between", operands: "range" },
302
+ { key: "LENGTH_EQ", code: 0x18, label: "length ==", operands: "single" },
303
+ { key: "LENGTH_GT", code: 0x19, label: "length >", operands: "single" },
304
+ { key: "LENGTH_LT", code: 0x1a, label: "length <", operands: "single" },
305
+ { key: "LENGTH_GTE", code: 0x1b, label: "length >=", operands: "single" },
306
+ { key: "LENGTH_LTE", code: 0x1c, label: "length <=", operands: "single" },
307
+ { key: "LENGTH_BETWEEN", code: 0x1d, label: "length between", operands: "range" },
268
308
  ] as const;
269
309
 
270
- /** Callcium policy operator codes. */
271
- export const Op: CodeMap<typeof OP_TABLE> & { readonly NOT: 0x80 } = { ...buildCodeMap(OP_TABLE), NOT: 0x80 };
310
+ /** Callcium policy operator codes, with the NOT flag and the operand source fields. */
311
+ export const Op = { ...buildCodeMap(OP_TABLE), NOT: 0x80, SOURCE_SHIFT: 5, COMPARISON_MASK: 0x1f } as const;
312
+
313
+ /** A defined comparison code. */
314
+ export type ComparisonCode = (typeof OP_TABLE)[number]["code"];
315
+
316
+ const SOURCE_TABLE = [
317
+ { key: "CONSTANT", code: 0, labelSuffix: "" },
318
+ { key: "CONTEXT", code: 1, labelSuffix: " ctx" },
319
+ ] as const;
320
+
321
+ /** Operand source: constant values, or a context property. */
322
+ export const Source = buildCodeMap(SOURCE_TABLE);
323
+
324
+ /** A defined operand source code. */
325
+ export type SourceCode = (typeof SOURCE_TABLE)[number]["code"];
272
326
 
273
327
  /** Display metadata for an operator code. */
274
328
  export type OpInfo = { label: string; operands: Operands };
@@ -277,24 +331,44 @@ const opByCode: ReadonlyMap<number, OpInfo> = new Map<number, OpInfo>(
277
331
  OP_TABLE.map((entry) => [entry.code, { label: entry.label, operands: entry.operands }]),
278
332
  );
279
333
 
334
+ const labelSuffixBySource: ReadonlyMap<number, string> = new Map<number, string>(
335
+ SOURCE_TABLE.map((entry) => [entry.code, entry.labelSuffix]),
336
+ );
337
+
338
+ /** Check whether a comparison is a defined comparison code. */
339
+ export function isComparisonCode(code: number): code is ComparisonCode {
340
+ return opByCode.has(code);
341
+ }
342
+
343
+ /** Check whether a source is a defined operand source code. */
344
+ export function isSourceCode(code: number): code is SourceCode {
345
+ return labelSuffixBySource.has(code);
346
+ }
347
+
348
+ /** The comparison bits of an operator code. */
349
+ export function comparisonOf(opCode: number): number {
350
+ return opCode & Op.COMPARISON_MASK;
351
+ }
352
+
353
+ /** The operand source bits of an operator code. */
354
+ export function sourceOf(opCode: number): number {
355
+ return (opCode & ~Op.NOT) >> Op.SOURCE_SHIFT;
356
+ }
357
+
280
358
  /**
281
- * Map an operator code to its display label. Strips the NOT flag automatically.
282
- * @param code - Operator byte value (may include the NOT flag).
359
+ * Map an operator code to its display label. Strips the NOT flag automatically. A context source adds ` ctx`.
360
+ * @param code - Operator byte value (can include the NOT flag).
283
361
  * @returns Display metadata for the base operator.
284
362
  * @throws {CallciumError} If the base code is not a recognised operator.
285
363
  */
286
364
  export function lookupOp(code: number): OpInfo {
287
365
  const base = code & ~Op.NOT;
288
- const info = opByCode.get(base);
289
- if (!info)
366
+ const info = opByCode.get(comparisonOf(base));
367
+ const labelSuffix = labelSuffixBySource.get(sourceOf(base));
368
+ if (!info || labelSuffix === undefined)
290
369
  throw new CallciumError(
291
370
  "UNKNOWN_OPERATOR",
292
371
  `Unknown operator code ${formatCode(base, PolicyFormat.RULE_OPCODE_SIZE)}`,
293
372
  );
294
- return info;
295
- }
296
-
297
- /** Operand count category for an operator code, or undefined when unrecognised. */
298
- export function operandsOf(opBase: number): Operands | undefined {
299
- return opByCode.get(opBase)?.operands;
373
+ return { label: `${info.label}${labelSuffix}`, operands: info.operands };
300
374
  }
package/src/constraint.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { bytesToHex, hexToBytes, toAddress } from "./bytes";
2
- import { PolicyFormat, Op, Scope, ContextProperty, lookupContextProperty } from "./constants";
2
+ import { ContextProperty, lookupContextProperty, Op, Scope, Source } from "./constants";
3
3
  import { CallciumError } from "./errors";
4
4
  import { encodePath } from "./path";
5
5
 
6
- import type { Address, Hex, Constraint } from "./types";
6
+ import type { Address, Hex } from "./types";
7
+ import type { Constraint } from "./types";
7
8
 
8
9
  ///////////////////////////////////////////////////////////////////////////
9
10
  // Value encoding helpers
@@ -32,6 +33,17 @@ function addressBody(value: string): string {
32
33
  return toAddress(value).slice(2);
33
34
  }
34
35
 
36
+ /**
37
+ * Require an operand that reached the builder untyped to be a bigint and return it.
38
+ * @throws {CallciumError} When the operand is of another type, as a JavaScript `number` is.
39
+ */
40
+ function requireBigint(value: unknown): bigint {
41
+ if (typeof value !== "bigint") {
42
+ throw new CallciumError("MALFORMED_OPERAND", "Operand is not a bigint, a boolean or an address string");
43
+ }
44
+ return value;
45
+ }
46
+
35
47
  /** Convert a scalar value to a 32-byte big-endian word. */
36
48
  function encodeWord(value: ScalarValue): Uint8Array {
37
49
  const word = new Uint8Array(32);
@@ -47,6 +59,8 @@ function encodeWord(value: ScalarValue): Uint8Array {
47
59
  return word;
48
60
  }
49
61
 
62
+ requireBigint(value);
63
+
50
64
  // A negative operand occupies the word in two's complement, which is the signed target's encoding.
51
65
  let bigValue = checkOperandRange(value);
52
66
  for (let i = 31; i >= 0; i--) {
@@ -79,6 +93,9 @@ function rangeOp(opCode: number, min: bigint, max: bigint): Hex {
79
93
  return bytesToHex(buffer);
80
94
  }
81
95
 
96
+ /** The `source` bits of an operator code whose operand is a context property. */
97
+ const CONTEXT_SOURCE = Source.CONTEXT << Op.SOURCE_SHIFT;
98
+
82
99
  /**
83
100
  * Require an assigned context property ID and return it as an operand word.
84
101
  * @throws {CallciumError} When the ID names no assigned property.
@@ -90,14 +107,13 @@ function contextOperand(contextPropertyId: number): bigint {
90
107
 
91
108
  /** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
92
109
  function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
93
- // PWF-21 orders members by their 32-byte encodings, so a negative operand and its unsigned
94
- // alias are one member at one position. Normalising after the domain check keeps an
95
- // out-of-range value an error rather than folding it into the word.
110
+ // PWF-21 orders members by their 32-byte encodings, so a negative operand and its unsigned alias are one member at
111
+ // one position. Normalising after the domain check keeps an out-of-range value an error rather than a word.
96
112
  const words = values.map((value) => {
97
113
  if (typeof value === "bigint") return BigInt.asUintN(256, checkOperandRange(value));
98
114
  if (typeof value === "boolean") return value ? 1n : 0n;
99
- // String address.
100
- return BigInt("0x" + addressBody(value));
115
+ if (typeof value === "string") return BigInt("0x" + addressBody(value));
116
+ return requireBigint(value);
101
117
  });
102
118
 
103
119
  const deduped = [...new Set(words)].toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0));
@@ -105,9 +121,6 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
105
121
  if (deduped.length === 0) {
106
122
  throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
107
123
  }
108
- if (deduped.length > PolicyFormat.MAX_SET_MEMBERS) {
109
- throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${PolicyFormat.MAX_SET_MEMBERS} values`);
110
- }
111
124
 
112
125
  const buffer = new Uint8Array(1 + deduped.length * 32);
113
126
  buffer[0] = opCode;
@@ -122,8 +135,8 @@ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
122
135
  ///////////////////////////////////////////////////////////////////////////
123
136
 
124
137
  /**
125
- * Extremes of the numeric operands a builder's value operators received, as written. Encoding folds
126
- * a negative operand onto its unsigned alias, so the sign survives nowhere else.
138
+ * Extremes of the numeric operands a builder's equality, comparison and set operators received, as written. Encoding
139
+ * folds a negative operand onto its unsigned alias, so the sign survives nowhere else.
127
140
  */
128
141
  export type OperandExtremes = {
129
142
  /** Most negative operand written, or zero when none was negative. */
@@ -133,10 +146,9 @@ export type OperandExtremes = {
133
146
  };
134
147
 
135
148
  /**
136
- * Extremes of the numeric operands the value operators of `constraint` received, as written.
137
- * They outlive edits to `operators`: removing an operator does not remove what was written to
138
- * produce it, and a fresh builder starts at zero. Undefined for a plain `Constraint`, whose
139
- * operators arrive already encoded.
149
+ * Extremes of the numeric operands the value operators of `constraint` received, as written. They outlive edits to
150
+ * `operators`: removing an operator does not remove what was written to produce it, and a fresh builder starts at zero.
151
+ * Undefined for a plain `Constraint`, whose operators arrive already encoded.
140
152
  */
141
153
  export function readOperandExtremes(constraint: Constraint | ConstraintBuilder): OperandExtremes | undefined {
142
154
  return "operandExtremes" in constraint ? constraint.operandExtremes : undefined;
@@ -147,23 +159,21 @@ export function readOperandExtremes(constraint: Constraint | ConstraintBuilder):
147
159
  ///////////////////////////////////////////////////////////////////////////
148
160
 
149
161
  /**
150
- * Mutable builder that accumulates operators targeting a single path.
151
- * Implements the `Constraint` interface so it can be passed directly to policy builders.
162
+ * Mutable builder that accumulates operators targeting a single path. Implements the `Constraint` interface so it can
163
+ * be passed directly to policy builders.
152
164
  */
153
165
  export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implements Constraint {
154
- readonly scope: number;
166
+ readonly scope: Constraint["scope"];
155
167
  readonly path: Hex;
156
168
  readonly operators: Hex[];
157
169
  /**
158
- * Extremes of the numeric operands the value operators received, as written. Only the extremes
159
- * can offend: an operand folds into a target's range from below it or from above it, and
160
- * whichever reaches furthest arrives first. Read it through {@link readOperandExtremes}.
170
+ * Extremes of the numeric operands the value operators received, as written.
161
171
  * @internal
162
172
  */
163
173
  readonly operandExtremes: OperandExtremes;
164
174
 
165
175
  /** @internal */
166
- constructor(scope: number, path: Hex) {
176
+ constructor(scope: Constraint["scope"], path: Hex) {
167
177
  this.scope = scope;
168
178
  this.path = path;
169
179
  this.operators = [];
@@ -171,8 +181,8 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
171
181
  }
172
182
 
173
183
  /**
174
- * Push a pre-encoded operator hex string and return this for chaining.
175
- * `values` are the operands as written; the numeric ones join the record.
184
+ * Push a pre-encoded operator hex string and return this for chaining. `values` are the operands as written. The
185
+ * numeric ones join the record.
176
186
  */
177
187
  private push(opHex: Hex, values: readonly ScalarValue[] = []): this {
178
188
  for (const value of values) {
@@ -200,12 +210,32 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
200
210
 
201
211
  /** Assert the value equals the context property `contextPropertyId`. */
202
212
  eqCtx(contextPropertyId: number): this {
203
- return this.push(singleOp(Op.EQ_CTX, contextOperand(contextPropertyId)));
213
+ return this.push(singleOp(Op.EQ | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
204
214
  }
205
215
 
206
216
  /** Assert the value does not equal the context property `contextPropertyId`. */
207
217
  neqCtx(contextPropertyId: number): this {
208
- return this.push(singleOp(Op.EQ_CTX | Op.NOT, contextOperand(contextPropertyId)));
218
+ return this.push(singleOp(Op.EQ | CONTEXT_SOURCE | Op.NOT, contextOperand(contextPropertyId)));
219
+ }
220
+
221
+ /** Assert the value is greater than the context property `contextPropertyId`. */
222
+ gtCtx(contextPropertyId: number): this {
223
+ return this.push(singleOp(Op.GT | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
224
+ }
225
+
226
+ /** Assert the value is less than the context property `contextPropertyId`. */
227
+ ltCtx(contextPropertyId: number): this {
228
+ return this.push(singleOp(Op.LT | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
229
+ }
230
+
231
+ /** Assert the value is greater than or equal to the context property `contextPropertyId`. */
232
+ gteCtx(contextPropertyId: number): this {
233
+ return this.push(singleOp(Op.GTE | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
234
+ }
235
+
236
+ /** Assert the value is less than or equal to the context property `contextPropertyId`. */
237
+ lteCtx(contextPropertyId: number): this {
238
+ return this.push(singleOp(Op.LTE | CONTEXT_SOURCE, contextOperand(contextPropertyId)));
209
239
  }
210
240
 
211
241
  /** Assert the value is greater than `bound`. */
@@ -241,9 +271,8 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
241
271
  ///////////////////////////////////////////////////////////////////////////
242
272
 
243
273
  /**
244
- * Assert the value is a member of the set.
245
- * Values are sorted and deduplicated before encoding.
246
- * @throws {CallciumError} If the set is empty after deduplication.
274
+ * Assert the value is a member of the set. Values are sorted and deduplicated before encoding.
275
+ * @throws {CallciumError} If the set is empty or a value is not encodable as an operand word.
247
276
  */
248
277
  isIn(values: readonly Operand[]): this {
249
278
  return this.push(setOp(Op.IN, values), values);
@@ -251,7 +280,7 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
251
280
 
252
281
  /**
253
282
  * Assert the value is not a member of the set.
254
- * @throws {CallciumError} If the set is empty after deduplication.
283
+ * @throws {CallciumError} If the set is empty or a value is not encodable as an operand word.
255
284
  */
256
285
  notIn(values: readonly Operand[]): this {
257
286
  return this.push(setOp(Op.IN | Op.NOT, values), values);
@@ -319,8 +348,7 @@ export class ConstraintBuilder<Operand extends ScalarValue = ScalarValue> implem
319
348
  ///////////////////////////////////////////////////////////////////////////
320
349
 
321
350
  /**
322
- * Target a calldata argument by path.
323
- * Each argument is a big-endian uint16 step; multiple steps navigate into nested types.
351
+ * Target a calldata argument by path. Each argument is a big-endian uint16 step. Further steps navigate nested types.
324
352
  */
325
353
  export function arg(p0: number, ...rest: number[]): ConstraintBuilder;
326
354
  export function arg(...steps: number[]): ConstraintBuilder {