From d72dd56d31889564b2e77ab342fcc6e048fb8ec1 Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Sun, 23 Nov 2025 23:52:10 +0100 Subject: [PATCH 1/8] fix(typesystem): improve tuple error handling Replace generic error message with descriptive ErrInvalidTuple that explains the issue and provides context for debugging. - Created ErrInvalidTuple error class - Updated Tuple.fromItems to use new error - Added test coverage for error cases Fixed TODO: Define a better error for tuple validation. --- src/abi/typesystem/tuple.spec.ts | 22 ++++++++++++++++++++++ src/abi/typesystem/tuple.ts | 5 +++-- src/core/errors.ts | 9 +++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/abi/typesystem/tuple.spec.ts diff --git a/src/abi/typesystem/tuple.spec.ts b/src/abi/typesystem/tuple.spec.ts new file mode 100644 index 000000000..3d6704dd1 --- /dev/null +++ b/src/abi/typesystem/tuple.spec.ts @@ -0,0 +1,22 @@ +import { assert } from "chai"; +import { ErrInvalidTuple } from "../../core/errors"; +import { U32Value } from "./numerical"; +import { Tuple } from "./tuple"; + +describe("test tuple error handling", () => { + it("should throw ErrInvalidTuple for empty items", () => { + assert.throws(() => Tuple.fromItems([]), ErrInvalidTuple, "Cannot create tuple from empty items array"); + }); + + it("should create tuple from valid items", () => { + const items = [new U32Value(1), new U32Value(2)]; + const tuple = Tuple.fromItems(items); + assert.equal(tuple.getFields().length, 2); + }); + + it("should create tuple from single item", () => { + const items = [new U32Value(42)]; + const tuple = Tuple.fromItems(items); + assert.equal(tuple.getFields().length, 1); + }); +}); diff --git a/src/abi/typesystem/tuple.ts b/src/abi/typesystem/tuple.ts index e52fe3c0f..ced9c58f4 100644 --- a/src/abi/typesystem/tuple.ts +++ b/src/abi/typesystem/tuple.ts @@ -48,8 +48,9 @@ export class Tuple extends Struct { static fromItems(items: TypedValue[]): Tuple { if (items.length < 1) { - // TODO: Define a better error. - throw new errors.ErrTypingSystem("bad tuple items"); + throw new errors.ErrInvalidTuple( + "Cannot create tuple from empty items array. At least one item is required.", + ); } let fieldsTypes = items.map((item) => item.getType()); diff --git a/src/core/errors.ts b/src/core/errors.ts index 69954e87a..bd880a2ba 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -236,6 +236,15 @@ export class ErrTypingSystem extends Err { } } +/** + * Signals an invalid tuple construction. + */ +export class ErrInvalidTuple extends Err { + public constructor(message: string) { + super(`Invalid tuple: ${message}`); + } +} + /** * Signals a missing field on a struct. */ From 81d5ebb422ac87e817f61a7bfb5f13241b11ad46 Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Sun, 23 Nov 2025 23:57:08 +0100 Subject: [PATCH 2/8] fix(typesystem): handle array types dynamically in TypeMapper Replace hardcoded array2-array256 factory entries with dynamic parsing that can handle arbitrary array sizes automatically. - Created parseArrayType helper function in typeExpressionParser - Updated TypeMapper to use dynamic array parsing - Removed 11 hardcoded array factory entries - Added test coverage for arbitrary array sizes (array999, array1000, array5) Fixed TODO: Array types are hardcoded (array2, array6, array8, etc.). Need dynamic parsing. --- src/abi/typesystem/typeExpressionParser.ts | 18 +++++++++++++++++ src/abi/typesystem/typeMapper.spec.ts | 6 ++++++ src/abi/typesystem/typeMapper.ts | 23 +++++++++------------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/abi/typesystem/typeExpressionParser.ts b/src/abi/typesystem/typeExpressionParser.ts index 076f44a04..2b2372125 100644 --- a/src/abi/typesystem/typeExpressionParser.ts +++ b/src/abi/typesystem/typeExpressionParser.ts @@ -1,8 +1,26 @@ import { TypeFormula } from "../../abi/typeFormula"; import { TypeFormulaParser } from "../../abi/typeFormulaParser"; import { ErrTypingSystem } from "../../core/errors"; +import { ArrayVecType } from "./genericArray"; import { Type } from "./types"; +/** + * Parses array type expressions like "array16" into ArrayVecType. + * @param typeName - The type name to parse (e.g., "array16", "array256") + * @param typeParameter - The element type for the array + * @returns ArrayVecType if the typeName matches array pattern, null otherwise + */ +export function parseArrayType(typeName: string, typeParameter: Type): Type | null { + const arrayMatch = typeName.match(/^array(\d+)$/); + + if (arrayMatch) { + const length = parseInt(arrayMatch[1], 10); + return new ArrayVecType(length, typeParameter); + } + + return null; +} + export class TypeExpressionParser { private readonly backingTypeFormulaParser: TypeFormulaParser; diff --git a/src/abi/typesystem/typeMapper.spec.ts b/src/abi/typesystem/typeMapper.spec.ts index 1eff9a9f8..125935a56 100644 --- a/src/abi/typesystem/typeMapper.spec.ts +++ b/src/abi/typesystem/typeMapper.spec.ts @@ -73,6 +73,12 @@ describe("test mapper", () => { testArrayMapping("array256", 256, new BigUIntType()); }); + it("should map arrays with arbitrary sizes", () => { + testArrayMapping("array999", 999, new U32Type()); + testArrayMapping("array1000", 1000, new BytesType()); + testArrayMapping("array5
", 5, new AddressType()); + }); + function testArrayMapping(expression: string, size: number, typeParameter: Type) { let type = parser.parse(expression); let mappedType = mapper.mapType(type); diff --git a/src/abi/typesystem/typeMapper.ts b/src/abi/typesystem/typeMapper.ts index 84a0de57f..f9e7d21a2 100644 --- a/src/abi/typesystem/typeMapper.ts +++ b/src/abi/typesystem/typeMapper.ts @@ -9,7 +9,6 @@ import { EnumType, EnumVariantDefinition } from "./enum"; import { ExplicitEnumType, ExplicitEnumVariantDefinition } from "./explicit-enum"; import { FieldDefinition } from "./fields"; import { ListType, OptionType } from "./generic"; -import { ArrayVecType } from "./genericArray"; import { H256Type } from "./h256"; import { ManagedDecimalType } from "./managedDecimal"; import { ManagedDecimalSignedType } from "./managedDecimalSigned"; @@ -30,6 +29,7 @@ import { StringType } from "./string"; import { StructType } from "./struct"; import { TokenIdentifierType } from "./tokenIdentifier"; import { TupleType } from "./tuple"; +import { parseArrayType } from "./typeExpressionParser"; import { CustomType, Type } from "./types"; import { VariadicType } from "./variadic"; @@ -66,19 +66,7 @@ export class TypeMapper { ["tuple6", (...typeParameters: Type[]) => new TupleType(...typeParameters)], ["tuple7", (...typeParameters: Type[]) => new TupleType(...typeParameters)], ["tuple8", (...typeParameters: Type[]) => new TupleType(...typeParameters)], - // Known-length arrays. - // TODO: Handle these in typeExpressionParser! - ["array2", (...typeParameters: Type[]) => new ArrayVecType(2, typeParameters[0])], - ["array6", (...typeParameters: Type[]) => new ArrayVecType(6, typeParameters[0])], - ["array8", (...typeParameters: Type[]) => new ArrayVecType(8, typeParameters[0])], - ["array16", (...typeParameters: Type[]) => new ArrayVecType(16, typeParameters[0])], - ["array20", (...typeParameters: Type[]) => new ArrayVecType(20, typeParameters[0])], - ["array32", (...typeParameters: Type[]) => new ArrayVecType(32, typeParameters[0])], - ["array46", (...typeParameters: Type[]) => new ArrayVecType(46, typeParameters[0])], - ["array48", (...typeParameters: Type[]) => new ArrayVecType(48, typeParameters[0])], - ["array64", (...typeParameters: Type[]) => new ArrayVecType(64, typeParameters[0])], - ["array128", (...typeParameters: Type[]) => new ArrayVecType(128, typeParameters[0])], - ["array256", (...typeParameters: Type[]) => new ArrayVecType(256, typeParameters[0])], + // Array types are now handled dynamically by parseArrayType() in mapGenericType() ["ManagedDecimal", (...metadata: any) => new ManagedDecimalType(metadata)], ["ManagedDecimalSigned", (...metadata: any) => new ManagedDecimalSignedType(metadata)], ]); @@ -230,6 +218,13 @@ export class TypeMapper { let factory = this.openTypesFactories.get(type.getName()); if (!factory) { + // Try dynamic array type parsing (e.g., array2, array16, array256, etc.) + if (mappedTypeParameters.length > 0) { + const arrayType = parseArrayType(type.getName(), mappedTypeParameters[0]); + if (arrayType) { + return arrayType; + } + } throw new errors.ErrTypingSystem(`Cannot map the generic type "${type.getName()}" to a known type`); } if (type.hasMetadata()) { From 879ea606f0396fcc13128a7fc7b71b92f699ad0c Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Mon, 24 Nov 2025 00:18:53 +0100 Subject: [PATCH 3/8] fix(typesystem): move native conversion logic to type classes Move conversion logic from NativeSerializer to respective type classes for better separation of concerns and encapsulation. - Added BytesValue.fromNative() for bytes conversion - Added StringValue.fromNative() for string conversion - Added AddressValue.fromNative() for address conversion - Added NumericalValue.fromNative() for numerical conversion - Updated NativeSerializer to use new static methods Fixed TODO: move logic to typesystem/bytes.ts Fixed TODO: move logic to typesystem/string.ts Fixed TODO: move logic to typesystem/address.ts Fixed TODO: move logic to typesystem/numerical.ts --- src/abi/nativeSerializer.ts | 104 ++++++-------------------------- src/abi/typesystem/address.ts | 26 ++++++++ src/abi/typesystem/bytes.ts | 31 ++++++++++ src/abi/typesystem/numerical.ts | 34 +++++++++++ src/abi/typesystem/string.ts | 22 +++++++ 5 files changed, 131 insertions(+), 86 deletions(-) diff --git a/src/abi/nativeSerializer.ts b/src/abi/nativeSerializer.ts index 0e0c4542d..9e9d5f331 100644 --- a/src/abi/nativeSerializer.ts +++ b/src/abi/nativeSerializer.ts @@ -2,15 +2,10 @@ import BigNumber from "bignumber.js"; import { Address } from "../core/address"; import { ErrInvalidArgument } from "../core/errors"; -import { numberToPaddedHex } from "../core/utils.codec"; import { ArgumentErrorContext } from "./argumentErrorContext"; import { AddressType, AddressValue, - BigIntType, - BigIntValue, - BigUIntType, - BigUIntValue, BooleanType, BooleanValue, BytesType, @@ -24,25 +19,19 @@ import { ExplicitEnumType, ExplicitEnumValue, Field, - I16Type, - I16Value, - I32Type, - I32Value, - I64Type, - I64Value, - I8Type, - I8Value, isTyped, List, ListType, ManagedDecimalType, ManagedDecimalValue, NumericalType, + NumericalValue, OptionalType, OptionalValue, OptionType, OptionValue, PrimitiveType, + StringValue, Struct, StructType, TokenIdentifierType, @@ -51,14 +40,6 @@ import { TupleType, Type, TypedValue, - U16Type, - U16Value, - U32Type, - U32Value, - U64Type, - U64Value, - U8Type, - U8Value, VariadicType, VariadicValue, } from "./typesystem"; @@ -361,93 +342,44 @@ export namespace NativeSerializer { errorContext.throwError(`(function: toManagedDecimal) unsupported native type ${typeof native}`); } - // TODO: move logic to typesystem/bytes.ts function convertNativeToBytesValue(native: NativeTypes.NativeBytes, errorContext: ArgumentErrorContext) { - const innerValue = native.valueOf(); - - if (native === undefined) { + try { + return BytesValue.fromNative(native); + } catch (error) { errorContext.convertError(native, "BytesValue"); } - if (native instanceof Buffer) { - return new BytesValue(native); - } - if (typeof native === "string") { - return BytesValue.fromUTF8(native); - } - if (innerValue instanceof Buffer) { - return new BytesValue(innerValue); - } - if (typeof innerValue === "number") { - return BytesValue.fromHex(numberToPaddedHex(innerValue)); - } - - errorContext.convertError(native, "BytesValue"); } - // TODO: move logic to typesystem/string.ts function convertNativeToString(native: NativeTypes.NativeBuffer, errorContext: ArgumentErrorContext): string { - if (native === undefined) { + try { + const stringValue = StringValue.fromNative(native); + return stringValue.valueOf(); + } catch (error) { errorContext.convertError(native, "Buffer"); } - if (native instanceof Buffer) { - return native.toString(); - } - if (typeof native === "string") { - return native; - } - errorContext.convertError(native, "Buffer"); } - // TODO: move logic to typesystem/address.ts export function convertNativeToAddress( native: NativeTypes.NativeAddress, errorContext: ArgumentErrorContext, ): Address { - if ((native).toBech32) { - return
native; - } - if ((native).getAddress) { - return (native).getAddress(); - } - - switch (native.constructor) { - case Buffer: - case String: - return new Address(native); - default: - errorContext.convertError(native, "Address"); + try { + const addressValue = AddressValue.fromNative(native); + return addressValue.valueOf(); + } catch (error) { + errorContext.convertError(native, "Address"); } } - // TODO: move logic to typesystem/numerical.ts function convertNumericalType( number: NativeTypes.NativeBigNumber, type: Type, errorContext: ArgumentErrorContext, ): TypedValue { - switch (type.constructor) { - case U8Type: - return new U8Value(number); - case I8Type: - return new I8Value(number); - case U16Type: - return new U16Value(number); - case I16Type: - return new I16Value(number); - case U32Type: - return new U32Value(number); - case I32Type: - return new I32Value(number); - case U64Type: - return new U64Value(number); - case I64Type: - return new I64Value(number); - case BigUIntType: - return new BigUIntValue(number); - case BigIntType: - return new BigIntValue(number); - default: - errorContext.unhandledType("convertNumericalType", type); + try { + return NumericalValue.fromNative(number, type); + } catch (error) { + errorContext.unhandledType("convertNumericalType", type); } } } diff --git a/src/abi/typesystem/address.ts b/src/abi/typesystem/address.ts index c7e0e4b1e..ad44d695f 100644 --- a/src/abi/typesystem/address.ts +++ b/src/abi/typesystem/address.ts @@ -1,4 +1,5 @@ import { Address } from "../../core/address"; +import * as errors from "../../core/errors"; import { PrimitiveType, PrimitiveValue } from "./types"; export class AddressType extends PrimitiveType { @@ -29,6 +30,31 @@ export class AddressValue extends PrimitiveValue { return AddressValue.ClassName; } + /** + * Creates an AddressValue from various native JavaScript types. + * @param native - Native value (Address, object with getAddress() or toBech32(), Buffer, or string) + * @returns AddressValue instance + * @throws ErrInvalidArgument if conversion fails + */ + static fromNative( + native: Address | { getAddress(): Address } | { toBech32(): string } | Buffer | string, + ): AddressValue { + if ((native).toBech32) { + return new AddressValue(
native); + } + if ((native).getAddress) { + return new AddressValue((native).getAddress()); + } + + switch (native.constructor) { + case Buffer: + case String: + return new AddressValue(new Address(native)); + default: + throw new errors.ErrInvalidArgument(`Cannot convert value to AddressValue: ${native}`); + } + } + /** * Returns whether two objects have the same value. * diff --git a/src/abi/typesystem/bytes.ts b/src/abi/typesystem/bytes.ts index 866039adb..ecaf05a7d 100644 --- a/src/abi/typesystem/bytes.ts +++ b/src/abi/typesystem/bytes.ts @@ -1,3 +1,5 @@ +import * as errors from "../../core/errors"; +import { numberToPaddedHex } from "../../core/utils.codec"; import { PrimitiveType, PrimitiveValue } from "./types"; export class BytesType extends PrimitiveType { @@ -41,6 +43,35 @@ export class BytesValue extends PrimitiveValue { return new BytesValue(buffer); } + /** + * Creates a BytesValue from various native JavaScript types. + * @param native - Native value (Buffer, string, or object with valueOf()) + * @returns BytesValue instance + * @throws ErrInvalidArgument if conversion fails + */ + static fromNative(native: Buffer | string | { valueOf(): Buffer | number }): BytesValue { + if (native === undefined) { + throw new errors.ErrInvalidArgument("Cannot convert undefined to BytesValue"); + } + + const innerValue = native.valueOf(); + + if (native instanceof Buffer) { + return new BytesValue(native); + } + if (typeof native === "string") { + return BytesValue.fromUTF8(native); + } + if (innerValue instanceof Buffer) { + return new BytesValue(innerValue); + } + if (typeof innerValue === "number") { + return BytesValue.fromHex(numberToPaddedHex(innerValue)); + } + + throw new errors.ErrInvalidArgument(`Cannot convert value to BytesValue: ${native}`); + } + getLength(): number { return this.value.length; } diff --git a/src/abi/typesystem/numerical.ts b/src/abi/typesystem/numerical.ts index de2a97df0..6ff1ab2e0 100644 --- a/src/abi/typesystem/numerical.ts +++ b/src/abi/typesystem/numerical.ts @@ -180,6 +180,40 @@ export class NumericalValue extends PrimitiveValue { return NumericalValue.ClassName; } + /** + * Creates appropriate numerical TypedValue from native number based on the type. + * @param value - Native BigNumber or bigint value + * @param type - Target numerical type + * @returns Appropriate numerical TypedValue + * @throws ErrInvalidArgument if type is not supported + */ + static fromNative(value: BigNumber.Value | bigint, type: NumericalType): NumericalValue { + switch (type.constructor) { + case U8Type: + return new U8Value(value); + case I8Type: + return new I8Value(value); + case U16Type: + return new U16Value(value); + case I16Type: + return new I16Value(value); + case U32Type: + return new U32Value(value); + case I32Type: + return new I32Value(value); + case U64Type: + return new U64Value(value); + case I64Type: + return new I64Value(value); + case BigUIntType: + return new BigUIntValue(value); + case BigIntType: + return new BigIntValue(value); + default: + throw new errors.ErrInvalidArgument(`Unsupported numerical type: ${type.getName()}`); + } + } + /** * Returns whether two objects have the same value. * diff --git a/src/abi/typesystem/string.ts b/src/abi/typesystem/string.ts index 901c897bf..6d4299234 100644 --- a/src/abi/typesystem/string.ts +++ b/src/abi/typesystem/string.ts @@ -1,3 +1,4 @@ +import * as errors from "../../core/errors"; import { PrimitiveType, PrimitiveValue } from "./types"; export class StringType extends PrimitiveType { @@ -40,6 +41,27 @@ export class StringValue extends PrimitiveValue { return new StringValue(decodedValue); } + /** + * Creates a StringValue from native JavaScript types. + * @param native - Native value (Buffer or string) + * @returns StringValue instance + * @throws ErrInvalidArgument if conversion fails + */ + static fromNative(native: Buffer | string): StringValue { + if (native === undefined) { + throw new errors.ErrInvalidArgument("Cannot convert undefined to StringValue"); + } + + if (native instanceof Buffer) { + return new StringValue(native.toString()); + } + if (typeof native === "string") { + return new StringValue(native); + } + + throw new errors.ErrInvalidArgument(`Cannot convert value to StringValue: ${native}`); + } + getLength(): number { return this.value.length; } From 528d55b136ee56cba41260176e15a7b084dd717c Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Mon, 24 Nov 2025 00:28:35 +0100 Subject: [PATCH 4/8] fix(core): merge guard functions for better code organization Consolidate guardValueIsSet and guardValueIsSetWithMessage into a single function that handles both use cases automatically. - Updated guardValueIsSet to accept either variable name or custom message - Deprecated guardValueIsSetWithMessage (kept for backward compatibility) - Added JSDoc documentation Fixed TODO: merge with guardValueIsSetWithMessage Fixed TODO: merge with guardValueIsSet --- src/core/utils.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/core/utils.ts b/src/core/utils.ts index c80c7c6df..b07210d8a 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -8,18 +8,27 @@ export function guardTrue(value: boolean, what: string) { } } -// TODO: merge with guardValueIsSetWithMessage -export function guardValueIsSet(name: string, value?: any | null | undefined) { - guardValueIsSetWithMessage(`${name} isn't set (null or undefined)`, value); -} - -// TODO: merge with guardValueIsSet -export function guardValueIsSetWithMessage(message: string, value?: any | null | undefined) { +/** + * Guards that a value is set (not null or undefined). + * @param nameOrMessage - Either a variable name or a custom error message + * @param value - The value to check + */ +export function guardValueIsSet(nameOrMessage: string, value?: any | null | undefined) { if (value == null || value === undefined) { + const message = nameOrMessage.includes(" ") + ? nameOrMessage + : `${nameOrMessage} isn't set (null or undefined)`; throw new errors.ErrInvariantFailed(message); } } +/** + * @deprecated Use guardValueIsSet instead. This function is kept for backward compatibility. + */ +export function guardValueIsSetWithMessage(message: string, value?: any | null | undefined) { + guardValueIsSet(message, value); +} + export function guardSameLength(a: any[], b: any[]) { a = a || []; b = b || []; From 71e165ce9a18da24bc01cfdc2a969141541e685d Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Mon, 24 Nov 2025 00:38:42 +0100 Subject: [PATCH 5/8] fix(typesystem): add runtime type assertions in constructors Add type validation in OptionalValue, CompositeValue, VariadicValue, OptionValue, and List constructors to catch type mismatches early. - Added type assertions in OptionalValue constructor - Added type assertions in CompositeValue constructor - Added type assertions in VariadicValue constructor - Added type assertions in OptionValue constructor - Added type assertions in List constructor - Updated test to verify type validation correctly catches bugs Fixed TODO: assert value is of type type.getFirstTypeParameter() Fixed TODO: assert type of each item (wrt. type.getTypeParameters()) Fixed TODO: assert items are of type type.getFirstTypeParameter() --- src/abi/nativeSerializer.spec.ts | 14 +++++++------- src/abi/typesystem/algebraic.ts | 7 ++++++- src/abi/typesystem/composite.ts | 10 +++++++++- src/abi/typesystem/generic.ts | 16 ++++++++++++++-- src/abi/typesystem/variadic.ts | 10 +++++++++- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/abi/nativeSerializer.spec.ts b/src/abi/nativeSerializer.spec.ts index 548aa877f..1dfe2370c 100644 --- a/src/abi/nativeSerializer.spec.ts +++ b/src/abi/nativeSerializer.spec.ts @@ -171,7 +171,7 @@ describe("test native serializer", () => { assert.deepEqual(typedValues[1].valueOf(), [Buffer.from("a"), Buffer.from("b"), Buffer.from("c")]); }); - it("should handle optionals in a strict manner (but it does not)", async () => { + it("should handle optionals in a strict manner", async () => { const endpoint = Abi.create({ endpoints: [ { @@ -186,16 +186,16 @@ describe("test native serializer", () => { ], }).getEndpoint("foo"); + // Creating OptionalValue with wrong type parameter should throw + assert.throws(() => new OptionalValue(new BooleanType() as any, new BooleanValue(true)), /Invariant failed/); + let typedValues = NativeSerializer.nativeToTypedValues( - [new OptionalValue(new BooleanType(), new BooleanValue(true))], + [new OptionalValue(new OptionalType(new BooleanType()), new BooleanValue(true))], endpoint, ); - // Isn't this a bug? Shouldn't it be be OptionalType(BooleanType()), instead? - assert.deepEqual(typedValues[0].getType(), new BooleanType()); - - // Isn't this a bug? Shouldn't it be OptionalValue(OptionalType(BooleanType()), BooleanValue(true)), instead? - assert.deepEqual(typedValues[0], new OptionalValue(new BooleanType(), new BooleanValue(true))); + assert.deepEqual(typedValues[0].getType(), new OptionalType(new BooleanType())); + assert.isTrue(typedValues[0].valueOf()); }); it("should accept a mix between typed values and regular JavaScript objects", async () => { diff --git a/src/abi/typesystem/algebraic.ts b/src/abi/typesystem/algebraic.ts index 249ba5003..4a86cff8b 100644 --- a/src/abi/typesystem/algebraic.ts +++ b/src/abi/typesystem/algebraic.ts @@ -1,3 +1,4 @@ +import * as errors from "../../core/errors"; import { guardValueIsSet } from "../../core/utils"; import { NullType, Type, TypeCardinality, TypedValue } from "./types"; @@ -33,7 +34,11 @@ export class OptionalValue extends TypedValue { constructor(type: OptionalType, value: TypedValue | null = null) { super(type); - // TODO: assert value is of type type.getFirstTypeParameter() + if (value !== null && !value.getType().equals(type.getFirstTypeParameter())) { + throw new errors.ErrInvariantFailed( + `OptionalValue: value type mismatch. Expected: ${type.getFirstTypeParameter().getName()}, got: ${value.getType().getName()}`, + ); + } this.value = value; } diff --git a/src/abi/typesystem/composite.ts b/src/abi/typesystem/composite.ts index 27ed7a88a..3b4dace11 100644 --- a/src/abi/typesystem/composite.ts +++ b/src/abi/typesystem/composite.ts @@ -1,3 +1,4 @@ +import * as errors from "../../core/errors"; import { guardLength } from "../../core/utils"; import { Type, TypeCardinality, TypedValue } from "./types"; @@ -22,7 +23,14 @@ export class CompositeValue extends TypedValue { guardLength(items, type.getTypeParameters().length); - // TODO: assert type of each item (wrt. type.getTypeParameters()). + const typeParameters = type.getTypeParameters(); + for (let i = 0; i < items.length; i++) { + if (!items[i].getType().equals(typeParameters[i])) { + throw new errors.ErrInvariantFailed( + `CompositeValue: item[${i}] type mismatch. Expected: ${typeParameters[i].getName()}, got: ${items[i].getType().getName()}`, + ); + } + } this.items = items; } diff --git a/src/abi/typesystem/generic.ts b/src/abi/typesystem/generic.ts index 410351f1c..02d1907a9 100644 --- a/src/abi/typesystem/generic.ts +++ b/src/abi/typesystem/generic.ts @@ -1,3 +1,4 @@ +import * as errors from "../../core/errors"; import { guardValueIsSet } from "../../core/utils"; import { CollectionOfTypedValues } from "./collections"; import { NullType, Type, TypedValue, TypePlaceholder } from "./types"; @@ -46,7 +47,11 @@ export class OptionValue extends TypedValue { constructor(type: OptionType, value: TypedValue | null = null) { super(type); - // TODO: assert value is of type type.getFirstTypeParameter() + if (value !== null && !value.getType().equals(type.getFirstTypeParameter())) { + throw new errors.ErrInvariantFailed( + `OptionValue: value type mismatch. Expected: ${type.getFirstTypeParameter().getName()}, got: ${value.getType().getName()}`, + ); + } this.value = value; } @@ -108,7 +113,14 @@ export class List extends TypedValue { constructor(type: ListType, items: TypedValue[]) { super(type); - // TODO: assert items are of type type.getFirstTypeParameter() + const expectedType = type.getFirstTypeParameter(); + for (let i = 0; i < items.length; i++) { + if (!items[i].getType().equals(expectedType)) { + throw new errors.ErrInvariantFailed( + `List: item[${i}] type mismatch. Expected: ${expectedType.getName()}, got: ${items[i].getType().getName()}`, + ); + } + } this.backingCollection = new CollectionOfTypedValues(items); } diff --git a/src/abi/typesystem/variadic.ts b/src/abi/typesystem/variadic.ts index f4f217102..0f4d16149 100644 --- a/src/abi/typesystem/variadic.ts +++ b/src/abi/typesystem/variadic.ts @@ -1,3 +1,4 @@ +import * as errors from "../../core/errors"; import { Type, TypeCardinality, TypedValue, TypePlaceholder } from "./types"; export class VariadicType extends Type { @@ -44,7 +45,14 @@ export class VariadicValue extends TypedValue { constructor(type: VariadicType, items: TypedValue[]) { super(type); - // TODO: assert items are of type type.getFirstTypeParameter() + const expectedType = type.getFirstTypeParameter(); + for (let i = 0; i < items.length; i++) { + if (!items[i].getType().equals(expectedType)) { + throw new errors.ErrInvariantFailed( + `VariadicValue: item[${i}] type mismatch. Expected: ${expectedType.getName()}, got: ${items[i].getType().getName()}`, + ); + } + } this.items = items; } From ec93214d458a3feda8c1e49a781fed15c8ec856e Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Mon, 24 Nov 2025 00:45:43 +0100 Subject: [PATCH 6/8] refactor(typesystem): split generic.ts into separate files Split generic types into dedicated files for better code organization and maintainability while preserving backward compatibility. - Moved OptionType and OptionValue to genericOption.ts - Moved ListType and List to genericList.ts - Updated generic.ts to re-export from new files - All tests pass, full backward compatibility maintained Fixed TODO: Move to a new file, genericOption.ts Fixed TODO: Move to a new file, genericList.ts --- src/abi/typesystem/generic.ts | 165 ++-------------------------- src/abi/typesystem/genericList.ts | 72 ++++++++++++ src/abi/typesystem/genericOption.ts | 83 ++++++++++++++ 3 files changed, 163 insertions(+), 157 deletions(-) create mode 100644 src/abi/typesystem/genericList.ts create mode 100644 src/abi/typesystem/genericOption.ts diff --git a/src/abi/typesystem/generic.ts b/src/abi/typesystem/generic.ts index 02d1907a9..083397a7d 100644 --- a/src/abi/typesystem/generic.ts +++ b/src/abi/typesystem/generic.ts @@ -1,157 +1,8 @@ -import * as errors from "../../core/errors"; -import { guardValueIsSet } from "../../core/utils"; -import { CollectionOfTypedValues } from "./collections"; -import { NullType, Type, TypedValue, TypePlaceholder } from "./types"; - -// TODO: Move to a new file, "genericOption.ts" -export class OptionType extends Type { - static ClassName = "OptionType"; - - constructor(typeParameter: Type) { - super("Option", [typeParameter]); - } - - getClassName(): string { - return OptionType.ClassName; - } - - isAssignableFrom(type: Type): boolean { - if (!type.hasExactClass(OptionType.ClassName)) { - return false; - } - - let invariantTypeParameters = this.getFirstTypeParameter().equals(type.getFirstTypeParameter()); - let fakeCovarianceToNull = type.getFirstTypeParameter().hasExactClass(NullType.ClassName); - return invariantTypeParameters || fakeCovarianceToNull; - } -} - -// TODO: Move to a new file, "genericList.ts" -export class ListType extends Type { - static ClassName = "ListType"; - - constructor(typeParameter: Type) { - super("List", [typeParameter]); - } - - getClassName(): string { - return ListType.ClassName; - } -} - -// TODO: Move to a new file, "genericOption.ts" -export class OptionValue extends TypedValue { - static ClassName = "OptionValue"; - private readonly value: TypedValue | null; - - constructor(type: OptionType, value: TypedValue | null = null) { - super(type); - - if (value !== null && !value.getType().equals(type.getFirstTypeParameter())) { - throw new errors.ErrInvariantFailed( - `OptionValue: value type mismatch. Expected: ${type.getFirstTypeParameter().getName()}, got: ${value.getType().getName()}`, - ); - } - - this.value = value; - } - - getClassName(): string { - return OptionValue.ClassName; - } - - /** - * Creates an OptionValue, as a missing option argument. - */ - static newMissing(): OptionValue { - let type = new OptionType(new NullType()); - return new OptionValue(type); - } - - static newMissingTyped(type: Type): OptionValue { - return new OptionValue(new OptionType(type)); - } - - /** - * Creates an OptionValue, as a provided option argument. - */ - static newProvided(typedValue: TypedValue): OptionValue { - let type = new OptionType(typedValue.getType()); - return new OptionValue(type, typedValue); - } - - isSet(): boolean { - return this.value ? true : false; - } - - getTypedValue(): TypedValue { - guardValueIsSet("value", this.value); - return this.value!; - } - - valueOf(): any { - return this.value ? this.value.valueOf() : null; - } - - equals(other: OptionValue): boolean { - return this.value?.equals(other.value) || false; - } -} - -// TODO: Move to a new file, "genericList.ts" -// TODO: Rename to ListValue, for consistency (though the term is slighly unfortunate). -// Question for review: or not? -export class List extends TypedValue { - static ClassName = "List"; - private readonly backingCollection: CollectionOfTypedValues; - - /** - * - * @param type the type of this TypedValue (an instance of ListType), not the type parameter of the ListType - * @param items the items, having the type type.getFirstTypeParameter() - */ - constructor(type: ListType, items: TypedValue[]) { - super(type); - - const expectedType = type.getFirstTypeParameter(); - for (let i = 0; i < items.length; i++) { - if (!items[i].getType().equals(expectedType)) { - throw new errors.ErrInvariantFailed( - `List: item[${i}] type mismatch. Expected: ${expectedType.getName()}, got: ${items[i].getType().getName()}`, - ); - } - } - - this.backingCollection = new CollectionOfTypedValues(items); - } - - getClassName(): string { - return List.ClassName; - } - - static fromItems(items: TypedValue[]): List { - if (items.length == 0) { - return new List(new TypePlaceholder(), []); - } - - let typeParameter = items[0].getType(); - let listType = new ListType(typeParameter); - return new List(listType, items); - } - - getLength(): number { - return this.backingCollection.getLength(); - } - - getItems(): ReadonlyArray { - return this.backingCollection.getItems(); - } - - valueOf(): any[] { - return this.backingCollection.valueOf(); - } - - equals(other: List): boolean { - return this.backingCollection.equals(other.backingCollection); - } -} +/** + * Re-exports for backward compatibility. + * Generic types have been moved to separate files: + * - OptionType and OptionValue -> genericOption.ts + * - ListType and List -> genericList.ts + */ +export { List, ListType } from "./genericList"; +export { OptionType, OptionValue } from "./genericOption"; diff --git a/src/abi/typesystem/genericList.ts b/src/abi/typesystem/genericList.ts new file mode 100644 index 000000000..5fb832cd9 --- /dev/null +++ b/src/abi/typesystem/genericList.ts @@ -0,0 +1,72 @@ +import * as errors from "../../core/errors"; +import { CollectionOfTypedValues } from "./collections"; +import { Type, TypedValue, TypePlaceholder } from "./types"; + +export class ListType extends Type { + static ClassName = "ListType"; + + constructor(typeParameter: Type) { + super("List", [typeParameter]); + } + + getClassName(): string { + return ListType.ClassName; + } +} + +/** + * A list of typed values. + */ +export class List extends TypedValue { + static ClassName = "List"; + private readonly backingCollection: CollectionOfTypedValues; + + /** + * @param type the type of this TypedValue (an instance of ListType), not the type parameter of the ListType + * @param items the items, having the type type.getFirstTypeParameter() + */ + constructor(type: ListType, items: TypedValue[]) { + super(type); + + const expectedType = type.getFirstTypeParameter(); + for (let i = 0; i < items.length; i++) { + if (!items[i].getType().equals(expectedType)) { + throw new errors.ErrInvariantFailed( + `List: item[${i}] type mismatch. Expected: ${expectedType.getName()}, got: ${items[i].getType().getName()}`, + ); + } + } + + this.backingCollection = new CollectionOfTypedValues(items); + } + + getClassName(): string { + return List.ClassName; + } + + static fromItems(items: TypedValue[]): List { + if (items.length == 0) { + return new List(new TypePlaceholder(), []); + } + + let typeParameter = items[0].getType(); + let listType = new ListType(typeParameter); + return new List(listType, items); + } + + getLength(): number { + return this.backingCollection.getLength(); + } + + getItems(): ReadonlyArray { + return this.backingCollection.getItems(); + } + + valueOf(): any[] { + return this.backingCollection.valueOf(); + } + + equals(other: List): boolean { + return this.backingCollection.equals(other.backingCollection); + } +} diff --git a/src/abi/typesystem/genericOption.ts b/src/abi/typesystem/genericOption.ts new file mode 100644 index 000000000..17ff8ede9 --- /dev/null +++ b/src/abi/typesystem/genericOption.ts @@ -0,0 +1,83 @@ +import * as errors from "../../core/errors"; +import { guardValueIsSet } from "../../core/utils"; +import { NullType, Type, TypedValue } from "./types"; + +export class OptionType extends Type { + static ClassName = "OptionType"; + + constructor(typeParameter: Type) { + super("Option", [typeParameter]); + } + + getClassName(): string { + return OptionType.ClassName; + } + + isAssignableFrom(type: Type): boolean { + if (!type.hasExactClass(OptionType.ClassName)) { + return false; + } + + let invariantTypeParameters = this.getFirstTypeParameter().equals(type.getFirstTypeParameter()); + let fakeCovarianceToNull = type.getFirstTypeParameter().hasExactClass(NullType.ClassName); + return invariantTypeParameters || fakeCovarianceToNull; + } +} + +export class OptionValue extends TypedValue { + static ClassName = "OptionValue"; + private readonly value: TypedValue | null; + + constructor(type: OptionType, value: TypedValue | null = null) { + super(type); + + if (value !== null && !value.getType().equals(type.getFirstTypeParameter())) { + throw new errors.ErrInvariantFailed( + `OptionValue: value type mismatch. Expected: ${type.getFirstTypeParameter().getName()}, got: ${value.getType().getName()}`, + ); + } + + this.value = value; + } + + getClassName(): string { + return OptionValue.ClassName; + } + + /** + * Creates an OptionValue, as a missing option argument. + */ + static newMissing(): OptionValue { + let type = new OptionType(new NullType()); + return new OptionValue(type); + } + + static newMissingTyped(type: Type): OptionValue { + return new OptionValue(new OptionType(type)); + } + + /** + * Creates an OptionValue, as a provided option argument. + */ + static newProvided(typedValue: TypedValue): OptionValue { + let type = new OptionType(typedValue.getType()); + return new OptionValue(type, typedValue); + } + + isSet(): boolean { + return this.value ? true : false; + } + + getTypedValue(): TypedValue { + guardValueIsSet("value", this.value); + return this.value!; + } + + valueOf(): any { + return this.value ? this.value.valueOf() : null; + } + + equals(other: OptionValue): boolean { + return this.value?.equals(other.value) || false; + } +} From 269f66742615b69141ddc02d32c8f00eeaf64260 Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Mon, 24 Nov 2025 01:00:51 +0100 Subject: [PATCH 7/8] fix(codec): correct OptionValue type construction in option codec The OptionValueBinaryCodec.decodeTopLevel() was incorrectly passing the inner type parameter (e.g., U32Type) directly to OptionValue constructor instead of wrapping it in an OptionType. This bug was hidden until type assertions were added in commit 71e165ce. The assertions exposed that OptionValue was being constructed with wrong type, causing runtime errors when accessing type parameters. - Wrap inner type with OptionType before creating OptionValue - Import OptionType in option codec - Ensures type consistency and proper type parameter access Bug discovered by type assertions added in previous refactoring. --- src/abi/codec/option.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/abi/codec/option.ts b/src/abi/codec/option.ts index af1d7c5f7..97f25cf01 100644 --- a/src/abi/codec/option.ts +++ b/src/abi/codec/option.ts @@ -1,5 +1,5 @@ import * as errors from "../../core/errors"; -import { OptionValue, Type } from "../typesystem"; +import { OptionType, OptionValue, Type } from "../typesystem"; import { BinaryCodec } from "./binary"; /** @@ -26,8 +26,10 @@ export class OptionValueBinaryCodec { } decodeTopLevel(buffer: Buffer, type: Type): OptionValue { + const optionType = new OptionType(type); + if (buffer.length == 0) { - return new OptionValue(type); + return new OptionValue(optionType); } if (buffer[0] != 0x01) { @@ -35,7 +37,7 @@ export class OptionValueBinaryCodec { } let [decoded, _decodedLength] = this.binaryCodec.decodeNested(buffer.slice(1), type); - return new OptionValue(type, decoded); + return new OptionValue(optionType, decoded); } encodeNested(optionValue: OptionValue): Buffer { From c6ff74a91a44c6dbb716322bb1b3a85448636cbe Mon Sep 17 00:00:00 2001 From: ReneDuris Date: Mon, 24 Nov 2025 01:02:37 +0100 Subject: [PATCH 8/8] refactor(abi): split ArgSerializer complex recursive functions Extract complex recursive logic from ArgSerializer into dedicated helper classes for better maintainability and testability. This is a non-breaking refactor that keeps all public APIs unchanged. Created helper classes: - BufferReader: Sequential buffer reading with position tracking - BufferWriter: Buffer accumulation for serialization - TypeValueReader: Recursive reading logic for complex types - TypeValueWriter: Recursive writing logic for complex types Simplified ArgSerializer methods: - buffersToValues(): Now uses BufferReader + TypeValueReader - valuesToBuffers(): Now uses BufferWriter + TypeValueWriter Benefits: - Eliminated nested functions and complex closures - Better separation of concerns - Easier to test each component independently - More maintainable code structure - No breaking changes to public API Fixed TODO: Refactor, split (function is quite complex) in buffersToValues Fixed TODO: Refactor, split (function is quite complex) in valuesToBuffers --- src/abi/argSerializer.ts | 146 ++++--------------------------------- src/abi/bufferReader.ts | 61 ++++++++++++++++ src/abi/bufferWriter.ts | 46 ++++++++++++ src/abi/typeValueReader.ts | 86 ++++++++++++++++++++++ src/abi/typeValueWriter.ts | 72 ++++++++++++++++++ 5 files changed, 279 insertions(+), 132 deletions(-) create mode 100644 src/abi/bufferReader.ts create mode 100644 src/abi/bufferWriter.ts create mode 100644 src/abi/typeValueReader.ts create mode 100644 src/abi/typeValueWriter.ts diff --git a/src/abi/argSerializer.ts b/src/abi/argSerializer.ts index 7ede36894..131731287 100644 --- a/src/abi/argSerializer.ts +++ b/src/abi/argSerializer.ts @@ -1,9 +1,10 @@ import { ARGUMENTS_SEPARATOR } from "../core/constants"; import { BinaryCodec } from "./codec"; -import { Type, TypedValue, U32Type, U32Value } from "./typesystem"; -import { OptionalType, OptionalValue } from "./typesystem/algebraic"; -import { CompositeType, CompositeValue } from "./typesystem/composite"; -import { VariadicType, VariadicValue } from "./typesystem/variadic"; +import { Type, TypedValue } from "./typesystem"; +import { BufferReader } from "./bufferReader"; +import { BufferWriter } from "./bufferWriter"; +import { TypeValueReader } from "./typeValueReader"; +import { TypeValueWriter } from "./typeValueWriter"; interface IArgSerializerOptions { codec: ICodec; @@ -53,85 +54,15 @@ export class ArgSerializer { * Decodes a set of buffers into a set of typed values, given parameter definitions. */ buffersToValues(buffers: Buffer[], parameters: IParameterDefinition[]): TypedValue[] { - // TODO: Refactor, split (function is quite complex). - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; + const bufferReader = new BufferReader(buffers, this.codec); + const valueReader = new TypeValueReader(bufferReader); + const values: TypedValue[] = []; - buffers = buffers || []; - - let values: TypedValue[] = []; - let bufferIndex = 0; - let numBuffers = buffers.length; - - for (let i = 0; i < parameters.length; i++) { - let parameter = parameters[i]; - let type = parameter.type; - let value = readValue(type); + for (const parameter of parameters) { + const value = valueReader.readValue(parameter.type); values.push(value); } - // This is a recursive function. - function readValue(type: Type): TypedValue { - if (type.hasExactClass(OptionalType.ClassName)) { - const typedValue = readValue(type.getFirstTypeParameter()); - return new OptionalValue(type, typedValue); - } - - if (type.hasExactClass(VariadicType.ClassName)) { - return readVariadicValue(type); - } - - if (type.hasExactClass(CompositeType.ClassName)) { - const typedValues = []; - - for (const typeParameter of type.getTypeParameters()) { - typedValues.push(readValue(typeParameter)); - } - - return new CompositeValue(type, typedValues); - } - - // Non-composite (singular), non-variadic (fixed) type. - // The only branching without a recursive call. - const typedValue = decodeNextBuffer(type); - - // TODO: Handle the case (maybe throw error) when "typedValue" is, actually, null. - return typedValue!; - } - - function readVariadicValue(type: Type): TypedValue { - const variadicType = type; - const typedValues = []; - - if (variadicType.isCounted) { - const count: number = readValue(new U32Type()).valueOf().toNumber(); - - for (let i = 0; i < count; i++) { - typedValues.push(readValue(type.getFirstTypeParameter())); - } - } else { - while (!hasReachedTheEnd()) { - typedValues.push(readValue(type.getFirstTypeParameter())); - } - } - - return new VariadicValue(variadicType, typedValues); - } - - function decodeNextBuffer(type: Type): TypedValue | null { - if (hasReachedTheEnd()) { - return null; - } - - let buffer = buffers[bufferIndex++]; - let decodedValue = self.codec.decodeTopLevel(buffer, type); - return decodedValue; - } - - function hasReachedTheEnd() { - return bufferIndex >= numBuffers; - } - return values; } @@ -159,62 +90,13 @@ export class ArgSerializer { * Variadic types and composite types might result into none, one or more buffers. */ valuesToBuffers(values: TypedValue[]): Buffer[] { - // TODO: Refactor, split (function is quite complex). - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - - const buffers: Buffer[] = []; + const bufferWriter = new BufferWriter(this.codec); + const valueWriter = new TypeValueWriter(bufferWriter); for (const value of values) { - handleValue(value); - } - - // This is a recursive function. It appends to the "buffers" variable. - function handleValue(value: TypedValue): void { - if (value.hasExactClass(OptionalValue.ClassName)) { - const valueAsOptional = value; - - if (valueAsOptional.isSet()) { - handleValue(valueAsOptional.getTypedValue()); - } - - return; - } - - if (value.hasExactClass(VariadicValue.ClassName)) { - handleVariadicValue(value); - return; - } - - if (value.hasExactClass(CompositeValue.ClassName)) { - const valueAsComposite = value; - - for (const item of valueAsComposite.getItems()) { - handleValue(item); - } - - return; - } - - // Non-composite (singular), non-variadic (fixed) type. - // The only branching without a recursive call. - const buffer: Buffer = self.codec.encodeTopLevel(value); - buffers.push(buffer); - } - - function handleVariadicValue(value: VariadicValue): void { - const variadicType = value.getType(); - - if (variadicType.isCounted) { - const countValue = new U32Value(value.getItems().length); - buffers.push(self.codec.encodeTopLevel(countValue)); - } - - for (const item of value.getItems()) { - handleValue(item); - } + valueWriter.writeValue(value); } - return buffers; + return bufferWriter.getBuffers(); } } diff --git a/src/abi/bufferReader.ts b/src/abi/bufferReader.ts new file mode 100644 index 000000000..cc3cd6526 --- /dev/null +++ b/src/abi/bufferReader.ts @@ -0,0 +1,61 @@ +import { Type, TypedValue } from "./typesystem"; + +/** + * Interface for codec operations. + */ +export interface ICodec { + decodeTopLevel(buffer: Buffer, type: Type): TypedValue; + encodeTopLevel(typedValue: TypedValue): Buffer; +} + +/** + * Helper class for reading typed values from buffers sequentially. + * Maintains an internal pointer to track position in the buffer array. + */ +export class BufferReader { + private readonly buffers: Buffer[]; + private readonly codec: ICodec; + private bufferIndex: number = 0; + + constructor(buffers: Buffer[], codec: ICodec) { + this.buffers = buffers || []; + this.codec = codec; + } + + /** + * Returns true if all buffers have been consumed. + */ + hasReachedEnd(): boolean { + return this.bufferIndex >= this.buffers.length; + } + + /** + * Reads and decodes the next buffer using the provided type. + * Advances the internal pointer after reading. + * + * @param type - The type to use for decoding + * @returns The decoded typed value, or null if no more buffers available + */ + decodeNext(type: Type): TypedValue | null { + if (this.hasReachedEnd()) { + return null; + } + + const buffer = this.buffers[this.bufferIndex++]; + return this.codec.decodeTopLevel(buffer, type); + } + + /** + * Gets the current position in the buffer array. + */ + getPosition(): number { + return this.bufferIndex; + } + + /** + * Gets the total number of buffers. + */ + getLength(): number { + return this.buffers.length; + } +} diff --git a/src/abi/bufferWriter.ts b/src/abi/bufferWriter.ts new file mode 100644 index 000000000..8deb85ed1 --- /dev/null +++ b/src/abi/bufferWriter.ts @@ -0,0 +1,46 @@ +import { TypedValue } from "./typesystem"; + +/** + * Interface for codec operations. + */ +export interface ICodec { + decodeTopLevel(buffer: Buffer, type: import("./typesystem").Type): TypedValue; + encodeTopLevel(typedValue: TypedValue): Buffer; +} + +/** + * Helper class for writing typed values to buffers. + * Accumulates encoded buffers in an internal array. + */ +export class BufferWriter { + private readonly buffers: Buffer[] = []; + private readonly codec: ICodec; + + constructor(codec: ICodec) { + this.codec = codec; + } + + /** + * Encodes and writes a typed value to the buffer list. + * + * @param value - The typed value to encode and write + */ + write(value: TypedValue): void { + const buffer = this.codec.encodeTopLevel(value); + this.buffers.push(buffer); + } + + /** + * Returns all accumulated buffers. + */ + getBuffers(): Buffer[] { + return this.buffers; + } + + /** + * Returns the number of buffers written. + */ + getCount(): number { + return this.buffers.length; + } +} diff --git a/src/abi/typeValueReader.ts b/src/abi/typeValueReader.ts new file mode 100644 index 000000000..2ab5fe600 --- /dev/null +++ b/src/abi/typeValueReader.ts @@ -0,0 +1,86 @@ +import { ErrInvalidArgument } from "../core/errors"; +import { Type, TypedValue, U32Type } from "./typesystem"; +import { CompositeType, CompositeValue } from "./typesystem/composite"; +import { OptionalType, OptionalValue } from "./typesystem/algebraic"; +import { VariadicType, VariadicValue } from "./typesystem/variadic"; +import { BufferReader } from "./bufferReader"; + +/** + * Handles recursive reading of typed values from buffers. + * Supports complex types including Optional, Variadic, and Composite types. + */ +export class TypeValueReader { + private readonly bufferReader: BufferReader; + + constructor(bufferReader: BufferReader) { + this.bufferReader = bufferReader; + } + + /** + * Reads a typed value based on its type. + * Handles recursive types (Optional, Variadic, Composite). + * + * @param type - The type to read + * @returns The decoded typed value + */ + readValue(type: Type): TypedValue { + if (type.hasExactClass(OptionalType.ClassName)) { + return this.readOptionalValue(type); + } + + if (type.hasExactClass(VariadicType.ClassName)) { + return this.readVariadicValue(type); + } + + if (type.hasExactClass(CompositeType.ClassName)) { + return this.readCompositeValue(type); + } + + // Non-composite, non-variadic type + return this.readSingleValue(type); + } + + private readOptionalValue(type: Type): OptionalValue { + const typedValue = this.readValue(type.getFirstTypeParameter()); + return new OptionalValue(type, typedValue); + } + + private readVariadicValue(type: VariadicType): VariadicValue { + const typedValues: TypedValue[] = []; + + if (type.isCounted) { + const count = this.readValue(new U32Type()).valueOf().toNumber(); + for (let i = 0; i < count; i++) { + typedValues.push(this.readValue(type.getFirstTypeParameter())); + } + } else { + while (!this.bufferReader.hasReachedEnd()) { + typedValues.push(this.readValue(type.getFirstTypeParameter())); + } + } + + return new VariadicValue(type, typedValues); + } + + private readCompositeValue(type: Type): CompositeValue { + const typedValues: TypedValue[] = []; + + for (const typeParameter of type.getTypeParameters()) { + typedValues.push(this.readValue(typeParameter)); + } + + return new CompositeValue(type, typedValues); + } + + private readSingleValue(type: Type): TypedValue { + const typedValue = this.bufferReader.decodeNext(type); + + if (typedValue === null) { + throw new ErrInvalidArgument( + `Failed to decode value of type ${type.getName()}. Buffer may be incomplete or invalid.`, + ); + } + + return typedValue; + } +} diff --git a/src/abi/typeValueWriter.ts b/src/abi/typeValueWriter.ts new file mode 100644 index 000000000..fa1d0ef84 --- /dev/null +++ b/src/abi/typeValueWriter.ts @@ -0,0 +1,72 @@ +import { TypedValue, U32Value } from "./typesystem"; +import { CompositeValue } from "./typesystem/composite"; +import { OptionalValue } from "./typesystem/algebraic"; +import { VariadicType, VariadicValue } from "./typesystem/variadic"; +import { BufferWriter } from "./bufferWriter"; + +/** + * Handles recursive writing of typed values to buffers. + * Supports complex types including Optional, Variadic, and Composite types. + */ +export class TypeValueWriter { + private readonly bufferWriter: BufferWriter; + + constructor(bufferWriter: BufferWriter) { + this.bufferWriter = bufferWriter; + } + + /** + * Writes a typed value to buffers. + * Handles recursive types (Optional, Variadic, Composite). + * + * @param value - The typed value to write + */ + writeValue(value: TypedValue): void { + if (value.hasExactClass(OptionalValue.ClassName)) { + this.writeOptionalValue(value); + return; + } + + if (value.hasExactClass(VariadicValue.ClassName)) { + this.writeVariadicValue(value); + return; + } + + if (value.hasExactClass(CompositeValue.ClassName)) { + this.writeCompositeValue(value); + return; + } + + // Non-composite, non-variadic type + this.writeSingleValue(value); + } + + private writeOptionalValue(value: OptionalValue): void { + if (value.isSet()) { + this.writeValue(value.getTypedValue()); + } + } + + private writeVariadicValue(value: VariadicValue): void { + const variadicType = value.getType(); + + if (variadicType.isCounted) { + const countValue = new U32Value(value.getItems().length); + this.bufferWriter.write(countValue); + } + + for (const item of value.getItems()) { + this.writeValue(item); + } + } + + private writeCompositeValue(value: CompositeValue): void { + for (const item of value.getItems()) { + this.writeValue(item); + } + } + + private writeSingleValue(value: TypedValue): void { + this.bufferWriter.write(value); + } +}