@callcium/sdk 0.10.0 → 0.11.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.
@@ -1,11 +1,27 @@
1
1
  import { keccak_256 } from "@noble/hashes/sha3.js";
2
2
 
3
3
  import { bytesToHex, hexToBytes, toHex, readU16, readU32, writeBE16, writeBE32 } from "./bytes";
4
- import { DescriptorFormat as DF, PolicyFormat as PF, Scope, Op, TypeCode, MAX_CONTEXT_PROPERTY_ID } from "./constants";
4
+ import {
5
+ DescriptorFormat as DF,
6
+ fieldMax,
7
+ formatCode,
8
+ MAX_CONTEXT_PROPERTY_ID,
9
+ Op,
10
+ PolicyFormat as PF,
11
+ Scope,
12
+ TypeCode,
13
+ } from "./constants";
5
14
  import { Descriptor } from "./descriptor";
6
15
  import { decodeDescriptor } from "./descriptor-coder";
7
- import { CallciumError } from "./errors";
8
- import { isLengthOp, isLengthValidType, toBigInt, isAddressableTarget, isValidOperatorData } from "./operators";
16
+ import { CallciumError, type CallciumErrorCode } from "./errors";
17
+ import {
18
+ isLengthOp,
19
+ isLengthValidType,
20
+ toBigInt,
21
+ isAddressableTarget,
22
+ isValidOperatorData,
23
+ toOperatorBytes,
24
+ } from "./operators";
9
25
  import { parsePathSteps } from "./path";
10
26
 
11
27
  import type {
@@ -28,6 +44,24 @@ function field<T>(value: T, start: number, end: number): Field<T> {
28
44
  return { value, span: { start, end } };
29
45
  }
30
46
 
47
+ /**
48
+ * Wrap the hex rendering of `data[start, end)` with its span, rendering on first read.
49
+ * Enforcement reads the span as bytes, so the string is built only for a caller that asks.
50
+ * The field reads and assigns like the plain one it replaces.
51
+ */
52
+ function hexField(data: Uint8Array, start: number, end: number): Field<Hex> {
53
+ let hex: Hex | undefined;
54
+ return {
55
+ get value(): Hex {
56
+ return (hex ??= toHex(data, start, end));
57
+ },
58
+ set value(replacement: Hex) {
59
+ hex = replacement;
60
+ },
61
+ span: { start, end },
62
+ };
63
+ }
64
+
31
65
  ///////////////////////////////////////////////////////////////////////////
32
66
  // Hint block
33
67
  ///////////////////////////////////////////////////////////////////////////
@@ -123,7 +157,7 @@ function hintTargetTypeCode(data: Uint8Array, hintStart: number, hintSize: numbe
123
157
  /** Decode a policy blob, returning the structural representation and the blob bytes it spans. */
124
158
  export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Array } {
125
159
  const data = hexToBytes(blob);
126
- if (data.length < PF.DESC_OFFSET + 1) {
160
+ if (data.length < PF.DESC_OFFSET) {
127
161
  throw new CallciumError("MALFORMED_HEADER", "Policy blob is too short");
128
162
  }
129
163
 
@@ -169,7 +203,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
169
203
  end: descStart + param.span.end,
170
204
  },
171
205
  }));
172
- const descriptorRaw = toHex(data, descStart, descEnd);
206
+
207
+ const descriptorRaw = hexField(data, descStart, descEnd);
173
208
 
174
209
  const groupCountStart = groupCountOffset;
175
210
  const groupCountEnd = groupCountStart + PF.GROUP_COUNT_SIZE;
@@ -191,6 +226,10 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
191
226
  const groupBodyStart = offset + PF.GROUP_HEADER_SIZE;
192
227
  const groupEnd = groupBodyStart + groupSizeValue;
193
228
 
229
+ if (groupEnd > data.length) {
230
+ throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
231
+ }
232
+
194
233
  const ruleCountStart = offset;
195
234
  const ruleCountValue = readU16(data, ruleCountStart);
