diff --git a/src/smartcontracts/nativeSerializer.spec.ts b/src/smartcontracts/nativeSerializer.spec.ts index e8a19e4f..61933ff4 100644 --- a/src/smartcontracts/nativeSerializer.spec.ts +++ b/src/smartcontracts/nativeSerializer.spec.ts @@ -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, 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", () => { @@ -57,12 +58,73 @@ describe("test native serializer", () => { it("should perform type inference (counted-variadic arguments)", async () => { const endpointModifiers = new EndpointModifiers("", []); - const inputParameters = [new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), true))]; + const inputParameters = [ + new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), true)), + new EndpointParameterDefinition("", "", new VariadicType(new BytesType(), true)) + ]; const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers); - const typedValues = NativeSerializer.nativeToTypedValues([8, 9, 10], endpoint); + // Implicit counted-variadic (not supported). + assert.throws(() => NativeSerializer.nativeToTypedValues([8, 9, 10, "a", "b", "c"], endpoint), ErrInvalidArgument); + + // 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")) + ], 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 () => { diff --git a/src/smartcontracts/nativeSerializer.ts b/src/smartcontracts/nativeSerializer.ts index dfeef544..7b214dbf 100644 --- a/src/smartcontracts/nativeSerializer.ts +++ b/src/smartcontracts/nativeSerializer.ts @@ -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. + } let parameters = endpoint.input; let values: TypedValue[] = []; @@ -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, counted-variadic)`. + function repackNonCountedVariadicParameters(args: any[], endpoint: EndpointDefinition) { + let parameters = endpoint.input; + + // TODO: Remove after first review. Temporarily left this way to make the review easier. + if (true) { const lastEndpointParamIndex = parameters.length - 1; const argAtIndex = args[lastEndpointParamIndex]; @@ -136,6 +158,10 @@ export namespace NativeSerializer { } 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 = []; }