Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-breaking change: add support for "counted-variadic" #323

Merged
merged 10 commits into from
Aug 31, 2023
24 changes: 20 additions & 4 deletions src/smartcontracts/argSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ARGUMENTS_SEPARATOR } from "../constants";
import { BinaryCodec } from "./codec";
import { EndpointParameterDefinition, Type, TypedValue } from "./typesystem";
import { EndpointParameterDefinition, Type, TypedValue, U32Type, U32Value } from "./typesystem";
import { OptionalType, OptionalValue } from "./typesystem/algebraic";
import { CompositeType, CompositeValue } from "./typesystem/composite";
import { VariadicType, VariadicValue } from "./typesystem/variadic";
Expand Down Expand Up @@ -74,13 +74,22 @@ export class ArgSerializer {
let typedValue = readValue(type.getFirstTypeParameter());
return new OptionalValue(type, typedValue);
} else if (type.hasExactClass(VariadicType.ClassName)) {
let variadicType = <VariadicType>type;
let typedValues = [];

while (!hasReachedTheEnd()) {
typedValues.push(readValue(type.getFirstTypeParameter()));
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()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-counted variadic arguments (regular variadic arguments) can only be at the tail of the arguments.

typedValues.push(readValue(type.getFirstTypeParameter()));
}
}

return new VariadicValue(type, typedValues);
return new VariadicValue(variadicType, typedValues);
} else if (type.hasExactClass(CompositeType.ClassName)) {
let typedValues = [];

Expand Down Expand Up @@ -158,6 +167,13 @@ export class ArgSerializer {
}
} else if (value.hasExactClass(VariadicValue.ClassName)) {
let valueAsVariadic = <VariadicValue>value;
let variadicType = <VariadicType>valueAsVariadic.getType();

if (variadicType.isCounted) {
const countValue = new U32Value(valueAsVariadic.getItems().length);
buffers.push(self.codec.encodeTopLevel(countValue));
Copy link
Contributor Author

@andreibancioiu andreibancioiu Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@count@var1@var2@var3.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the variadic arg handling can be pulled into a function so we have less complexity in the if/else branches.

Q: do you like the else if {} approach better? Since every branch returns, we could also have less indenting doing:

if (condition) {
  ...
  return result
}

if (condition2) {
  ...
  return result2;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, fixed. Extracted handling into separate (though "nested") functions. Also re-organized branches for less indenting 👍

}

for (const item of valueAsVariadic.getItems()) {
handleValue(item);
}
Expand Down
90 changes: 86 additions & 4 deletions src/smartcontracts/nativeSerializer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import BigNumber from "bignumber.js";
import { assert } from "chai";
import { Address } from "../address";
import { ErrInvalidArgument } from "../errors";
import { NativeSerializer } from "./nativeSerializer";
import { AbiRegistry, AddressType, AddressValue, BigUIntType, BooleanType, BooleanValue, CompositeType, CompositeValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, EnumType, ListType, NullType, OptionalType, OptionalValue, OptionType, OptionValue, TupleType, U32Type, U64Type, U64Value, U8Type, U8Value, VariadicType, VariadicValue } from "./typesystem";
import { AbiRegistry, AddressType, AddressValue, BigUIntType, BooleanType, BooleanValue, CompositeType, CompositeValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, ListType, NullType, OptionalType, OptionalValue, OptionType, OptionValue, TupleType, TypePlaceholder, U32Type, U32Value, U64Type, U64Value, U8Type, U8Value, VariadicType, VariadicValue } from "./typesystem";
import { BytesType, BytesValue } from "./typesystem/bytes";

describe("test native serializer", () => {
Expand Down Expand Up @@ -45,6 +46,87 @@ describe("test native serializer", () => {
assert.deepEqual(typedValues[6].valueOf(), null);
});

it("should perform type inference (variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const inputParameters = [new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), false))];
const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers);
const typedValues = NativeSerializer.nativeToTypedValues([8, 9, 10], endpoint);

assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), false));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
});

it("should perform type inference (counted-variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const inputParameters = [
new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), true)),
new EndpointParameterDefinition("", "", new VariadicType(new BytesType(), true))
];
const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers);

// Implicit counted-variadic (not supported).
assert.throws(() => NativeSerializer.nativeToTypedValues([8, 9, 10, "a", "b", "c"], endpoint), ErrInvalidArgument);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot be supported in the current implementation of the NativeSerializer, due to args-packing ambiguity.


// Explicit, non-empty counted-variadic.
let typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(new U32Value(8), new U32Value(9), new U32Value(10)),
VariadicValue.fromItemsCounted(BytesValue.fromUTF8("a"), BytesValue.fromUTF8("b"), BytesValue.fromUTF8("c"))
Comment on lines +72 to +73
Copy link
Contributor Author

@andreibancioiu andreibancioiu Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to have multiple counted variadic arguments (users are required to pass them in the explicitly typed form).

], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), true));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType(), true));
assert.deepEqual(typedValues[1].valueOf(), [Buffer.from("a"), Buffer.from("b"), Buffer.from("c")]);