196
235
  if (ruleCountValue === 0) {
@@ -199,9 +238,6 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
199
238
  if (groupSizeValue < ruleCountValue * PF.RULE_MIN_SIZE) {
200
239
  throw new CallciumError("GROUP_TOO_SMALL", "Declared group size is too small for its rule count", offset);
201
240
  }
202
- if (groupEnd > data.length) {
203
- throw new CallciumError("GROUP_OVERFLOW", "Group extends beyond policy blob", offset);
204
- }
205
241
 
206
242
  const rules: DecodedRule[] = [];
207
243
  let ruleOffset = groupBodyStart;
@@ -219,8 +255,8 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
219
255
  ruleOffset,
220
256
  );
221
257
  }
222
- if (ruleOffset + ruleSizeValue > groupEnd) {
223
- throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
258
+ if (ruleOffset + ruleSizeValue > data.length) {
259
+ throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond policy blob", ruleOffset);
224
260
  }
225
261
 
226
262
  const scopeOffset = ruleOffset + PF.RULE_SCOPE_OFFSET;
@@ -282,12 +318,13 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
282
318
  // IN operands must be strictly ascending (unsigned); strictness also rejects duplicates.
283
319
  if (opBase === Op.IN) {
284
320
  const wordCount = dataLengthValue / 32;
321
+ let prev = toBigInt(data, dataStart);
285
322
  for (let word = 1; word < wordCount; word++) {
286
- const prev = data.subarray(dataStart + (word - 1) * 32, dataStart + word * 32);
287
- const cur = data.subarray(dataStart + word * 32, dataStart + (word + 1) * 32);
288
- if (compareBytes(prev, cur) >= 0) {
323
+ const cur = toBigInt(data, dataStart + word * 32);
324
+ if (cur <= prev) {
289
325
  throw new CallciumError("UNSORTED_IN_SET", "IN operands must be strictly ascending", ruleOffset);
290
326
  }
327
+ prev = cur;
291
328
  }
292
329
  }
293
330
 
@@ -327,7 +364,7 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
327
364
  if (contextPropertyId > MAX_CONTEXT_PROPERTY_ID) {
328
365
  throw new CallciumError(
329
366
  "UNKNOWN_CONTEXT_PROPERTY",
330
- `Context rule references undefined context property 0x${contextPropertyId.toString(16).padStart(4, "0")}`,
367
+ `Context rule references undefined context property ${formatCode(contextPropertyId, PF.PATH_STEP_SIZE)}`,
331
368
  ruleOffset,
332
369
  );
333
370
  }
@@ -340,14 +377,17 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
340
377
  pathDepth: field(depthValue, depthOffset, depthOffset + 1),
341
378
  path: field(pathHex, pathStart, pathStart + pathLength),
342
379
  ...(hintSize > 0 && {
343
- hint: field(toHex(data, hintStart, hintStart + hintSize), hintStart, hintStart + hintSize),
380
+ hint: hexField(data, hintStart, hintStart + hintSize),
344
381
  }),
345
382
  opCode: field(opCodeValue, opCodeOffset, opCodeOffset + PF.RULE_OPCODE_SIZE),
346
383
  dataLength: field(dataLengthValue, dataLengthOffset, dataLengthOffset + PF.RULE_DATALENGTH_SIZE),
347
- data: field(toHex(data, dataStart, dataStart + dataLengthValue), dataStart, dataStart + dataLengthValue),
384
+ data: hexField(data, dataStart, dataStart + dataLengthValue),
348
385
  span: { start: ruleOffset, end: ruleOffset + ruleSizeValue },
349
386
  });
350
387
 
388
+ if (ruleOffset + ruleSizeValue > groupEnd) {
389
+ throw new CallciumError("RULE_OVERFLOW", "Rule extends beyond group boundary", ruleOffset);
390
+ }
351
391
  ruleOffset += ruleSizeValue;
352
392
  }
353
393
 
@@ -374,7 +414,12 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
374
414
  selector: field(selectorHex, selectorStart, selectorEnd),
375
415
  descLength: field(descLengthValue, descLengthStart, descLengthStart + PF.DESC_LENGTH_SIZE),
