@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.
@@ -1,19 +1,20 @@
1
+ import { concatBytes } from "@noble/hashes/utils.js";
2
+
1
3
  import { readU16 } from "./bytes";
2
- import { DescriptorFormat as DF, TypeCode } from "./constants";
4
+ import { DescriptorFormat as DF } from "./constants";
3
5
  import { Descriptor, readNode } from "./descriptor";
4
6
  import { CallciumError } from "./errors";
5
- import { classifyTypeCode, lookupTypeCode } from "./operators";
7
+ import { lookupTypeCode } from "./operators";
6
8
  import { address, array, bool, bytes, bytesN, function_, intN, string_, tuple, uintN } from "./type-desc";
7
9
 
8
- import type { DecodedParam, Hex } from "./types";
10
+ import type { DecodedDescriptor, DecodedNode } from "./types";
9
11
 
10
12
  ///////////////////////////////////////////////////////////////////////////
11
13
  // Splitting helpers
12
14
  ///////////////////////////////////////////////////////////////////////////
13
15
 
14
16
  /**
15
- * Collect the positions of depth-0 commas in input[start..end).
16
- * Commas inside `()` or `[]` are skipped.
17
+ * Collect the positions of depth-0 commas in input[start..end). Commas inside `()` or `[]` are skipped.
17
18
  */
