@callcium/sdk 0.12.0 → 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,13 +1,13 @@
1
1
  import { concatBytes } from "@noble/hashes/utils.js";
2
2
 
3
3
  import { readU16 } from "./bytes";
4
- import { DescriptorFormat as DF, TypeCode } from "./constants";
4
+ import { DescriptorFormat as DF } from "./constants";
5
5
  import { Descriptor, readNode } from "./descriptor";
6
6
  import { CallciumError } from "./errors";
7
- import { classifyTypeCode, lookupTypeCode } from "./operators";
7
+ import { lookupTypeCode } from "./operators";
8
8
  import { address, array, bool, bytes, bytesN, function_, intN, string_, tuple, uintN } from "./type-desc";
9
9
 
10
- import type { DecodedParam, Hex } from "./types";
10
+ import type { DecodedDescriptor, DecodedNode } from "./types";
11
11
 
12
12
  ///////////////////////////////////////////////////////////////////////////
13
13
  // Splitting helpers
@@ -138,12 +138,19 @@ function parseBaseType(input: string, start: number, end: number): Uint8Array {
138
138
  if (segment.startsWith("(")) return parseTuple(input, start, end);
139
139
 
140
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.
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));
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
  ///////////////////////////////////////////////////////////////////////////
@@ -197,7 +204,7 @@ function parseType(input: string, start: number, end: number): Uint8Array {
197
204
  /**
198
205
  * Encode a comma-separated list of ABI type strings into a binary descriptor.
199
206
  *
200
- * @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"`.
201
208
  * @returns Binary descriptor bytes starting with the version+paramCount header.
202
209
  * @throws {CallciumError} With code `MALFORMED_TYPE_STRING` for malformed input.
203
210
  * @throws {CallciumError} With code `UNKNOWN_TYPE` for unrecognised type names.
@@ -228,76 +235,24 @@ function fromTypes(typesCsv: string): Uint8Array {
228
235
  return result;
229
236
  }
230
237
 
231
- ///////////////////////////////////////////////////////////////////////////
232
- // Inverse: descriptor bytes → type string
233
- ///////////////////////////////////////////////////////////////////////////
234
-
235
- /** Reconstruct an ABI type string from a descriptor node at the given offset. */
236
- function nodeToTypeString(desc: Uint8Array, offset: number): string {
237
- const { typeCode } = Descriptor.inspect(desc, offset);
238
-
239
- if (typeCode === TypeCode.TUPLE) {
240
- const fieldCount = Descriptor.tupleFieldCount(desc, offset);
241
- const fieldTypes: string[] = [];
242
- let fieldOffset = offset + DF.TUPLE_HEADER_SIZE;
243
- for (let i = 0; i < fieldCount; i++) {
244
- fieldTypes.push(nodeToTypeString(desc, fieldOffset));
245
- fieldOffset += Descriptor.nodeLength(desc, fieldOffset);
246
- }
247
- return `(${fieldTypes.join(",")})`;
248
- }
249
-
250
- if (typeCode === TypeCode.STATIC_ARRAY) {
251
- const length = Descriptor.staticArrayLength(desc, offset);
252
- const elemType = nodeToTypeString(desc, Descriptor.arrayElementOffset(offset));
253
- return `${elemType}[${length}]`;
254
- }
255
-
256
- if (typeCode === TypeCode.DYNAMIC_ARRAY) {
257
- const elemType = nodeToTypeString(desc, Descriptor.arrayElementOffset(offset));
258
- return `${elemType}[]`;
259
- }
260
-
261
- return lookupTypeCode(typeCode).label;
262
- }
263
-
264
- /**
265
- * Reconstruct a comma-separated list of ABI type strings from a binary descriptor.
266
- *
267
- * Inverse of `fromTypes`: `toTypes(fromTypes(s))` returns `s` for any valid input.
268
- *
269
- * @param desc - Binary descriptor bytes (with version+paramCount header).
270
- * @returns Comma-separated ABI type strings, e.g. `"address,uint256,(bool,bytes32)[]"`.
271
- * @throws {CallciumError} If the descriptor is malformed.
272
- */
273
- function toTypes(desc: Uint8Array): string {
274
- const count = Descriptor.paramCount(desc);
275
- const types: string[] = [];
276
- let offset = DF.HEADER_SIZE;
277
- for (let i = 0; i < count; i++) {
278
- types.push(nodeToTypeString(desc, offset));
279
- offset += Descriptor.nodeLength(desc, offset);
280
- }
281
- return types.join(",");
282
- }
283
-
284
238
  ///////////////////////////////////////////////////////////////////////////
285
239
  // Descriptor binary decoder
286
240
  ///////////////////////////////////////////////////////////////////////////
287
241
 
288
- /** Shape of a parsed descriptor node and the offset just past it. */
289
- 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 };
290
244
 
