diff --git a/src/abi/argSerializer.ts b/src/abi/argSerializer.ts index 7ede3689..13173128 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 00000000..cc3cd652 --- /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 00000000..8deb85ed --- /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/codec/option.ts b/src/abi/codec/option.ts index af1d7c5f..97f25cf0 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 { diff --git a/src/abi/nativeSerializer.spec.ts b/src/abi/nativeSerializer.spec.ts index 548aa877..1dfe2370 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/nativeSerializer.ts b/src/abi/nativeSerializer.ts index 0e0c4542..9e9d5f33 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/typeValueReader.ts b/src/abi/typeValueReader.ts new file mode 100644 index 00000000..2ab5fe60 --- /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 00000000..fa1d0ef8 --- /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); + } +} diff --git a/src/abi/typesystem/address.ts b/src/abi/typesystem/address.ts index c7e0e4b1..ad44d695 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/algebraic.ts b/src/abi/typesystem/algebraic.ts index 249ba500..4a86cff8 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/bytes.ts b/src/abi/typesystem/bytes.ts index 866039ad..ecaf05a7 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/composite.ts b/src/abi/typesystem/composite.ts index 27ed7a88..3b4dace1 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 410351f1..083397a7 100644 --- a/src/abi/typesystem/generic.ts +++ b/src/abi/typesystem/generic.ts @@ -1,145 +1,8 @@ -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); - - // TODO: assert value is of type type.getFirstTypeParameter() - - 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); - - // TODO: assert items are of type type.getFirstTypeParameter() - - 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 00000000..5fb832cd --- /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 00000000..17ff8ede --- /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; + } +} diff --git a/src/abi/typesystem/numerical.ts b/src/abi/typesystem/numerical.ts index de2a97df..6ff1ab2e 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 901c897b..6d429923 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; } diff --git a/src/abi/typesystem/tuple.spec.ts b/src/abi/typesystem/tuple.spec.ts new file mode 100644 index 00000000..3d6704dd --- /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 e52fe3c0..ced9c58f 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/abi/typesystem/typeExpressionParser.ts b/src/abi/typesystem/typeExpressionParser.ts index 076f44a0..2b237212 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 1eff9a9f..125935a5 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 84a0de57..f9e7d21a 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()) { diff --git a/src/abi/typesystem/variadic.ts b/src/abi/typesystem/variadic.ts index f4f21710..0f4d1614 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; } diff --git a/src/core/errors.ts b/src/core/errors.ts index 69954e87..bd880a2b 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. */ diff --git a/src/core/utils.ts b/src/core/utils.ts index c80c7c6d..b07210d8 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 || [];