// Explicit, empty counted-variadic.
typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(),
VariadicValue.fromItemsCounted()
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new TypePlaceholder(), true));
assert.deepEqual(typedValues[0].valueOf(), []);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new TypePlaceholder(), true));
assert.deepEqual(typedValues[1].valueOf(), []);
});

it("should perform type inference (counted-variadic and regular variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const inputParameters = [
new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), true)),
new EndpointParameterDefinition("", "", new VariadicType(new BytesType(), false))
];
const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers);

// Implicit counted-variadic (not supported).
assert.throws(() => NativeSerializer.nativeToTypedValues([8, 9, 10], endpoint), ErrInvalidArgument);

// Explicit counted-variadic, empty implicit regular variadic.
let typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(new U32Value(8), new U32Value(9), new U32Value(10))
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), true));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType(), false));
assert.deepEqual(typedValues[1].valueOf(), []);

// Explicit counted-variadic, non-empty implicit regular variadic.
typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(new U32Value(8), new U32Value(9), new U32Value(10)),
"a", "b", "c"
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), true));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType(), false));
assert.deepEqual(typedValues[1].valueOf(), [Buffer.from("a"), Buffer.from("b"), Buffer.from("c")]);
});

it("should should handle optionals in a strict manner (but it does not)", async () => {
const endpoint = AbiRegistry.create({
"endpoints": [
Expand Down Expand Up @@ -279,7 +361,7 @@ describe("test native serializer", () => {
assert.deepEqual(typedValues[0].valueOf(), new BigNumber(42));
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType()));
assert.deepEqual(typedValues[1].valueOf(), []);
});
});

it("should perform type inference (enums)", async () => {
const abiRegistry = AbiRegistry.create({
Expand Down Expand Up @@ -354,8 +436,8 @@ describe("test native serializer", () => {
assert.deepEqual(typedValues[1].getType(), enumType);
assert.deepEqual(typedValues[1].valueOf(), { name: "Nothing", fields: [] });
assert.deepEqual(typedValues[2].getType(), enumType);
assert.deepEqual(typedValues[2].valueOf(), { name: 'Something', fields: [ new Address('erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha') ] });
assert.deepEqual(typedValues[2].valueOf(), { name: 'Something', fields: [new Address('erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha')] });
assert.deepEqual(typedValues[3].getType(), enumType);
assert.deepEqual(typedValues[3].valueOf(), { name: 'Else', fields: [ new BigNumber(42), new BigNumber(43) ] });
assert.deepEqual(typedValues[3].valueOf(), { name: 'Else', fields: [new BigNumber(42), new BigNumber(43)] });
});
});
40 changes: 33 additions & 7 deletions src/smartcontracts/nativeSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ export namespace NativeSerializer {
*/
export function nativeToTypedValues(args: any[], endpoint: EndpointDefinition): TypedValue[] {
args = args || [];
args = handleVariadicArgsAndRePack(args, endpoint);

checkArgumentsCardinality(args, endpoint);

if (hasNonCountedVariadicParameter(endpoint)) {
args = repackNonCountedVariadicParameters(args, endpoint);
} else {
// Repacking makes sense (it's possible) only for regular, non-counted variadic parameters.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No repacking: there are no regular (non-counted) variadic parameters.

}

let parameters = endpoint.input;
let values: TypedValue[] = [];
Expand All @@ -33,16 +40,31 @@ export namespace NativeSerializer {
return values;
}

function handleVariadicArgsAndRePack(args: any[], endpoint: EndpointDefinition) {
let parameters = endpoint.input;

let { min, max, variadic } = getArgumentsCardinality(parameters);
function checkArgumentsCardinality(args: any[], endpoint: EndpointDefinition) {
// With respect to the notes of "repackNonCountedVariadicParameters", "getArgumentsCardinality" will not be needed anymore.
// Currently, it is used only for a arguments count check, which will become redundant.
const { min, max } = getArgumentsCardinality(endpoint.input);

if (!(min <= args.length && args.length <= max)) {
throw new ErrInvalidArgument(`Wrong number of arguments for endpoint ${endpoint.name}: expected between ${min} and ${max} arguments, have ${args.length}`);
}
}

if (variadic) {
function hasNonCountedVariadicParameter(endpoint: EndpointDefinition): boolean {
const lastParameter = endpoint.input[endpoint.input.length - 1];
return lastParameter?.type instanceof VariadicType && !lastParameter.type.isCounted;
}

// In a future version of the type inference system, re-packing logic will be removed.
// The client code will be responsible for passing the correctly packed arguments (variadic arguments explicitly packed as arrays).
// For developers, calling `foo(["erd1", 42, [1, 2, 3]])` will be less ambiguous than `foo(["erd1", 42, 1, 2, 3])`.
// Furthermore, multiple counted-variadic arguments cannot be expressed in the current variant.
// E.g. now, it's unreasonable to decide that `foo([1, 2, 3, "a", "b", "c"])` calls `foo(counted-variadic<int>, counted-variadic<string>)`.
function repackNonCountedVariadicParameters(args: any[], endpoint: EndpointDefinition) {
let parameters = endpoint.input;

// TODO: Remove after first review. Temporarily left this way to make the review easier.
Copy link
Contributor Author

@andreibancioiu andreibancioiu Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Remove if (true) after first review! ⚠️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

if (true) {
const lastEndpointParamIndex = parameters.length - 1;
const argAtIndex = args[lastEndpointParamIndex];

Expand Down Expand Up @@ -135,7 +157,11 @@ export namespace NativeSerializer {
return new OptionalValue(type, converted);
}

function toVariadicValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue {
function toVariadicValue(native: any, type: VariadicType, errorContext: ArgumentErrorContext): TypedValue {
if (type.isCounted) {
throw new ErrInvalidArgument(`Counted variadic arguments must be explicitly typed. E.g. use "VariadicValue.fromItemsCounted()"`);
}

if (native == null) {
native = [];
}
Expand Down
35 changes: 33 additions & 2 deletions src/smartcontracts/resultsParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import * as fs from "fs";
import path from "path";
import { Address } from "../address";
import { ITransactionOnNetwork } from "../interfaceOfNetwork";
import { Logger, LogLevel } from "../logger";
import { LogLevel, Logger } from "../logger";
import { ArgSerializer } from "./argSerializer";
import { ResultsParser } from "./resultsParser";
import { ReturnCode } from "./returnCode";
import { BigUIntType, BigUIntValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, TypedValue, U64Type, U64Value } from "./typesystem";
import { BigUIntType, BigUIntValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, TypedValue, U32Type, U32Value, U64Type, U64Value, VariadicType, VariadicValue } from "./typesystem";
import { BytesType, BytesValue } from "./typesystem/bytes";

const KnownReturnCodes: string[] = [
Expand Down Expand Up @@ -97,6 +97,37 @@ describe("test smart contract results parser", () => {
assert.lengthOf(bundle.values, 2);
});

it("should parse query response (variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const outputParameters = [new EndpointParameterDefinition("a", "a", new VariadicType(new U32Type(), false))];
const endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers);
const queryResponse = new ContractQueryResponse({
returnData: [
Buffer.from([42]).toString("base64"),
Buffer.from([43]).toString("base64"),
]
});

const bundle = parser.parseQueryResponse(queryResponse, endpoint);
assert.deepEqual(bundle.values[0], VariadicValue.fromItems(new U32Value(42), new U32Value(43)));
});

it("should parse query response (counted-variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const outputParameters = [new EndpointParameterDefinition("a", "a", new VariadicType(new U32Type(), true))];
const endpoint = new EndpointDefinition("foo", [], outputParameters, endpointModifiers);
const queryResponse = new ContractQueryResponse({
returnData: [
Buffer.from([2]).toString("base64"),
Buffer.from([42]).toString("base64"),
Buffer.from([43]).toString("base64"),
]
});

const bundle = parser.parseQueryResponse(queryResponse, endpoint);
assert.deepEqual(bundle.values[0], VariadicValue.fromItemsCounted(new U32Value(42), new U32Value(43)));
});

it("should parse contract outcome", async () => {
let endpointModifiers = new EndpointModifiers("", []);
let outputParameters = [
Expand Down
12 changes: 12 additions & 0 deletions src/smartcontracts/typesystem/abiRegistry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ArrayVecType } from "./genericArray";
import { BigUIntType, I64Type, U32Type, U64Type } from "./numerical";
import { StructType } from "./struct";
import { TokenIdentifierType } from "./tokenIdentifier";
import { VariadicType } from "./variadic";

describe("test abi registry", () => {
it("load should also remap known to types", async () => {
Expand Down Expand Up @@ -128,4 +129,15 @@ describe("test abi registry", () => {
assert.lengthOf(registry.customTypes, 12);
assert.deepEqual(registry.getStruct("AuctionItem").getNamesOfDependencies(), ["u64", "Address", "BigUint", "Option", "NftData", "bytes", "TokenIdentifier", "List"]);
});

it("should load ABI with counted-variadic", async () => {
const registry = await loadAbiRegistry("src/testdata/counted-variadic.abi.json");
const dummyType = registry.getStruct("Dummy");

assert.deepEqual(registry.getEndpoint("foo").input[0].type, new VariadicType(dummyType, true));
assert.deepEqual(registry.getEndpoint("bar").input[0].type, new VariadicType(new U32Type(), true));
assert.deepEqual(registry.getEndpoint("bar").input[1].type, new VariadicType(new BytesType(), true));
assert.deepEqual(registry.getEndpoint("bar").output[0].type, new VariadicType(new U32Type(), true));
assert.deepEqual(registry.getEndpoint("bar").output[1].type, new VariadicType(new BytesType(), true));
});
});
Loading
Loading