18
19
  function commaPositions(input: string, start: number, end: number): number[] {
19
20
  const positions: number[] = [];
@@ -47,8 +48,8 @@ const MAX_TYPE_STRING_NUMBER = 256 ** DF.ARRAY_LENGTH_SIZE - 1;
47
48
  /**
48
49
  * Parse a decimal integer from input[start..end).
49
50
  *
50
- * Rejects an empty span, a non-digit, a leading zero, and a value too wide for the fields a type
51
- * string feeds. Width and length validity are out of scope here.
51
+ * Rejects an empty span, a non-digit, a leading zero, and a value too wide for the fields a type string feeds. Width
52
+ * and length validity are out of scope here.
52
53
  */
53
54
  function parseUint(input: string, start: number, end: number): number {
54
55
  if (start >= end) {
@@ -77,8 +78,7 @@ function parseUint(input: string, start: number, end: number): number {
77
78
  ///////////////////////////////////////////////////////////////////////////
78
79
 
79
80
  /**
80
- * Parse a tuple literal `(field0,field1,...)` where the opening `(` is at
81
- * `start` and the substring to parse ends before `end`.
81
+ * Parse a tuple literal `(field0,field1,...)` whose opening `(` is at `start` and whose content ends before `end`.
82
82
  */
83
83
  function parseTuple(input: string, start: number, end: number): Uint8Array {
84
84
  if (input[start] !== "(") {
@@ -137,13 +137,20 @@ function parseBaseType(input: string, start: number, end: number): Uint8Array {
137
137
  if (segment === "string") return string_();
138
138
  if (segment.startsWith("(")) return parseTuple(input, start, end);
139
139
 
140
- // A prefix with no width after it spells no type, so it falls through to the unknown-type report
141
- // rather than being read as a width that is not there.
142
- if (segment.length > 4 && segment.startsWith("uint")) return uintN(parseUint(input, start + 4, end));
143
- if (segment.length > 3 && segment.startsWith("int")) return intN(parseUint(input, start + 3, end));
144
- if (segment.length > 5 && segment.startsWith("bytes")) return bytesN(parseUint(input, start + 5, end));
140
+ // A prefix with no width after it spells no type, so it falls through to the unknown-type report rather than being
141
+ // read as a width that is not there. A width outside its type's range names no type either.
142
+ if (segment.length > 4 && segment.startsWith("uint")) {
143
+ const bits = parseUint(input, start + 4, end);
144
+ if (bits >= 8 && bits <= 256 && bits % 8 === 0) return uintN(bits);
145
+ } else if (segment.length > 3 && segment.startsWith("int")) {
146
+ const bits = parseUint(input, start + 3, end);
147
+ if (bits >= 8 && bits <= 256 && bits % 8 === 0) return intN(bits);
148
+ } else if (segment.length > 5 && segment.startsWith("bytes")) {
149
+ const length = parseUint(input, start + 5, end);
150
+ if (length >= 1 && length <= 32) return bytesN(length);
151
+ }
145
152
 
146
- throw new CallciumError("UNKNOWN_TYPE", `Unrecognised type '${segment}'`);
153
+ throw new CallciumError("UNKNOWN_TYPE", `Unknown type '${segment}'`);
147
154
  }
148
155
 
149
156
  ///////////////////////////////////////////////////////////////////////////
@@ -153,16 +160,16 @@ function parseBaseType(input: string, start: number, end: number): Uint8Array {
153
160
  /**
154
161
  * Parse a type segment from input[start..end).
155
162
  *
156
- * Scans backward from end to collect all `[...]` suffixes, identifies the
157
- * base type extent, parses the base, then applies suffixes left-to-right.
163
+ * Scans backward from end to collect all `[...]` suffixes, identifies the base type extent, parses the base, then
164
+ * applies suffixes left-to-right.
158
165
  */
159
166
  function parseType(input: string, start: number, end: number): Uint8Array {
160
167
  if (start >= end) {
161
168
  throw new CallciumError("MALFORMED_TYPE_STRING", "Empty type segment");
162
169
  }
163
170
 
164
- // Locate array suffixes by scanning backward, reading no length yet: the base names the type, so
165
- // an unrecognised one is reported before a suffix it carries is parsed.
171
+ // Locate array suffixes by scanning backward, reading no length yet: the base names the type, so an unrecognised one
172
+ // is reported before a suffix it carries is parsed.
166
173
  const spans: Array<[number, number]> = [];
167
174
  let baseEnd = end;
168
175
 
@@ -182,8 +189,7 @@ function parseType(input: string, start: number, end: number): Uint8Array {
182
189
 
183
190
  let desc = parseBaseType(input, start, baseEnd);
184
191
 
185
- // Apply suffixes left-to-right: the leftmost suffix is the outermost array.
186
- // A dynamic array spells no length at all.
192
+ // Apply suffixes left-to-right: the leftmost suffix is the innermost array. A dynamic array spells no length at all.
187
193
  for (const [innerStart, innerEnd] of spans) {
188
194
  desc = array(desc, innerStart === innerEnd ? undefined : parseUint(input, innerStart, innerEnd));
189
195
  }
@@ -198,7 +204,7 @@ function parseType(input: string, start: number, end: number): Uint8Array {
198
204
  /**
199
205
  * Encode a comma-separated list of ABI type strings into a binary descriptor.
200
206
  *
201
- * @param typesCsv - Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[],string"`.
207
+ * @param typesCsv - Comma-separated ABI type strings, for example `"address,uint256,(bool,bytes32)[],string"`.
202
208
  * @returns Binary descriptor bytes starting with the version+paramCount header.
203
209
  * @throws {CallciumError} With code `MALFORMED_TYPE_STRING` for malformed input.
204
210
  * @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
@@ -223,90 +229,30 @@ function fromTypes(typesCsv: string): Uint8Array {
223
229
  if (paramDescs.length > DF.MAX_PARAMS) {
224
230
  throw new CallciumError("TOO_MANY_PARAMS", `Parameter count ${paramDescs.length} exceeds maximum ${DF.MAX_PARAMS}`);
225
231
  }
226
- const totalBytes = paramDescs.reduce((sum, d) => sum + d.length, 0);
227
- const result = new Uint8Array(DF.HEADER_SIZE + totalBytes);
228
- result[0] = DF.VERSION;
229
- result[1] = paramDescs.length;
230
- let offset = DF.HEADER_SIZE;
231
- for (const desc of paramDescs) {
232
- result.set(desc, offset);
233
- offset += desc.length;
234
- }
232
+ const result = concatBytes(Uint8Array.of(DF.VERSION, paramDescs.length), ...paramDescs);
235
233
  // Nesting depth is a property of the assembled descriptor, not of any single node.
236
234
  decodeDescriptor(result);
237
235
  return result;
238
236
  }
239
237
 
240
- ///////////////////////////////////////////////////////////////////////////
241
- // Inverse: descriptor bytes → type string
242
- ///////////////////////////////////////////////////////////////////////////
243
-
244
- /** Reconstruct an ABI type string from a descriptor node at the given offset. */
245
- function nodeToTypeString(desc: Uint8Array, offset: number): string {
246
- const { typeCode } = Descriptor.inspect(desc, offset);
247
-
248
- if (typeCode === TypeCode.TUPLE) {
249
- const fieldCount = Descriptor.tupleFieldCount(desc, offset);
250
- const fieldTypes: string[] = [];
251
- let fieldOffset = offset + DF.TUPLE_HEADER_SIZE;
252
- for (let i = 0; i < fieldCount; i++) {
253
- fieldTypes.push(nodeToTypeString(desc, fieldOffset));
254
- fieldOffset += Descriptor.nodeLength(desc, fieldOffset);
255
- }
256
- return `(${fieldTypes.join(",")})`;
257
- }
258
-
259
- if (typeCode === TypeCode.STATIC_ARRAY) {
260
- const length = Descriptor.staticArrayLength(desc, offset);
261
- const elemType = nodeToTypeString(desc, Descriptor.arrayElementOffset(offset));
262
- return `${elemType}[${length}]`;
263
- }
264
-
265
- if (typeCode === TypeCode.DYNAMIC_ARRAY) {
266
- const elemType = nodeToTypeString(desc, Descriptor.arrayElementOffset(offset));
267
- return `${elemType}[]`;
268
- }
269
-
270
- return lookupTypeCode(typeCode).label;
271
- }
272
-
273
- /**
274
- * Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
275
- *
276
- * Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
277
- *
278
- * @param desc - Binary descriptor bytes (with version+paramCount header).
279
- * @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
280
- * @throws {CallciumError} If the descriptor is malformed.
281
- */
282
- function toTypes(desc: Uint8Array): string {
283
- const count = Descriptor.paramCount(desc);
284
- const types: string[] = [];
285
- let offset = DF.HEADER_SIZE;
286
- for (let i = 0; i < count; i++) {
287
- types.push(nodeToTypeString(desc, offset));
288
- offset += Descriptor.nodeLength(desc, offset);
289
- }
290
- return types.join(",");
291
- }
292
-
293
238
  ///////////////////////////////////////////////////////////////////////////
294
239
  // Descriptor binary decoder
295
240
  ///////////////////////////////////////////////////////////////////////////
296
241
 
297
- /** Shape of a parsed descriptor node and the offset just past it. */
298
- type ParseResult = { typeCode: number; isDynamic: boolean; staticSize: number; next: number };
242
+ /** A parsed descriptor node and the offset just past it. */
243
+ type ParseResult = { node: DecodedNode; next: number };
299
244
 
300
- /** Recursively parse a single descriptor node starting at offset. */
301
- function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult {
245
+ /** Recursively parse a single descriptor node starting at offset, its span shifted by `base`. */
246
+ function parseNode(data: Uint8Array, offset: number, depth: number, base: number): ParseResult {
302
247
  const { typeCode: code, staticWords, nodeLength } = readNode(data, offset);
303
- const info = classifyTypeCode(code);
248
+ const info = lookupTypeCode(code);
304
249
  const isDynamic = staticWords === 0;
305
250
  const staticSize = isDynamic ? 0 : staticWords * 32;
306
251
  const nodeEnd = offset + nodeLength;
252
+ const span = { start: base + offset, end: base + nodeEnd };
307
253
 
308
254
  if (info.typeClass === "elementary") {
309
- return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
255
+ return { node: { typeClass: "elementary", typeCode: code, isDynamic, staticSize, span }, next: nodeEnd };
310
256
  }
311
257
 
312
258
  // Only composites nest; a leaf below the deepest allowed composite is fine.
@@ -320,7 +266,7 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
320
266
 
321
267
  if (info.typeClass === "tuple") {
322
268
  const fieldCountOffset = offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE;
323
- if (fieldCountOffset + 2 > data.length) {
269
+ if (fieldCountOffset + DF.TUPLE_FIELDCOUNT_SIZE > data.length) {
324
270
  throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
325
271
  }
326
272
  const fieldCount = readU16(data, fieldCountOffset);
@@ -335,17 +281,21 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
335
281
  );
336
282
  }
337
283
 
284
+ const fields: DecodedNode[] = [];
338
285
  let cursor = offset + DF.TUPLE_HEADER_SIZE;
339
286
  for (let i = 0; i < fieldCount; i++) {
340
- cursor = parseNode(data, cursor, depth + 1).next;
287
+ const field = parseNode(data, cursor, depth + 1, base);
288
+ fields.push(field.node);
289
+ cursor = field.next;
341
290
  }
342
291
 
343
- return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
292
+ return { node: { typeClass: "tuple", typeCode: code, isDynamic, staticSize, span, fields }, next: nodeEnd };
344
293
  }
345
294
 
295
+ const element = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1, base);
296
+
346
297
  if (info.typeClass === "staticArray") {
347
- const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
348
- const lengthOffset = elemResult.next;
298
+ const lengthOffset = element.next;
349
299
  if (lengthOffset + DF.ARRAY_LENGTH_SIZE > data.length) {
350
300
  throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
351
301
  }
@@ -361,25 +311,36 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
361
311
  );
362
312
  }
363
313
 
364
- return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
314
+ const node: DecodedNode = {
315
+ typeClass: "staticArray",
316
+ typeCode: code,
317
+ isDynamic,
318
+ staticSize,
319
+ span,
320
+ length,
321
+ element: element.node,
322
+ };
323
+ return { node, next: nodeEnd };
365
324
  }
366
325
 
367
- parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
368
- return { typeCode: code, isDynamic: true, staticSize: 0, next: nodeEnd };
326
+ const node: DecodedNode = {
327
+ typeClass: "dynamicArray",
328
+ typeCode: code,
329
+ isDynamic: true,
330
+ staticSize: 0,
331
+ span,
332
+ element: element.node,
333
+ };
334
+ return { node, next: nodeEnd };
369
335
  }
370
336
 
371
- /** Convert a zero-based param index to a BE16 hex path step. */
372
- function indexToPath(index: number): Hex {
373
- return `0x${index.toString(16).padStart(4, "0")}`;
374
- }
375
-
376
- type DecodedDescriptor = {
377
- version: number;
378
- params: DecodedParam[];
379
- };
380
-
381
- /** Decode a binary descriptor blob, returning its structural representation. */
382
- export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescriptor } {
337
+ /**
338
+ * Decode a binary descriptor blob, returning its structural representation.
339
+ * @param data - The descriptor bytes.
340
+ * @param base - Byte offset of the descriptor in the blob its spans locate.
341
+ * @throws {CallciumError} When the descriptor breaks a well-formedness invariant.
342
+ */
343
+ export function decodeDescriptor(data: Uint8Array, base = 0): DecodedDescriptor {
383
344
  if (data.length < 1) {
384
345
  throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
385
346
  }
@@ -393,29 +354,21 @@ export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescrip
393
354
  throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
394
355
  }
395
356
 
396
- const params: DecodedParam[] = [];
357
+ const params: DecodedNode[] = [];
397
358
  let cursor: number = DF.HEADER_SIZE;
398
359
 
399
360
  while (cursor < data.length) {
400
- const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
361
+ const { node, next } = parseNode(data, cursor, 1, base);
401
362
 
402
363
  if (params.length === DF.MAX_PARAMS) {
403
364
  throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DF.MAX_PARAMS} top-level params`);
404
365
  }
405
366
 
406
- params.push({
407
- index: params.length,
408
- typeCode,
409
- isDynamic,
410
- staticSize,
411
- path: indexToPath(params.length),
412
- span: { start: cursor, end: next },
413
- });
414
-
367
+ params.push(node);
415
368
  cursor = next;
416
369
  }
417
370
 
418
- const declaredCount = data[1]!;
371
+ const declaredCount = data[DF.HEADER_PARAMCOUNT_OFFSET]!;
419
372
  if (params.length !== declaredCount) {
420
373
  throw new CallciumError(
421
374
  "PARAM_COUNT_MISMATCH",
@@ -423,8 +376,69 @@ export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescrip
423
376
  );
424
377
  }
425
378
 
426
- return { descriptor: { version, params } };
379
+ return {
380
+ version: { value: version, span: { start: base, end: base + DF.HEADER_PARAMCOUNT_OFFSET } },
381
+ paramCount: {
382
+ value: declaredCount,
383
+ span: { start: base + DF.HEADER_PARAMCOUNT_OFFSET, end: base + DF.HEADER_SIZE },
384
+ },
385
+ params,
386
+ span: { start: base, end: base + data.length },
387
+ };
388
+ }
389
+
390
+ ///////////////////////////////////////////////////////////////////////////
391
+ // Descriptor inspection
392
+ ///////////////////////////////////////////////////////////////////////////
393
+
394
+ /**
395
+ * Decode a binary descriptor and check every composite node against its children.
396
+ * @param desc - The descriptor bytes.
397
+ * @param base - Byte offset of the descriptor in the blob its spans locate.
398
+ * @throws {CallciumError} If the descriptor is malformed, or a composite node disagrees with its children.
399
+ */
400
+ export function inspectDescriptor(desc: Uint8Array, base = 0): DecodedDescriptor {
401
+ const decoded = decodeDescriptor(desc, base);
402
+ Descriptor.validateMeta(desc);
403
+ return decoded;
404
+ }
405
+
406
+ /**
407
+ * Inspect a binary descriptor: its header fields and its parameters as a tree of nodes, each with the bytes it spans.
408
+ * @param desc - Binary descriptor bytes (with version+paramCount header).
409
+ * @returns The decoded descriptor.
410
+ * @throws {CallciumError} If the descriptor is malformed, or a composite node disagrees with its children.
411
+ */
412
+ function inspect(desc: Uint8Array): DecodedDescriptor {
413
+ return inspectDescriptor(desc);
414
+ }
415
+
416
+ /** Reconstruct the ABI type string of a decoded node. */
417
+ function typeString(node: DecodedNode): string {
418
+ switch (node.typeClass) {
419
+ case "elementary":
420
+ return lookupTypeCode(node.typeCode).label;
421
+ case "tuple":
422
+ return `(${node.fields.map(typeString).join(",")})`;
423
+ case "staticArray":
424
+ return `${typeString(node.element)}[${node.length}]`;
425
+ case "dynamicArray":
426
+ return `${typeString(node.element)}[]`;
427
+ default:
428
+ return node satisfies never;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Reconstruct a comma-separated list of ABI type strings from a binary descriptor. `fromTypes(toTypes(desc))` returns
434
+ * `desc` for every descriptor `inspect` accepts.
435
+ * @param desc - Binary descriptor bytes (with version+paramCount header).
436
+ * @returns Comma-separated ABI type strings, for example `"address,uint256,(bool,bytes32)[]"`.
437
+ * @throws {CallciumError} If the descriptor is malformed, or a composite node disagrees with its children.
438
+ */
439
+ function toTypes(desc: Uint8Array): string {
440
+ return inspect(desc).params.map(typeString).join(",");
427
441
  }
428
442
 
429
- /** Encode and decode descriptors. */
430
- export const DescriptorCoder = { fromTypes, toTypes };
443
+ /** Encode descriptors from type strings, inspect them and read their type strings back. */
444
+ export const DescriptorCoder = { fromTypes, inspect, toTypes };