376
416
  descriptor: {
377
- raw: descriptorRaw,
417
+ get raw(): Hex {
418
+ return descriptorRaw.value;
419
+ },
420
+ set raw(replacement: Hex) {
421
+ descriptorRaw.value = replacement;
422
+ },
378
423
  header: field({ version: desc.version, paramCount: desc.params.length }, descStart, descStart + DF.HEADER_SIZE),
379
424
  params,
380
425
  span: { start: descStart, end: descEnd },
@@ -395,15 +440,11 @@ export function decodePolicy(blob: Hex): { policy: DecodedPolicy; data: Uint8Arr
395
440
 
396
441
  type Rule = { scope: number; path: Uint8Array; operator: Uint8Array; hint: Uint8Array };
397
442
 
398
- /** Flatten a Constraint into one Rule per operator, compiling its hint against the descriptor. */
399
- function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
400
- const operators = constraint.operators.map(hexToBytes);
401
- for (const operator of operators) {
402
- if (operator.length < 1) {
403
- throw new CallciumError("INVALID_OPERATOR_BYTES", "Operator must have at least one byte (opcode)");
404
- }
405
- }
443
+ /** The path bytes and compiled hint every rule a constraint produces carries. */
444
+ type ConstraintTarget = { path: Uint8Array; hint: Uint8Array };
406
445
 
446
+ /** Read a constraint's path and compile the hint its rules carry. */
447
+ function readConstraintTarget(constraint: Constraint, desc: Uint8Array): ConstraintTarget {
407
448
  const path = hexToBytes(constraint.path);
408
449
 
409
450
  // Path shape checks precede compilation: the compiler assumes a framed, depth-bounded path.
@@ -422,7 +463,21 @@ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
422
463
  constraint.scope === Scope.CALLDATA
423
464
  ? Descriptor.compileHint(desc, parsePathSteps(constraint.path))
424
465
  : new Uint8Array(0);
425
- return operators.map((operator) => ({ scope: constraint.scope, path, operator, hint }));
466
+ return { path, hint };
467
+ }
468
+
469
+ /** Flatten a Constraint into one Rule per operator, compiling its hint against the descriptor. */
470
+ function flattenConstraint(constraint: Constraint, desc: Uint8Array): Rule[] {
471
+ // The rule is the unit these checks run over: each rule's operator is established before the
472
+ // path and hint it shares with its siblings. A constraint carrying no operator produces no rule,
473
+ // so its path is never read.
474
+ let target: ConstraintTarget | undefined;
475
+
476
+ return constraint.operators.map((operatorHex) => {
477
+ const operator = toOperatorBytes(operatorHex);
478
+ target ??= readConstraintTarget(constraint, desc);
479
+ return { scope: constraint.scope, operator, ...target };
480
+ });
426
481
  }
427
482
 
428
483
  /** Compare two byte arrays lexicographically. */
@@ -447,6 +502,12 @@ function sortRules(rules: Rule[]): void {
447
502
  });
448
503
  }
449
504
 
