Skip to content
146 changes: 14 additions & 132 deletions src/abi/argSerializer.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 = <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;
}

Expand Down Expand Up @@ -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 = <OptionalValue>value;

if (valueAsOptional.isSet()) {
handleValue(valueAsOptional.getTypedValue());
}

return;
}

if (value.hasExactClass(VariadicValue.ClassName)) {
handleVariadicValue(<VariadicValue>value);
return;
}

if (value.hasExactClass(CompositeValue.ClassName)) {
const valueAsComposite = <CompositeValue>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 = <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();
}
}
61 changes: 61 additions & 0 deletions src/abi/bufferReader.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
46 changes: 46 additions & 0 deletions src/abi/bufferWriter.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
8 changes: 5 additions & 3 deletions src/abi/codec/option.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -26,16 +26,18 @@ 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) {
throw new errors.ErrCodec("invalid buffer for optional value");
}

let [decoded, _decodedLength] = this.binaryCodec.decodeNested(buffer.slice(1), type);
return new OptionValue(type, decoded);
return new OptionValue(optionType, decoded);
}

encodeNested(optionValue: OptionValue): Buffer {
Expand Down
14 changes: 7 additions & 7 deletions src/abi/nativeSerializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand All @@ -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 () => {
Expand Down
Loading
Loading