291
- /** Recursively parse a single descriptor node starting at offset. */
292
- 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 {
293
247
  const { typeCode: code, staticWords, nodeLength } = readNode(data, offset);
294
- const info = classifyTypeCode(code);
248
+ const info = lookupTypeCode(code);
295
249
  const isDynamic = staticWords === 0;
296
250
  const staticSize = isDynamic ? 0 : staticWords * 32;
297
251
  const nodeEnd = offset + nodeLength;
252
+ const span = { start: base + offset, end: base + nodeEnd };
298
253
 
299
254
  if (info.typeClass === "elementary") {
300
- return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
255
+ return { node: { typeClass: "elementary", typeCode: code, isDynamic, staticSize, span }, next: nodeEnd };
301
256
  }
302
257
 
303
258
  // Only composites nest; a leaf below the deepest allowed composite is fine.
@@ -311,7 +266,7 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
311
266
 
312
267
  if (info.typeClass === "tuple") {
313
268
  const fieldCountOffset = offset + DF.TYPECODE_SIZE + DF.COMPOSITE_META_SIZE;
314
- if (fieldCountOffset + 2 > data.length) {
269
+ if (fieldCountOffset + DF.TUPLE_FIELDCOUNT_SIZE > data.length) {
315
270
  throw new CallciumError("UNEXPECTED_END", "Incomplete tuple header", offset);
316
271
  }
317
272
  const fieldCount = readU16(data, fieldCountOffset);
@@ -326,17 +281,21 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
326
281
  );
327
282
  }
328
283
 
284
+ const fields: DecodedNode[] = [];
329
285
  let cursor = offset + DF.TUPLE_HEADER_SIZE;
330
286
  for (let i = 0; i < fieldCount; i++) {
331
- 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;
332
290
  }
333
291
 
334
- return { typeCode: code, isDynamic, staticSize, next: nodeEnd };
292
+ return { node: { typeClass: "tuple", typeCode: code, isDynamic, staticSize, span, fields }, next: nodeEnd };
335
293
  }
336
294
 
295
+ const element = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1, base);
296
+
337
297
  if (info.typeClass === "staticArray") {
338
- const elemResult = parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
339
- const lengthOffset = elemResult.next;
298
+ const lengthOffset = element.next;
340
299
  if (lengthOffset + DF.ARRAY_LENGTH_SIZE > data.length) {
341
300
  throw new CallciumError("UNEXPECTED_END", "Missing static array length suffix", offset);
342
301
  }
@@ -352,25 +311,36 @@ function parseNode(data: Uint8Array, offset: number, depth: number): ParseResult
352
311
  );
353
312
  }
354
313
 
355
- 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 };
356
324
  }
357
325
 
358
- parseNode(data, Descriptor.arrayElementOffset(offset), depth + 1);
359
- return { typeCode: code, isDynamic: true, staticSize: 0, next: nodeEnd };
360
- }
361
-
362
- /** Convert a zero-based param index to a BE16 hex path step. */
363
- function indexToPath(index: number): Hex {
364
- return `0x${index.toString(16).padStart(4, "0")}`;
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 };
365
335
  }
366
336
 
367
- type DecodedDescriptor = {
368
- version: number;
369
- params: DecodedParam[];
370
- };
371
-
372
- /** Decode a binary descriptor blob, returning its structural representation. */
373
- 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 {
374
344
  if (data.length < 1) {
375
345
  throw new CallciumError("MALFORMED_HEADER", "Descriptor is empty");
376
346
  }
@@ -384,29 +354,21 @@ export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescrip
384
354
  throw new CallciumError("MALFORMED_HEADER", "Descriptor too short for header");
385
355
  }
386
356
 
387
- const params: DecodedParam[] = [];
357
+ const params: DecodedNode[] = [];
388
358
  let cursor: number = DF.HEADER_SIZE;
389
359
 
390
360
  while (cursor < data.length) {
391
- const { typeCode, isDynamic, staticSize, next } = parseNode(data, cursor, 1);
361
+ const { node, next } = parseNode(data, cursor, 1, base);
392
362
 
393
363
  if (params.length === DF.MAX_PARAMS) {
394
364
  throw new CallciumError("TOO_MANY_PARAMS", `Descriptor exceeds the maximum of ${DF.MAX_PARAMS} top-level params`);
395
365
  }
396
366
 
397
- params.push({
398
- index: params.length,
399
- typeCode,
400
- isDynamic,
401
- staticSize,
402
- path: indexToPath(params.length),
403
- span: { start: cursor, end: next },
404
- });
405
-
367
+ params.push(node);
406
368
  cursor = next;
407
369
  }
408
370
 
409
- const declaredCount = data[1]!;
371
+ const declaredCount = data[DF.HEADER_PARAMCOUNT_OFFSET]!;
410
372
  if (params.length !== declaredCount) {
411
373
  throw new CallciumError(
412
374
  "PARAM_COUNT_MISMATCH",
@@ -414,8 +376,69 @@ export function decodeDescriptor(data: Uint8Array): { descriptor: DecodedDescrip
414
376
  );
415
377
  }
416
378
 
417
- 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(",");
418
441
  }
419
442
 
420
- /** Encode and decode descriptors. */
421
- 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 };