505
+ /** Require a value to fit the big-endian field of `byteWidth` bytes. */
506
+ function requireFieldFits(value: number, byteWidth: number, code: CallciumErrorCode, subject: string): void {
507
+ const max = fieldMax(byteWidth);
508
+ if (value > max) throw new CallciumError(code, `${subject} ${value} exceeds maximum ${max}`);
509
+ }
510
+
450
511
  /**
451
512
  * Serialize a single rule to its wire format bytes.
452
513
  * Path shape and operator framing are established by `flattenConstraint`.
@@ -458,9 +519,7 @@ function encodeRule(rule: Rule): Uint8Array {
458
519
  }
459
520
  const data = rule.operator.subarray(1);
460
521
  const ruleSize = PF.RULE_FIXED_OVERHEAD + rule.path.length + rule.hint.length + data.length;
461
- if (ruleSize > 0xffff) {
462
- throw new CallciumError("RULE_SIZE_OVERFLOW", `Rule size ${ruleSize} exceeds maximum 65535`);
463
- }
522
+ requireFieldFits(ruleSize, PF.RULE_SIZE_SIZE, "RULE_SIZE_OVERFLOW", "Rule size");
464
523
 
465
524
  const buf = new Uint8Array(ruleSize);
466
525
  writeBE16(buf, 0, ruleSize);
@@ -511,9 +570,7 @@ function encode(data: PolicyData): Hex {
511
570
  if (data.groups.length === 0) {
512
571
  throw new CallciumError("EMPTY_POLICY", "Policy must contain at least one group");
513
572
  }
514
- if (data.groups.length > 0xff) {
515
- throw new CallciumError("GROUP_COUNT_OVERFLOW", `Group count ${data.groups.length} exceeds maximum 255`);
516
- }
573
+ requireFieldFits(data.groups.length, PF.GROUP_COUNT_SIZE, "GROUP_COUNT_OVERFLOW", "Group count");
517
574
 
518
575
  // Flatten constraints into rules and sort within each group. Hints are compiled first because
519
576
  // they are part of the rule bytes the group hash covers.
@@ -523,20 +580,13 @@ function encode(data: PolicyData): Hex {
523
580
  return rules;
524
581
  });
525
582
 
526
- if (descBytes.length > 0xffff) {
527
- throw new CallciumError("DESC_LENGTH_OVERFLOW", `Descriptor length ${descBytes.length} exceeds maximum 65535`);
528
- }
583
+ requireFieldFits(descBytes.length, PF.DESC_LENGTH_SIZE, "DESC_LENGTH_OVERFLOW", "Descriptor length");
529
584
 
530
585
  const encodedGroups = sortedGroups.map((rules, groupIndex) => {
531
586
  if (rules.length === 0) {
532
587
  throw new CallciumError("EMPTY_GROUP", `Group ${groupIndex} is empty`);
533
588
  }
534
- if (rules.length > 0xffff) {
535
- throw new CallciumError(
536
- "RULE_COUNT_OVERFLOW",
537
- `Group ${groupIndex} rule count ${rules.length} exceeds maximum 65535`,
538
- );
539
- }
589
+ requireFieldFits(rules.length, PF.GROUP_RULECOUNT_SIZE, "RULE_COUNT_OVERFLOW", `Group ${groupIndex} rule count`);
540
590
  return { wireBytes: encodeGroupRules(rules), ruleCount: rules.length };
541
591
  });
542
592
 
@@ -547,7 +597,13 @@ function encode(data: PolicyData): Hex {
547
597
  encodedGroups.sort((a, b) => compareBytes(hashes.get(a)!, hashes.get(b)!));
548
598
  }
549
599
 
550
- const selectorBytes = data.isSelectorless ? new Uint8Array(4) : hexToBytes(data.selector);
600
+ const selectorBytes = data.isSelectorless ? new Uint8Array(PF.SELECTOR_SIZE) : hexToBytes(data.selector);
601
+ if (selectorBytes.length !== PF.SELECTOR_SIZE) {
602
+ throw new CallciumError(
603
+ "MALFORMED_SELECTOR",
604
+ `Selector must be ${PF.SELECTOR_SIZE} bytes, got ${selectorBytes.length}`,
605
+ );
606
+ }
551
607
 
552
608
  const headerByte = PF.VERSION | (data.isSelectorless ? PF.FLAG_NO_SELECTOR : 0);
553
609
 
@@ -599,8 +655,8 @@ function decode(blob: Hex): PolicyData {
599
655
  const { policy } = decodePolicy(blob);
600
656
 
601
657
  const groups: Constraint[][] = policy.groups.map((group) => {
658
+ // A Map keeps its insertion order, which is the order the rules named their constraints in.
602
659
  const constraintMap = new Map<string, Constraint>();
603
- const constraintOrder: string[] = [];
604
660
 
605
661
  for (const rule of group.rules) {
606
662
  const hintHex = rule.hint?.value;
@@ -619,11 +675,10 @@ function decode(blob: Hex): PolicyData {
619
675
  span: rule.span,
620
676
  };
621
677
  constraintMap.set(key, constraint);
622
- constraintOrder.push(key);
623
678
  }
624
679
  }
625
680
 
626
- return constraintOrder.map((k) => constraintMap.get(k)!);
681
+ return [...constraintMap.values()];
627
682
  });
628
683
 
629
684
  return {