From 0da455a1664d8755c6e3bf71279b5dc7dccc08b9 Mon Sep 17 00:00:00 2001 From: Daniel Bate Date: Fri, 17 May 2024 08:09:06 +0100 Subject: [PATCH] feat!: remove nested option size validation (#2322) --- .changeset/olive-meals-love.md | 5 + .../src/encoding/coders/ArrayCoder.ts | 7 +- .../src/encoding/coders/EnumCoder.ts | 7 +- .../src/encoding/coders/StructCoder.ts | 7 +- .../src/encoding/coders/TupleCoder.ts | 7 +- .../abi-coder/src/encoding/coders/VecCoder.ts | 11 +- packages/fuel-gauge/src/options.test.ts | 151 ++++++++++++++++++ .../test/fixtures/forc-projects/Forc.toml | 1 + .../fixtures/forc-projects/options/Forc.toml | 7 + .../forc-projects/options/src/main.sw | 46 ++++++ 10 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 .changeset/olive-meals-love.md create mode 100644 packages/fuel-gauge/src/options.test.ts create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/options/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/options/src/main.sw diff --git a/.changeset/olive-meals-love.md b/.changeset/olive-meals-love.md new file mode 100644 index 00000000000..ef4d695c944 --- /dev/null +++ b/.changeset/olive-meals-love.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/abi-coder": minor +--- + +feat!: remove nested option size validation diff --git a/packages/abi-coder/src/encoding/coders/ArrayCoder.ts b/packages/abi-coder/src/encoding/coders/ArrayCoder.ts index 4927701f920..228073846f9 100644 --- a/packages/abi-coder/src/encoding/coders/ArrayCoder.ts +++ b/packages/abi-coder/src/encoding/coders/ArrayCoder.ts @@ -1,7 +1,7 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { concat } from '@fuel-ts/utils'; -import { MAX_BYTES } from '../../utils/constants'; +import { MAX_BYTES, OPTION_CODER_TYPE } from '../../utils/constants'; import type { TypesOfCoder } from './AbstractCoder'; import { Coder } from './AbstractCoder'; @@ -15,11 +15,14 @@ export class ArrayCoder extends Coder< > { coder: TCoder; length: number; + #hasNestedOption: boolean; constructor(coder: TCoder, length: number) { + const hasNestedOption = coder.type === OPTION_CODER_TYPE; super('array', `[${coder.type}; ${length}]`, length * coder.encodedLength); this.coder = coder; this.length = length; + this.#hasNestedOption = hasNestedOption; } encode(value: InputValueOf): Uint8Array { @@ -35,7 +38,7 @@ export class ArrayCoder extends Coder< } decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { - if (data.length < this.encodedLength || data.length > MAX_BYTES) { + if ((!this.#hasNestedOption && data.length < this.encodedLength) || data.length > MAX_BYTES) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid array data size.`); } diff --git a/packages/abi-coder/src/encoding/coders/EnumCoder.ts b/packages/abi-coder/src/encoding/coders/EnumCoder.ts index 159e19d3411..1a67095b079 100644 --- a/packages/abi-coder/src/encoding/coders/EnumCoder.ts +++ b/packages/abi-coder/src/encoding/coders/EnumCoder.ts @@ -3,7 +3,7 @@ import { toNumber } from '@fuel-ts/math'; import { concat } from '@fuel-ts/utils'; import type { RequireExactlyOne } from 'type-fest'; -import { WORD_SIZE } from '../../utils/constants'; +import { OPTION_CODER_TYPE, WORD_SIZE } from '../../utils/constants'; import type { TypesOfCoder } from './AbstractCoder'; import { Coder } from './AbstractCoder'; @@ -30,8 +30,10 @@ export class EnumCoder> extends Coder< coders: TCoders; #caseIndexCoder: BigNumberCoder; #encodedValueSize: number; + #hasNestedOption: boolean; constructor(name: string, coders: TCoders) { + const hasNestedOption = Object.values(coders).some((coder) => coder.type === OPTION_CODER_TYPE); const caseIndexCoder = new BigNumberCoder('u64'); const encodedValueSize = Object.values(coders).reduce( (max, coder) => Math.max(max, coder.encodedLength), @@ -42,6 +44,7 @@ export class EnumCoder> extends Coder< this.coders = coders; this.#caseIndexCoder = caseIndexCoder; this.#encodedValueSize = encodedValueSize; + this.#hasNestedOption = hasNestedOption; } #encodeNativeEnum(value: string): Uint8Array { @@ -77,7 +80,7 @@ export class EnumCoder> extends Coder< } decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { - if (data.length < this.#encodedValueSize) { + if (!this.#hasNestedOption && data.length < this.#encodedValueSize) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid enum data size.`); } diff --git a/packages/abi-coder/src/encoding/coders/StructCoder.ts b/packages/abi-coder/src/encoding/coders/StructCoder.ts index 54bea7fa211..878fabc8f34 100644 --- a/packages/abi-coder/src/encoding/coders/StructCoder.ts +++ b/packages/abi-coder/src/encoding/coders/StructCoder.ts @@ -1,6 +1,8 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { concatBytes } from '@fuel-ts/utils'; +import { OPTION_CODER_TYPE } from '../../utils/constants'; + import type { TypesOfCoder } from './AbstractCoder'; import { Coder } from './AbstractCoder'; import { OptionCoder } from './OptionCoder'; @@ -18,8 +20,10 @@ export class StructCoder> extends Coder< > { name: string; coders: TCoders; + #hasNestedOption: boolean; constructor(name: string, coders: TCoders) { + const hasNestedOption = Object.values(coders).some((coder) => coder.type === OPTION_CODER_TYPE); const encodedLength = Object.values(coders).reduce( (acc, coder) => acc + coder.encodedLength, 0 @@ -27,6 +31,7 @@ export class StructCoder> extends Coder< super('struct', `struct ${name}`, encodedLength); this.name = name; this.coders = coders; + this.#hasNestedOption = hasNestedOption; } encode(value: InputValueOf): Uint8Array { @@ -48,7 +53,7 @@ export class StructCoder> extends Coder< } decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { - if (data.length < this.encodedLength) { + if (!this.#hasNestedOption && data.length < this.encodedLength) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid struct data size.`); } diff --git a/packages/abi-coder/src/encoding/coders/TupleCoder.ts b/packages/abi-coder/src/encoding/coders/TupleCoder.ts index 39186a0f945..92bf81a1d81 100644 --- a/packages/abi-coder/src/encoding/coders/TupleCoder.ts +++ b/packages/abi-coder/src/encoding/coders/TupleCoder.ts @@ -1,6 +1,8 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { concatBytes } from '@fuel-ts/utils'; +import { OPTION_CODER_TYPE } from '../../utils/constants'; + import type { TypesOfCoder } from './AbstractCoder'; import { Coder } from './AbstractCoder'; @@ -16,11 +18,14 @@ export class TupleCoder extends Coder< DecodedValueOf > { coders: TCoders; + #hasNestedOption: boolean; constructor(coders: TCoders) { + const hasNestedOption = coders.some((coder) => coder.type === OPTION_CODER_TYPE); const encodedLength = coders.reduce((acc, coder) => acc + coder.encodedLength, 0); super('tuple', `(${coders.map((coder) => coder.type).join(', ')})`, encodedLength); this.coders = coders; + this.#hasNestedOption = hasNestedOption; } encode(value: InputValueOf): Uint8Array { @@ -32,7 +37,7 @@ export class TupleCoder extends Coder< } decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { - if (data.length < this.encodedLength) { + if (!this.#hasNestedOption && data.length < this.encodedLength) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid tuple data size.`); } diff --git a/packages/abi-coder/src/encoding/coders/VecCoder.ts b/packages/abi-coder/src/encoding/coders/VecCoder.ts index f09cc313c88..6fd127caa97 100644 --- a/packages/abi-coder/src/encoding/coders/VecCoder.ts +++ b/packages/abi-coder/src/encoding/coders/VecCoder.ts @@ -2,13 +2,12 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import { bn } from '@fuel-ts/math'; import { concatBytes } from '@fuel-ts/utils'; -import { MAX_BYTES, WORD_SIZE } from '../../utils/constants'; +import { MAX_BYTES, OPTION_CODER_TYPE, WORD_SIZE } from '../../utils/constants'; import { isUint8Array } from '../../utils/utilities'; import { Coder } from './AbstractCoder'; import type { TypesOfCoder } from './AbstractCoder'; import { BigNumberCoder } from './BigNumberCoder'; -import { OptionCoder } from './OptionCoder'; type InputValueOf = Array['Input']> | Uint8Array; type DecodedValueOf = Array['Decoded']>; @@ -18,12 +17,12 @@ export class VecCoder extends Coder< DecodedValueOf > { coder: TCoder; - #isOptionVec: boolean; + #hasNestedOption: boolean; constructor(coder: TCoder) { super('struct', `struct Vec`, coder.encodedLength + WORD_SIZE); this.coder = coder; - this.#isOptionVec = this.coder instanceof OptionCoder; + this.#hasNestedOption = coder.type === OPTION_CODER_TYPE; } encode(value: InputValueOf): Uint8Array { @@ -47,7 +46,7 @@ export class VecCoder extends Coder< } decode(data: Uint8Array, offset: number): [DecodedValueOf, number] { - if (!this.#isOptionVec && (data.length < this.encodedLength || data.length > MAX_BYTES)) { + if ((!this.#hasNestedOption && data.length < this.encodedLength) || data.length > MAX_BYTES) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid vec data size.`); } @@ -57,7 +56,7 @@ export class VecCoder extends Coder< const dataLength = length * this.coder.encodedLength; const dataBytes = data.slice(offsetAndLength, offsetAndLength + dataLength); - if (!this.#isOptionVec && dataBytes.length !== dataLength) { + if (!this.#hasNestedOption && dataBytes.length !== dataLength) { throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid vec byte data size.`); } diff --git a/packages/fuel-gauge/src/options.test.ts b/packages/fuel-gauge/src/options.test.ts new file mode 100644 index 00000000000..71210e8d659 --- /dev/null +++ b/packages/fuel-gauge/src/options.test.ts @@ -0,0 +1,151 @@ +import type { Contract } from 'fuels'; + +import { getSetupContract } from './utils'; + +const U8_MAX = 255; +const U16_MAX = 65535; +const U32_MAX = 4294967295; + +const setupContract = getSetupContract('options'); +let contractInstance: Contract; +beforeAll(async () => { + contractInstance = await setupContract(); +}); + +/** + * @group node + */ +describe('Options Tests', () => { + it('echos u8 option', async () => { + const someInput = U8_MAX; + const noneInput = undefined; + + const { value: someValue } = await contractInstance.functions.echo_option(someInput).call(); + + expect(someValue).toBe(someInput); + + const { value: noneValue } = await contractInstance.functions.echo_option(noneInput).call(); + + expect(noneValue).toBe(noneInput); + }); + + it('echos struct enum option', async () => { + const someInput = { + one: { + a: U8_MAX, + }, + two: U32_MAX, + }; + + const { value: someValue } = await contractInstance.functions + .echo_struct_enum_option(someInput) + .call(); + + expect(someValue).toStrictEqual(someInput); + + const noneInput = { + one: { + a: undefined, + }, + two: undefined, + }; + + const { value: noneValue } = await contractInstance.functions + .echo_struct_enum_option(noneInput) + .call(); + + expect(noneValue).toStrictEqual(noneInput); + }); + + it('echos vec option', async () => { + const someInput = [U8_MAX, U16_MAX, U32_MAX]; + + const { value: someValue } = await contractInstance.functions.echo_vec_option(someInput).call(); + + expect(someValue).toStrictEqual(someInput); + + const noneInput = [undefined, undefined, undefined]; + + const { value: noneValue } = await contractInstance.functions.echo_vec_option(noneInput).call(); + + expect(noneValue).toStrictEqual(noneInput); + + const mixedInput = [U8_MAX, undefined, U32_MAX]; + + const { value: mixedValue } = await contractInstance.functions + .echo_vec_option(mixedInput) + .call(); + + expect(mixedValue).toStrictEqual(mixedInput); + }); + + it('echos tuple option', async () => { + const someInput = [U8_MAX, U16_MAX]; + + const { value: someValue } = await contractInstance.functions + .echo_tuple_option(someInput) + .call(); + + expect(someValue).toStrictEqual(someInput); + + const noneInput = [undefined, undefined]; + + const { value: noneValue } = await contractInstance.functions + .echo_tuple_option(noneInput) + .call(); + + expect(noneValue).toStrictEqual(noneInput); + + const mixedInput = [U8_MAX, undefined]; + + const { value: mixedValue } = await contractInstance.functions + .echo_tuple_option(mixedInput) + .call(); + + expect(mixedValue).toStrictEqual(mixedInput); + }); + + it('echoes enum option', async () => { + const someInput = { a: U8_MAX }; + + const { value: someValue } = await contractInstance.functions + .echo_enum_option(someInput) + .call(); + + expect(someValue).toStrictEqual(someInput); + + const noneInput = { b: undefined }; + + const { value: noneValue } = await contractInstance.functions + .echo_enum_option(noneInput) + .call(); + + expect(noneValue).toStrictEqual(noneInput); + }); + + it('echos array option', async () => { + const someInput = [U8_MAX, U16_MAX, 123]; + + const { value: someValue } = await contractInstance.functions + .echo_array_option(someInput) + .call(); + + expect(someValue).toStrictEqual(someInput); + + const noneInput = [undefined, undefined, undefined]; + + const { value: noneValue } = await contractInstance.functions + .echo_array_option(noneInput) + .call(); + + expect(noneValue).toStrictEqual(noneInput); + + const mixedInput = [U8_MAX, undefined, 123]; + + const { value: mixedValue } = await contractInstance.functions + .echo_array_option(mixedInput) + .call(); + + expect(mixedValue).toStrictEqual(mixedInput); + }); +}); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index a6ada56e64f..eb8d8a6182d 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml @@ -62,4 +62,5 @@ members = [ "vector-types-contract", "vector-types-script", "vectors", + "options", ] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/options/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/options/Forc.toml new file mode 100644 index 00000000000..4b0808785d2 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/options/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "options" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/options/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/options/src/main.sw new file mode 100644 index 00000000000..e166367100f --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/options/src/main.sw @@ -0,0 +1,46 @@ +contract; + +enum OptionEnum { + a: Option, + b: Option, +} + +struct OptionStruct { + one: OptionEnum, + two: Option, +} + +abi OptionContract { + fn echo_option(arg: Option) -> Option; + fn echo_struct_enum_option(arg: OptionStruct) -> OptionStruct; + fn echo_vec_option(arg: Vec>) -> Vec>; + fn echo_tuple_option(arg: (Option, Option)) -> (Option, Option); + fn echo_enum_option(arg: OptionEnum) -> OptionEnum; + fn echo_array_option(arg: [Option; 3]) -> [Option; 3]; +} + +impl OptionContract for Contract { + fn echo_option(arg: Option) -> Option { + arg + } + + fn echo_struct_enum_option(arg: OptionStruct) -> OptionStruct { + arg + } + + fn echo_vec_option(arg: Vec>) -> Vec> { + arg + } + + fn echo_tuple_option(arg: (Option, Option)) -> (Option, Option) { + arg + } + + fn echo_enum_option(arg: OptionEnum) -> OptionEnum { + arg + } + + fn echo_array_option(arg: [Option; 3]) -> [Option; 3] { + arg + } +}