From 9c07b0048650b520a2ecaf454977391a4f5be0c8 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Tue, 30 Jul 2024 16:44:13 +0100 Subject: [PATCH] feat!: improve `()` and `Option` type handling (#2777) * chore: organized workspace alphabetically * feat: added void contract * feat: added void test * chore: added `VOID_TYPE` * feat: converted `void` tests to echo based, added NativeEnums * feat: added `VoidCoder` * chore: changeset * chore: simplify the `EnumCoder::isNativeEnum` check * chore: update void test * feat: implemented EmptyType for typegen, matched to `undefined` * feat: added e2e tests around the void arguments * chore: fixing tests * feat: implemented `getMandatoryInputs` * feat: added optional argument parsing * chore: added Option ABI type testing * chore: added forcBuildFlag to test * feat: implemented option function parameters * chore: removed optional arguments from tests * chore: made void return type for EmptyType * chore: added missing test group * deps: added `ramda` to `abi-coder` * chore: implemented optional parameters for coder * chore: fixing lock file * chore: lint * chore: renamed `findNonEmptyInputs` to `findNonVoidInputs` * chore: iterate over all inputs for `decodeArguments` * chore: removed void check for `decodeOutput` * chore: added test for optional option * chore: housekeeping * Add option tests * Linting * chore: add script test with options (WIP) * chore: added script options test * chore: renamed `getMandatoryInputs` -> `getFunctionInputs` * chore: fixed lockfile * chore: updated changeset * chore: fix lock file * reinstall ramda * Removed dependency on ramda * Added `@group browser` tests * chore: implemented optimizations for `getFunctionInputs` * chore: implemented `padValuesWithUndefined` * chore: added missing test groupings --------- Co-authored-by: Chad Nehemiah --- .changeset/tender-birds-tap.md | 6 + .../src/guide/types/options.test.ts | 31 ++-- packages/abi-coder/src/FunctionFragment.ts | 81 +++------- .../src/encoding/coders/EnumCoder.ts | 12 +- .../src/encoding/coders/VoidCoder.test.ts | 38 +++++ .../src/encoding/coders/VoidCoder.ts | 17 +++ .../src/encoding/strategies/getCoderV1.ts | 4 + packages/abi-coder/src/utils/constants.ts | 2 + .../src/utils/getFunctionInputs.test.ts | 138 ++++++++++++++++++ .../abi-coder/src/utils/getFunctionInputs.ts | 22 +++ packages/abi-coder/src/utils/json-abi.test.ts | 8 +- packages/abi-coder/src/utils/json-abi.ts | 10 +- .../src/utils/padValuesWithUndefined.test.ts | 52 +++++++ .../src/utils/padValuesWithUndefined.ts | 12 ++ .../src/abi/functions/Function.test.ts | 2 +- .../abi-typegen/src/abi/functions/Function.ts | 19 +-- .../src/abi/types/EmptyType.test.ts | 2 +- .../abi-typegen/src/abi/types/EmptyType.ts | 8 +- .../src/templates/contract/dts.test.ts | 2 +- .../src/utils/getFunctionInputs.test.ts | 135 +++++++++++++++++ .../src/utils/getFunctionInputs.ts | 28 ++++ .../fixtures/forc-projects/full/src/main.sw | 8 + .../test/fixtures/templates/contract/dts.hbs | 14 +- .../fuel-gauge/src/call-test-contract.test.ts | 5 +- .../fuel-gauge/src/coverage-contract.test.ts | 5 +- packages/fuel-gauge/src/options.test.ts | 125 ++++++++-------- .../predicate/predicate-input-data.test.ts | 9 +- .../src/script-with-options.test.ts | 49 +++++++ packages/fuel-gauge/src/void.test.ts | 84 +++++++++++ .../test/fixtures/forc-projects/Forc.toml | 30 ++-- .../forc-projects/options/src/main.sw | 18 +++ .../script-with-options/Forc.toml | 6 + .../script-with-options/src/main.sw | 5 + .../fixtures/forc-projects/void/Forc.toml | 4 + .../fixtures/forc-projects/void/src/main.sw | 45 ++++++ 35 files changed, 825 insertions(+), 211 deletions(-) create mode 100644 .changeset/tender-birds-tap.md create mode 100644 packages/abi-coder/src/encoding/coders/VoidCoder.test.ts create mode 100644 packages/abi-coder/src/encoding/coders/VoidCoder.ts create mode 100644 packages/abi-coder/src/utils/getFunctionInputs.test.ts create mode 100644 packages/abi-coder/src/utils/getFunctionInputs.ts create mode 100644 packages/abi-coder/src/utils/padValuesWithUndefined.test.ts create mode 100644 packages/abi-coder/src/utils/padValuesWithUndefined.ts create mode 100644 packages/abi-typegen/src/utils/getFunctionInputs.test.ts create mode 100644 packages/abi-typegen/src/utils/getFunctionInputs.ts create mode 100644 packages/fuel-gauge/src/script-with-options.test.ts create mode 100644 packages/fuel-gauge/src/void.test.ts create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/src/main.sw create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/void/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/void/src/main.sw diff --git a/.changeset/tender-birds-tap.md b/.changeset/tender-birds-tap.md new file mode 100644 index 00000000000..8038e725ddc --- /dev/null +++ b/.changeset/tender-birds-tap.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/abi-coder": patch +"@fuel-ts/abi-typegen": patch +--- + +feat!: improve `()` and `Option` type handling diff --git a/apps/docs-snippets/src/guide/types/options.test.ts b/apps/docs-snippets/src/guide/types/options.test.ts index 30b9120a558..fb18259debd 100644 --- a/apps/docs-snippets/src/guide/types/options.test.ts +++ b/apps/docs-snippets/src/guide/types/options.test.ts @@ -1,19 +1,25 @@ -import type { Contract } from 'fuels'; +import { launchTestNode } from 'fuels/test-utils'; -import { DocSnippetProjectsEnum } from '../../../test/fixtures/forc-projects'; -import { createAndDeployContractFromProject } from '../../utils'; +import { SumOptionU8Abi__factory } from '../../../test/typegen'; +import bytecode from '../../../test/typegen/contracts/SumOptionU8Abi.hex'; + +function setupContract() { + return launchTestNode({ + contractsConfigs: [{ deployer: SumOptionU8Abi__factory, bytecode }], + }); +} /** * @group node + * @group browser */ -describe(__filename, () => { - let contract: Contract; - - beforeAll(async () => { - contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.SUM_OPTION_U8); - }); - +describe('options', () => { it('should successfully execute contract call to sum 2 option inputs (2 INPUTS)', async () => { + using launched = await setupContract(); + const { + contracts: [contract], + } = launched; + // #region options-1 // Sway Option // #region options-3 @@ -29,6 +35,11 @@ describe(__filename, () => { }); it('should successfully execute contract call to sum 2 option inputs (1 INPUT)', async () => { + using launched = await setupContract(); + const { + contracts: [contract], + } = launched; + // #region options-4 const input: number | undefined = 5; diff --git a/packages/abi-coder/src/FunctionFragment.ts b/packages/abi-coder/src/FunctionFragment.ts index e8941de523d..addc6eed345 100644 --- a/packages/abi-coder/src/FunctionFragment.ts +++ b/packages/abi-coder/src/FunctionFragment.ts @@ -10,20 +10,11 @@ import { ResolvedAbiType } from './ResolvedAbiType'; import type { DecodedValue, InputValue } from './encoding/coders/AbstractCoder'; import { StdStringCoder } from './encoding/coders/StdStringCoder'; import { TupleCoder } from './encoding/coders/TupleCoder'; -import type { - JsonAbi, - JsonAbiArgument, - JsonAbiFunction, - JsonAbiFunctionAttribute, -} from './types/JsonAbi'; +import type { JsonAbi, JsonAbiFunction, JsonAbiFunctionAttribute } from './types/JsonAbi'; import type { EncodingVersion } from './utils/constants'; -import { OPTION_CODER_TYPE } from './utils/constants'; -import { - findFunctionByName, - findNonEmptyInputs, - findTypeById, - getEncodingVersion, -} from './utils/json-abi'; +import { getFunctionInputs } from './utils/getFunctionInputs'; +import { findFunctionByName, findNonVoidInputs, getEncodingVersion } from './utils/json-abi'; +import { padValuesWithUndefined } from './utils/padValuesWithUndefined'; export class FunctionFragment< TAbi extends JsonAbi = JsonAbi, @@ -66,59 +57,30 @@ export class FunctionFragment< } encodeArguments(values: InputValue[]): Uint8Array { - FunctionFragment.verifyArgsAndInputsAlign(values, this.jsonFn.inputs, this.jsonAbi); - - const shallowCopyValues = values.slice(); - const nonEmptyInputs = findNonEmptyInputs(this.jsonAbi, this.jsonFn.inputs); - - if (Array.isArray(values) && nonEmptyInputs.length !== values.length) { - shallowCopyValues.length = this.jsonFn.inputs.length; - shallowCopyValues.fill(undefined as unknown as InputValue, values.length); + const inputs = getFunctionInputs({ jsonAbi: this.jsonAbi, inputs: this.jsonFn.inputs }); + const mandatoryInputLength = inputs.filter((i) => !i.isOptional).length; + if (values.length < mandatoryInputLength) { + throw new FuelError( + ErrorCode.ABI_TYPES_AND_VALUES_MISMATCH, + `Invalid number of arguments. Expected a minimum of ${mandatoryInputLength} arguments, received ${values.length}` + ); } - const coders = nonEmptyInputs.map((t) => + const coders = this.jsonFn.inputs.map((t) => AbiCoder.getCoder(this.jsonAbi, t, { encoding: this.encoding, }) ); - return new TupleCoder(coders).encode(shallowCopyValues); - } - - private static verifyArgsAndInputsAlign( - args: InputValue[], - inputs: readonly JsonAbiArgument[], - abi: JsonAbi - ) { - if (args.length === inputs.length) { - return; - } - - const inputTypes = inputs.map((input) => findTypeById(abi, input.type)); - const optionalInputs = inputTypes.filter( - (x) => x.type === OPTION_CODER_TYPE || x.type === '()' - ); - if (optionalInputs.length === inputTypes.length) { - return; - } - if (inputTypes.length - optionalInputs.length === args.length) { - return; - } - - const errorMsg = `Mismatch between provided arguments and expected ABI inputs. Provided ${ - args.length - } arguments, but expected ${inputs.length - optionalInputs.length} (excluding ${ - optionalInputs.length - } optional inputs).`; - - throw new FuelError(ErrorCode.ABI_TYPES_AND_VALUES_MISMATCH, errorMsg); + const argumentValues = padValuesWithUndefined(values, this.jsonFn.inputs); + return new TupleCoder(coders).encode(argumentValues); } decodeArguments(data: BytesLike) { const bytes = arrayify(data); - const nonEmptyInputs = findNonEmptyInputs(this.jsonAbi, this.jsonFn.inputs); + const nonVoidInputs = findNonVoidInputs(this.jsonAbi, this.jsonFn.inputs); - if (nonEmptyInputs.length === 0) { + if (nonVoidInputs.length === 0) { // The VM is current return 0x0000000000000000, but we should treat it as undefined / void if (bytes.length === 0) { return undefined; @@ -129,19 +91,19 @@ export class FunctionFragment< `Types/values length mismatch during decode. ${JSON.stringify({ count: { types: this.jsonFn.inputs.length, - nonEmptyInputs: nonEmptyInputs.length, + nonVoidInputs: nonVoidInputs.length, values: bytes.length, }, value: { args: this.jsonFn.inputs, - nonEmptyInputs, + nonVoidInputs, values: bytes, }, })}` ); } - const result = nonEmptyInputs.reduce( + const result = this.jsonFn.inputs.reduce( (obj: { decoded: unknown[]; offset: number }, input) => { const coder = AbiCoder.getCoder(this.jsonAbi, input, { encoding: this.encoding }); const [decodedValue, decodedValueByteSize] = coder.decode(bytes, obj.offset); @@ -158,11 +120,6 @@ export class FunctionFragment< } decodeOutput(data: BytesLike): [DecodedValue | undefined, number] { - const outputAbiType = findTypeById(this.jsonAbi, this.jsonFn.output.type); - if (outputAbiType.type === '()') { - return [undefined, 0]; - } - const bytes = arrayify(data); const coder = AbiCoder.getCoder(this.jsonAbi, this.jsonFn.output, { encoding: this.encoding, diff --git a/packages/abi-coder/src/encoding/coders/EnumCoder.ts b/packages/abi-coder/src/encoding/coders/EnumCoder.ts index 1638dcbcb9f..0124042bfe7 100644 --- a/packages/abi-coder/src/encoding/coders/EnumCoder.ts +++ b/packages/abi-coder/src/encoding/coders/EnumCoder.ts @@ -3,13 +3,12 @@ import { toNumber } from '@fuel-ts/math'; import { concat } from '@fuel-ts/utils'; import type { RequireExactlyOne } from 'type-fest'; -import { OPTION_CODER_TYPE } from '../../utils/constants'; +import { OPTION_CODER_TYPE, VOID_TYPE } from '../../utils/constants'; import { hasNestedOption } from '../../utils/utilities'; import type { TypesOfCoder } from './AbstractCoder'; import { Coder } from './AbstractCoder'; import { BigNumberCoder } from './BigNumberCoder'; -import type { TupleCoder } from './TupleCoder'; export type InputValueOf> = RequireExactlyOne<{ [P in keyof TCoders]: TypesOfCoder['Input']; @@ -42,14 +41,9 @@ export class EnumCoder> extends Coder< this.#shouldValidateLength = !(this.type === OPTION_CODER_TYPE || hasNestedOption(coders)); } - // We parse a native enum as an empty tuple, so we are looking for a tuple with no child coders. - // The '()' is enough but the child coders is a stricter check. + // Checks that we're handling a native enum that is of type void. #isNativeEnum(coder: Coder): boolean { - if (this.type !== OPTION_CODER_TYPE && coder.type === '()') { - const tupleCoder = coder as TupleCoder<[]>; - return tupleCoder.coders.length === 0; - } - return false; + return this.type !== OPTION_CODER_TYPE && coder.type === VOID_TYPE; } #encodeNativeEnum(value: string): Uint8Array { diff --git a/packages/abi-coder/src/encoding/coders/VoidCoder.test.ts b/packages/abi-coder/src/encoding/coders/VoidCoder.test.ts new file mode 100644 index 00000000000..de494da7fef --- /dev/null +++ b/packages/abi-coder/src/encoding/coders/VoidCoder.test.ts @@ -0,0 +1,38 @@ +import { VoidCoder } from './VoidCoder'; + +/** + * @group node + * @group browser + */ +describe('VoidCoder', () => { + it('should have properties', () => { + const coder = new VoidCoder(); + expect(coder.name).toEqual('void'); + expect(coder.type).toEqual('()'); + expect(coder.encodedLength).toEqual(0); + }); + + describe('encode', () => { + it('should return an empty Uint8Array', () => { + const input = undefined; + const expected = new Uint8Array([]); + + const coder = new VoidCoder(); + const value = coder.encode(input); + expect(value).toEqual(expected); + }); + }); + + describe('decode', () => { + it('should return an undefined result', () => { + const input = new Uint8Array([]); + const expected = undefined; + const expectedOffset = 0; + + const coder = new VoidCoder(); + const [value, offset] = coder.decode(input, 0); + expect(value).toEqual(expected); + expect(offset).toEqual(expectedOffset); + }); + }); +}); diff --git a/packages/abi-coder/src/encoding/coders/VoidCoder.ts b/packages/abi-coder/src/encoding/coders/VoidCoder.ts new file mode 100644 index 00000000000..d835bf5ad2c --- /dev/null +++ b/packages/abi-coder/src/encoding/coders/VoidCoder.ts @@ -0,0 +1,17 @@ +import { VOID_TYPE } from '../../utils/constants'; + +import { Coder } from './AbstractCoder'; + +export class VoidCoder extends Coder { + constructor() { + super('void', VOID_TYPE, 0); + } + + encode(_value: undefined): Uint8Array { + return new Uint8Array([]); + } + + decode(_data: Uint8Array, offset: number): [undefined, number] { + return [undefined, offset]; + } +} diff --git a/packages/abi-coder/src/encoding/strategies/getCoderV1.ts b/packages/abi-coder/src/encoding/strategies/getCoderV1.ts index 6e89b95a2d5..816fc1d3934 100644 --- a/packages/abi-coder/src/encoding/strategies/getCoderV1.ts +++ b/packages/abi-coder/src/encoding/strategies/getCoderV1.ts @@ -20,6 +20,7 @@ import { U64_CODER_TYPE, U8_CODER_TYPE, VEC_CODER_TYPE, + VOID_TYPE, arrayRegEx, enumRegEx, stringRegEx, @@ -44,6 +45,7 @@ import { StringCoder } from '../coders/StringCoder'; import { StructCoder } from '../coders/StructCoder'; import { TupleCoder } from '../coders/TupleCoder'; import { VecCoder } from '../coders/VecCoder'; +import { VoidCoder } from '../coders/VoidCoder'; import { getCoders } from './getCoders'; @@ -82,6 +84,8 @@ export const getCoder: GetCoderFn = ( return new StdStringCoder(); case STR_SLICE_CODER_TYPE: return new StrSliceCoder(); + case VOID_TYPE: + return new VoidCoder(); default: break; } diff --git a/packages/abi-coder/src/utils/constants.ts b/packages/abi-coder/src/utils/constants.ts index a533ddf8f51..7deac9d7785 100644 --- a/packages/abi-coder/src/utils/constants.ts +++ b/packages/abi-coder/src/utils/constants.ts @@ -16,6 +16,8 @@ export const VEC_CODER_TYPE = 'struct Vec'; export const BYTES_CODER_TYPE = 'struct Bytes'; export const STD_STRING_CODER_TYPE = 'struct String'; export const STR_SLICE_CODER_TYPE = 'str'; +export const VOID_TYPE = '()'; +export const OPTION_REGEX: RegExp = /^enum (std::option::)?Option$/m; export const stringRegEx = /str\[(?[0-9]+)\]/; export const arrayRegEx = /\[(?[\w\s\\[\]]+);\s*(?[0-9]+)\]/; export const structRegEx = /^struct (?\w+)$/; diff --git a/packages/abi-coder/src/utils/getFunctionInputs.test.ts b/packages/abi-coder/src/utils/getFunctionInputs.test.ts new file mode 100644 index 00000000000..7bab5a7e85c --- /dev/null +++ b/packages/abi-coder/src/utils/getFunctionInputs.test.ts @@ -0,0 +1,138 @@ +import type { JsonAbi, JsonAbiArgument, JsonAbiType } from '../types/JsonAbi'; + +import { getFunctionInputs } from './getFunctionInputs'; + +const nonEmptyType: JsonAbiType = { + type: 'u8', + typeId: 1, + components: null, + typeParameters: null, +}; + +const voidAbiType: JsonAbiType = { + type: '()', + typeId: 2, + components: null, + typeParameters: null, +}; + +const optionAbiType: JsonAbiType = { + type: 'enum Option', + typeId: 3, + components: null, + typeParameters: null, +}; + +const debugOptionAbiType: JsonAbiType = { + type: 'enum std::option::Option', + typeId: 4, + components: null, + typeParameters: null, +}; + +const EMPTY_ABI_TYPES: [string, JsonAbiType][] = [ + ['void', voidAbiType], + ['option (release)', optionAbiType], + ['option (debug)', debugOptionAbiType], +]; + +const jsonAbi: JsonAbi = { + encoding: '1', + types: [nonEmptyType, voidAbiType, optionAbiType, debugOptionAbiType], + functions: [], + loggedTypes: [], + messagesTypes: [], + configurables: [], +}; + +/** + * @group node + * @group browser + */ +describe.each(EMPTY_ABI_TYPES)( + 'getFunctionInputs.ts [empty=%s]', + (_: string, emptyAbiType: JsonAbiType) => { + it('should handle no inputs', () => { + const inputs: Array = []; + + const result = getFunctionInputs({ jsonAbi, inputs }); + + expect(result).toEqual([]); + }); + + it('should handle all empty types [empty, empty, empty]', () => { + const A = { type: emptyAbiType.typeId, name: 'a', typeArguments: null }; + const B = { type: emptyAbiType.typeId, name: 'b', typeArguments: null }; + const C = { type: emptyAbiType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ jsonAbi, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: true }, + { ...B, isOptional: true }, + { ...C, isOptional: true }, + ]); + }); + + it('should handle all non-empty types', () => { + const A = { type: nonEmptyType.typeId, name: 'a', typeArguments: null }; + const B = { type: nonEmptyType.typeId, name: 'b', typeArguments: null }; + const C = { type: nonEmptyType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ jsonAbi, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: false }, + ]); + }); + + it('should handle a mix [non-empty, non-empty, empty]', () => { + const A = { type: nonEmptyType.typeId, name: 'a', typeArguments: null }; + const B = { type: nonEmptyType.typeId, name: 'b', typeArguments: null }; + const C = { type: emptyAbiType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ jsonAbi, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: true }, + ]); + }); + + it('should handle a mix [empty, non-empty, non-empty]', () => { + const A = { type: emptyAbiType.typeId, name: 'a', typeArguments: null }; + const B = { type: nonEmptyType.typeId, name: 'b', typeArguments: null }; + const C = { type: nonEmptyType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ jsonAbi, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: false }, + ]); + }); + + it('should handle a mix [non-empty, empty, non-empty]', () => { + const A = { type: nonEmptyType.typeId, name: 'a', typeArguments: null }; + const B = { type: emptyAbiType.typeId, name: 'b', typeArguments: null }; + const C = { type: nonEmptyType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ jsonAbi, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: false }, + ]); + }); + } +); diff --git a/packages/abi-coder/src/utils/getFunctionInputs.ts b/packages/abi-coder/src/utils/getFunctionInputs.ts new file mode 100644 index 00000000000..784694847be --- /dev/null +++ b/packages/abi-coder/src/utils/getFunctionInputs.ts @@ -0,0 +1,22 @@ +import type { JsonAbi, JsonAbiArgument } from '../types/JsonAbi'; + +import { OPTION_REGEX, VOID_TYPE } from './constants'; +import { findTypeById } from './json-abi'; + +export type FunctionInput = TArg & { + isOptional: boolean; +}; + +export const getFunctionInputs = (params: { + jsonAbi: JsonAbi; + inputs: readonly JsonAbiArgument[]; +}): Array => { + const { jsonAbi, inputs } = params; + let isMandatory = false; + + return inputs.reduceRight((result, input) => { + const type = findTypeById(jsonAbi, input.type); + isMandatory = isMandatory || (type.type !== VOID_TYPE && !OPTION_REGEX.test(type.type)); + return [{ ...input, isOptional: !isMandatory }, ...result]; + }, [] as FunctionInput[]); +}; diff --git a/packages/abi-coder/src/utils/json-abi.test.ts b/packages/abi-coder/src/utils/json-abi.test.ts index 853530c87bb..be661e335a2 100644 --- a/packages/abi-coder/src/utils/json-abi.test.ts +++ b/packages/abi-coder/src/utils/json-abi.test.ts @@ -4,7 +4,7 @@ import type { JsonAbi, JsonAbiArgument } from '../types/JsonAbi'; import { ENCODING_V1 } from './constants'; import { findFunctionByName, - findNonEmptyInputs, + findNonVoidInputs, findTypeById, findVectorBufferArgument, getEncodingVersion, @@ -98,7 +98,7 @@ describe('json-abi', () => { }); }); - describe('findNonEmptyInputs', () => { + describe('findNonVoidInputs', () => { it('should find non-empty inputs', () => { const inputs: JsonAbiArgument[] = [ { name: 'a', type: 1, typeArguments: [] }, @@ -106,7 +106,7 @@ describe('json-abi', () => { ]; const expected = [{ name: 'b', type: 2, typeArguments: [] }]; - const actual = findNonEmptyInputs(MOCK_ABI, inputs); + const actual = findNonVoidInputs(MOCK_ABI, inputs); expect(actual).toEqual(expected); }); @@ -114,7 +114,7 @@ describe('json-abi', () => { it('should throw an error if the type is not found', () => { const inputs: JsonAbiArgument[] = [{ name: 'a', type: -1, typeArguments: [] }]; - expect(() => findNonEmptyInputs(MOCK_ABI, inputs)).toThrowError( + expect(() => findNonVoidInputs(MOCK_ABI, inputs)).toThrowError( `Type with typeId '-1' doesn't exist in the ABI.` ); }); diff --git a/packages/abi-coder/src/utils/json-abi.ts b/packages/abi-coder/src/utils/json-abi.ts index 753244174a8..1091554c8a4 100644 --- a/packages/abi-coder/src/utils/json-abi.ts +++ b/packages/abi-coder/src/utils/json-abi.ts @@ -3,7 +3,7 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors'; import type { ResolvedAbiType } from '../ResolvedAbiType'; import type { JsonAbi, JsonAbiArgument, JsonAbiFunction, JsonAbiType } from '../types/JsonAbi'; -import { ENCODING_V1, type EncodingVersion } from './constants'; +import { ENCODING_V1, VOID_TYPE, type EncodingVersion } from './constants'; /** * Asserts that the encoding version is supported by the ABI coder. @@ -63,17 +63,17 @@ export const findTypeById = (abi: JsonAbi, typeId: number): JsonAbiType => { }; /** - * Find all non-empty inputs in a list of inputs. + * Find all non-void inputs in a list of inputs. * i.e. all inputs that are not of the type '()'. * * @param abi - the JsonAbi object * @param inputs - the list of inputs to filter - * @returns the list of non-empty inputs + * @returns the list of non-void inputs */ -export const findNonEmptyInputs = ( +export const findNonVoidInputs = ( abi: JsonAbi, inputs: readonly JsonAbiArgument[] -): JsonAbiArgument[] => inputs.filter((input) => findTypeById(abi, input.type).type !== '()'); +): JsonAbiArgument[] => inputs.filter((input) => findTypeById(abi, input.type).type !== VOID_TYPE); /** * Find the vector buffer argument in a list of components. diff --git a/packages/abi-coder/src/utils/padValuesWithUndefined.test.ts b/packages/abi-coder/src/utils/padValuesWithUndefined.test.ts new file mode 100644 index 00000000000..728a8b29240 --- /dev/null +++ b/packages/abi-coder/src/utils/padValuesWithUndefined.test.ts @@ -0,0 +1,52 @@ +import type { InputValue } from '../encoding/coders/AbstractCoder'; +import type { JsonAbiArgument } from '../types/JsonAbi'; + +import { padValuesWithUndefined } from './padValuesWithUndefined'; + +const MOCK_INPUT: JsonAbiArgument = { + name: 'test', + type: 0, + typeArguments: [], +}; + +/** + * @group node + * @group browser + */ +describe('padValuesWithUndefined', () => { + it('should not pad values if they are already the same length as inputs', () => { + const values: InputValue[] = [1, 2, 3]; + const inputs: ReadonlyArray = [MOCK_INPUT, MOCK_INPUT, MOCK_INPUT]; + + const result = padValuesWithUndefined(values, inputs); + + expect(result).toEqual(values); + }); + + it('should not pad values if they are longer than inputs', () => { + const values: InputValue[] = [1, 2, 3]; + const inputs: ReadonlyArray = [MOCK_INPUT, MOCK_INPUT]; + + const result = padValuesWithUndefined(values, inputs); + + expect(result).toEqual(values); + }); + + it('should pad values with undefined if they are shorter than inputs', () => { + const values: InputValue[] = [1, 2]; + const inputs: ReadonlyArray = [MOCK_INPUT, MOCK_INPUT, MOCK_INPUT]; + + const result = padValuesWithUndefined(values, inputs); + + expect(result).toEqual([1, 2, undefined]); + }); + + it('should pad values with undefined if they are empty', () => { + const values: InputValue[] = []; + const inputs: ReadonlyArray = [MOCK_INPUT, MOCK_INPUT, MOCK_INPUT]; + + const result = padValuesWithUndefined(values, inputs); + + expect(result).toEqual([undefined, undefined, undefined]); + }); +}); diff --git a/packages/abi-coder/src/utils/padValuesWithUndefined.ts b/packages/abi-coder/src/utils/padValuesWithUndefined.ts new file mode 100644 index 00000000000..7410fa8ee45 --- /dev/null +++ b/packages/abi-coder/src/utils/padValuesWithUndefined.ts @@ -0,0 +1,12 @@ +import type { InputValue } from '../encoding/coders/AbstractCoder'; + +export const padValuesWithUndefined = (values: InputValue[], inputs: ArrayLike) => { + if (values.length >= inputs.length) { + return values; + } + + const paddedValues = values.slice(); + paddedValues.length = inputs.length; + paddedValues.fill(undefined, values.length); + return paddedValues; +}; diff --git a/packages/abi-typegen/src/abi/functions/Function.test.ts b/packages/abi-typegen/src/abi/functions/Function.test.ts index 8eaabb796c3..8270f48e981 100644 --- a/packages/abi-typegen/src/abi/functions/Function.test.ts +++ b/packages/abi-typegen/src/abi/functions/Function.test.ts @@ -63,6 +63,6 @@ describe('Function.ts', () => { expect(func.name).toEqual(rawAbiFunction.name); expect(func.attributes.inputs).toEqual('Option'); expect(func.attributes.output).toEqual('Option'); - expect(func.attributes.prefixedInputs).toEqual('x: Option'); + expect(func.attributes.prefixedInputs).toEqual('x?: Option'); }); }); diff --git a/packages/abi-typegen/src/abi/functions/Function.ts b/packages/abi-typegen/src/abi/functions/Function.ts index 30a031daf5b..bc30f24cc28 100644 --- a/packages/abi-typegen/src/abi/functions/Function.ts +++ b/packages/abi-typegen/src/abi/functions/Function.ts @@ -1,10 +1,9 @@ import type { IFunction, JsonAbiFunction, IFunctionAttributes } from '../../index'; import { TargetEnum } from '../../types/enums/TargetEnum'; import type { IType } from '../../types/interfaces/IType'; -import { findType } from '../../utils/findType'; +import { getFunctionInputs } from '../../utils/getFunctionInputs'; import { resolveInputLabel } from '../../utils/getTypeDeclaration'; import { parseTypeArguments } from '../../utils/parseTypeArguments'; -import { EmptyType } from '../types/EmptyType'; export class Function implements IFunction { public name: string; @@ -27,24 +26,22 @@ export class Function implements IFunction { bundleInputTypes(shouldPrefixParams: boolean = false) { const { types } = this; - // loop through all inputs - const inputs = this.rawAbiFunction.inputs - .filter((input) => { - const type = findType({ types, typeId: input.type }); - return type.rawAbiType.type !== EmptyType.swayType; - }) - .map((input) => { + // loop through all mandatory inputs + const inputs = getFunctionInputs({ types, inputs: this.rawAbiFunction.inputs }).map( + ({ isOptional, ...input }) => { const { name, type: typeId, typeArguments } = input; const typeDecl = resolveInputLabel(types, typeId, typeArguments); // assemble it in `[key: string]: ` fashion if (shouldPrefixParams) { - return `${name}: ${typeDecl}`; + const optionalSuffix = isOptional ? '?' : ''; + return `${name}${optionalSuffix}: ${typeDecl}`; } return typeDecl; - }); + } + ); return inputs.join(', '); } diff --git a/packages/abi-typegen/src/abi/types/EmptyType.test.ts b/packages/abi-typegen/src/abi/types/EmptyType.test.ts index 8b2a745d4b0..ea2ea15c7a9 100644 --- a/packages/abi-typegen/src/abi/types/EmptyType.test.ts +++ b/packages/abi-typegen/src/abi/types/EmptyType.test.ts @@ -14,7 +14,7 @@ describe('EmptyType.ts', () => { }, }); - expect(emptyType.attributes.inputLabel).toEqual('never'); + expect(emptyType.attributes.inputLabel).toEqual('undefined'); expect(emptyType.attributes.outputLabel).toEqual('void'); }); }); diff --git a/packages/abi-typegen/src/abi/types/EmptyType.ts b/packages/abi-typegen/src/abi/types/EmptyType.ts index 346dde2bb84..37e1b59899c 100644 --- a/packages/abi-typegen/src/abi/types/EmptyType.ts +++ b/packages/abi-typegen/src/abi/types/EmptyType.ts @@ -13,12 +13,8 @@ export class EmptyType extends AType implements IType { constructor(params: { rawAbiType: JsonAbiType }) { super(params); this.attributes = { - /** - * The empty type is always ignored in function inputs. If it makes - * its way into a function's inputs list, it's a bug in the typegen. - */ - inputLabel: `never`, - outputLabel: `void`, + inputLabel: 'undefined', + outputLabel: 'void', }; } diff --git a/packages/abi-typegen/src/templates/contract/dts.test.ts b/packages/abi-typegen/src/templates/contract/dts.test.ts index 37311973429..10814b41565 100644 --- a/packages/abi-typegen/src/templates/contract/dts.test.ts +++ b/packages/abi-typegen/src/templates/contract/dts.test.ts @@ -14,7 +14,7 @@ import { renderDtsTemplate } from './dts'; * @group node */ describe('templates/dts', () => { - test.each(['debug', 'release'])('should render dts template', (build) => { + test.each(['debug', 'release'])('should render dts template [%s]', (build) => { // mocking const { restore } = mockVersions(); diff --git a/packages/abi-typegen/src/utils/getFunctionInputs.test.ts b/packages/abi-typegen/src/utils/getFunctionInputs.test.ts new file mode 100644 index 00000000000..d993fe8dce5 --- /dev/null +++ b/packages/abi-typegen/src/utils/getFunctionInputs.test.ts @@ -0,0 +1,135 @@ +import type { IType } from '../types/interfaces/IType'; +import type { JsonAbiArgument, JsonAbiType } from '../types/interfaces/JsonAbi'; + +import { getFunctionInputs } from './getFunctionInputs'; +import { makeType } from './makeType'; + +const nonEmptyType: JsonAbiType = { + type: 'u8', + typeId: 1, + components: null, + typeParameters: null, +}; + +const voidAbiType: JsonAbiType = { + type: '()', + typeId: 2, + components: null, + typeParameters: null, +}; + +const optionAbiType: JsonAbiType = { + type: 'enum Option', + typeId: 3, + components: null, + typeParameters: null, +}; + +const debugOptionAbiType: JsonAbiType = { + type: 'enum std::option::Option', + typeId: 4, + components: null, + typeParameters: null, +}; + +const EMPTY_ABI_TYPES: [string, JsonAbiType][] = [ + ['void', voidAbiType], + ['option (release)', optionAbiType], + ['option (debug)', debugOptionAbiType], +]; + +const types: Array = [nonEmptyType, voidAbiType, optionAbiType, debugOptionAbiType].map( + (rawAbiType) => makeType({ rawAbiType }) +); + +/** + * @group node + * @group browser + */ +describe.each(EMPTY_ABI_TYPES)( + 'getFunctionInputs.ts [empty=%s]', + (_: string, emptyAbiType: JsonAbiType) => { + it('should handle no inputs', () => { + const inputs: Array = []; + + const result = getFunctionInputs({ types, inputs }); + + expect(result).toEqual([]); + }); + + it('should handle all empty types [empty, empty, empty]', () => { + const A = { type: emptyAbiType.typeId, name: 'a', typeArguments: null }; + const B = { type: emptyAbiType.typeId, name: 'b', typeArguments: null }; + const C = { type: emptyAbiType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ types, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: true }, + { ...B, isOptional: true }, + { ...C, isOptional: true }, + ]); + }); + + it('should handle all non-empty types', () => { + const A = { type: nonEmptyType.typeId, name: 'a', typeArguments: null }; + const B = { type: nonEmptyType.typeId, name: 'b', typeArguments: null }; + const C = { type: nonEmptyType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ types, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: false }, + ]); + }); + + it('should handle a mix [non-empty, non-empty, empty]', () => { + const A = { type: nonEmptyType.typeId, name: 'a', typeArguments: null }; + const B = { type: nonEmptyType.typeId, name: 'b', typeArguments: null }; + const C = { type: emptyAbiType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ types, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: true }, + ]); + }); + + it('should handle a mix [empty, non-empty, non-empty]', () => { + const A = { type: emptyAbiType.typeId, name: 'a', typeArguments: null }; + const B = { type: nonEmptyType.typeId, name: 'b', typeArguments: null }; + const C = { type: nonEmptyType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ types, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: false }, + ]); + }); + + it('should handle a mix [non-empty, empty, non-empty]', () => { + const A = { type: nonEmptyType.typeId, name: 'a', typeArguments: null }; + const B = { type: emptyAbiType.typeId, name: 'b', typeArguments: null }; + const C = { type: nonEmptyType.typeId, name: 'c', typeArguments: null }; + const inputs: Array = [A, B, C]; + + const result = getFunctionInputs({ types, inputs }); + + expect(result).toEqual([ + { ...A, isOptional: false }, + { ...B, isOptional: false }, + { ...C, isOptional: false }, + ]); + }); + } +); diff --git a/packages/abi-typegen/src/utils/getFunctionInputs.ts b/packages/abi-typegen/src/utils/getFunctionInputs.ts new file mode 100644 index 00000000000..62df774ea8a --- /dev/null +++ b/packages/abi-typegen/src/utils/getFunctionInputs.ts @@ -0,0 +1,28 @@ +import { EmptyType } from '../abi/types/EmptyType'; +import { OptionType } from '../abi/types/OptionType'; +import type { IType } from '../types/interfaces/IType'; +import type { JsonAbiArgument } from '../types/interfaces/JsonAbi'; + +import { findType } from './findType'; + +export type FunctionInput = TArg & { + isOptional: boolean; +}; + +export const getFunctionInputs = (params: { + types: IType[]; + inputs: readonly JsonAbiArgument[]; +}): Array => { + const { types, inputs } = params; + let isMandatory = false; + + return inputs.reduceRight((result, input) => { + const type = findType({ types, typeId: input.type }); + const isTypeMandatory = + !EmptyType.isSuitableFor({ type: type.rawAbiType.type }) && + !OptionType.isSuitableFor({ type: type.rawAbiType.type }); + + isMandatory = isMandatory || isTypeMandatory; + return [{ ...input, isOptional: !isMandatory }, ...result]; + }, [] as FunctionInput[]); +}; diff --git a/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw b/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw index f71ae5ec2aa..05d5656cd91 100644 --- a/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw +++ b/packages/abi-typegen/test/fixtures/forc-projects/full/src/main.sw @@ -57,6 +57,7 @@ abi MyContract { fn types_empty_then_value(x: (), y: u8) -> (); fn types_value_then_empty(x: u8, y: ()) -> (); fn types_value_then_empty_then_value(x: u8, y: (), z: u8) -> (); + fn types_value_then_value_then_empty_then_empty(x: u8, y: u8, z: (), a: ()) -> (); fn types_u8(x: u8) -> u8; fn types_u16(x: u16) -> u16; fn types_u32(x: u32) -> u32; @@ -96,16 +97,23 @@ impl MyContract for Contract { fn types_empty(x: ()) -> () { x } + fn types_empty_then_value(x: (), y: u8) -> () { () } + fn types_value_then_empty(x: u8, y: ()) -> () { () } + fn types_value_then_empty_then_value(x: u8, y: (), z: u8) -> () { () } + fn types_value_then_value_then_empty_then_empty(x: u8, y: u8, z: (), a: ()) -> () { + () + } + fn types_u8(x: u8) -> u8 { 255 } diff --git a/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs b/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs index de5a210e251..84f478f076b 100644 --- a/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs +++ b/packages/abi-typegen/test/fixtures/templates/contract/dts.hbs @@ -89,6 +89,7 @@ export interface MyContractAbiInterface extends Interface { types_u8: FunctionFragment; types_value_then_empty: FunctionFragment; types_value_then_empty_then_value: FunctionFragment; + types_value_then_value_then_empty_then_empty: FunctionFragment; types_vector_geo: FunctionFragment; types_vector_option: FunctionFragment; types_vector_u8: FunctionFragment; @@ -109,15 +110,15 @@ export class MyContractAbi extends Contract { types_b512: InvokeFunction<[x: string], string>; types_bool: InvokeFunction<[x: boolean], boolean>; types_bytes: InvokeFunction<[x: Bytes], Bytes>; - types_empty: InvokeFunction<[], void>; - types_empty_then_value: InvokeFunction<[y: BigNumberish], void>; + types_empty: InvokeFunction<[x?: undefined], void>; + types_empty_then_value: InvokeFunction<[x: undefined, y: BigNumberish], void>; types_enum: InvokeFunction<[x: MyEnumInput], MyEnumOutput>; types_enum_with_vector: InvokeFunction<[x: EnumWithVectorInput], EnumWithVectorOutput>; types_evm_address: InvokeFunction<[x: EvmAddress], EvmAddress>; types_generic_enum: InvokeFunction<[x: GenericEnumInput], GenericEnumOutput>; types_generic_struct: InvokeFunction<[x: GenericStructWithEnumInput], GenericStructWithEnumOutput>; - types_option: InvokeFunction<[x: Option], Option>; - types_option_geo: InvokeFunction<[x: Option], Option>; + types_option: InvokeFunction<[x?: Option], Option>; + types_option_geo: InvokeFunction<[x?: Option], Option>; types_raw_slice: InvokeFunction<[x: RawSlice], RawSlice>; types_result: InvokeFunction<[x: Result], Result>; types_std_string: InvokeFunction<[x: StdString], StdString>; @@ -130,8 +131,9 @@ export class MyContractAbi extends Contract { types_u32: InvokeFunction<[x: BigNumberish], number>; types_u64: InvokeFunction<[x: BigNumberish], BN>; types_u8: InvokeFunction<[x: BigNumberish], number>; - types_value_then_empty: InvokeFunction<[x: BigNumberish], void>; - types_value_then_empty_then_value: InvokeFunction<[x: BigNumberish, z: BigNumberish], void>; + types_value_then_empty: InvokeFunction<[x: BigNumberish, y?: undefined], void>; + types_value_then_empty_then_value: InvokeFunction<[x: BigNumberish, y: undefined, z: BigNumberish], void>; + types_value_then_value_then_empty_then_empty: InvokeFunction<[x: BigNumberish, y: BigNumberish, z?: undefined, a?: undefined], void>; types_vector_geo: InvokeFunction<[x: Vec], Vec>; types_vector_option: InvokeFunction<[x: Vec], Vec>; types_vector_u8: InvokeFunction<[x: Vec], Vec>; diff --git a/packages/fuel-gauge/src/call-test-contract.test.ts b/packages/fuel-gauge/src/call-test-contract.test.ts index 87fa0f729b0..6466ae154d6 100644 --- a/packages/fuel-gauge/src/call-test-contract.test.ts +++ b/packages/fuel-gauge/src/call-test-contract.test.ts @@ -46,7 +46,7 @@ describe('CallTestContract', () => { const { value: empty } = await call1.waitForResult(); expect(empty.toHex()).toEqual(toHex(63)); - const call2 = await contract.functions.empty_then_value(35).call(); + const call2 = await contract.functions.empty_then_value(undefined, 35).call(); const { value: emptyThenValue } = await call2.waitForResult(); expect(emptyThenValue.toHex()).toEqual(toHex(63)); @@ -54,8 +54,7 @@ describe('CallTestContract', () => { const { value: valueThenEmpty } = await call3.waitForResult(); expect(valueThenEmpty.toHex()).toEqual(toHex(63)); - const call4 = await contract.functions.value_then_empty_then_value(35, 35).call(); - + const call4 = await contract.functions.value_then_empty_then_value(35, undefined, 35).call(); const { value: valueThenEmptyThenValue } = await call4.waitForResult(); expect(valueThenEmptyThenValue.toHex()).toEqual(toHex(63)); }); diff --git a/packages/fuel-gauge/src/coverage-contract.test.ts b/packages/fuel-gauge/src/coverage-contract.test.ts index ad5d7ca9705..75b6e9bb0bf 100644 --- a/packages/fuel-gauge/src/coverage-contract.test.ts +++ b/packages/fuel-gauge/src/coverage-contract.test.ts @@ -355,16 +355,13 @@ describe('Coverage Contract', () => { using contractInstance = await setupContract(); const INPUT_NONE = undefined; - const call1 = await contractInstance.functions.echo_option_extract_u32(INPUT_NONE).call(); + const call1 = await contractInstance.functions.echo_option_extract_u32(INPUT_NONE).call(); const { value: None } = await call1.waitForResult(); - expect(None).toStrictEqual(500); const call2 = await contractInstance.functions.echo_option_extract_u32().call(); - const { value: NoneVoid } = await call2.waitForResult(); - expect(NoneVoid).toStrictEqual(500); }); diff --git a/packages/fuel-gauge/src/options.test.ts b/packages/fuel-gauge/src/options.test.ts index c1662a28e3b..5169b770d7b 100644 --- a/packages/fuel-gauge/src/options.test.ts +++ b/packages/fuel-gauge/src/options.test.ts @@ -1,7 +1,10 @@ +import type { BigNumberish } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; import { OptionsAbi__factory } from '../test/typegen/contracts'; +import type { DeepStructInput } from '../test/typegen/contracts/OptionsAbi'; import OptionsAbiHex from '../test/typegen/contracts/OptionsAbi.hex'; +import type { Option } from '../test/typegen/contracts/common'; import { launchTestContract } from './utils'; @@ -16,6 +19,9 @@ function launchOptionsContract() { }); } +type DoubleTupleOptions = [Option, Option]; +type TripleTupleOptions = [Option, Option, Option]; + /** * @group node * @group browser @@ -44,35 +50,30 @@ describe('Options Tests', () => { }); it('echos u8 option', async () => { - const someInput = U8_MAX; - const noneInput = undefined; - using contract = await launchOptionsContract(); + const someInput = U8_MAX; const call1 = await contract.functions.echo_option(someInput).call(); const { value: someValue } = await call1.waitForResult(); - expect(someValue).toBe(someInput); + const noneInput = undefined; const call2 = await contract.functions.echo_option(noneInput).call(); const { value: noneValue } = await call2.waitForResult(); - expect(noneValue).toBe(noneInput); }); it('echos struct enum option', async () => { + using contract = await launchOptionsContract(); + const someInput = { one: { a: U8_MAX, }, two: U32_MAX, }; - - using contract = await launchOptionsContract(); - const call1 = await contract.functions.echo_struct_enum_option(someInput).call(); const { value: someValue } = await call1.waitForResult(); - expect(someValue).toStrictEqual(someInput); const noneInput = { @@ -81,123 +82,92 @@ describe('Options Tests', () => { }, two: undefined, }; - const call2 = await contract.functions.echo_struct_enum_option(noneInput).call(); const { value: noneValue } = await call2.waitForResult(); - expect(noneValue).toStrictEqual(noneInput); }); it('echos vec option', async () => { - const someInput = [U8_MAX, U16_MAX, U32_MAX]; - using contract = await launchOptionsContract(); + const someInput = [U8_MAX, U16_MAX, U32_MAX]; const call1 = await contract.functions.echo_vec_option(someInput).call(); - const { value: someValue } = await call1.waitForResult(); expect(someValue).toStrictEqual(someInput); const noneInput = [undefined, undefined, undefined]; - const call2 = await contract.functions.echo_vec_option(noneInput).call(); const { value: noneValue } = await call2.waitForResult(); - expect(noneValue).toStrictEqual(noneInput); const mixedInput = [U8_MAX, undefined, U32_MAX]; - const call3 = await contract.functions.echo_vec_option(mixedInput).call(); const { value: mixedValue } = await call3.waitForResult(); - expect(mixedValue).toStrictEqual(mixedInput); }); it('echos tuple option', async () => { - const someInput = [U8_MAX, U16_MAX]; - using contract = await launchOptionsContract(); + const someInput = [U8_MAX, U16_MAX] as DoubleTupleOptions; const call1 = await contract.functions.echo_tuple_option(someInput).call(); - const { value: someValue } = await call1.waitForResult(); - expect(someValue).toStrictEqual(someInput); - const noneInput = [undefined, undefined]; - + const noneInput = [undefined, undefined] as DoubleTupleOptions; const call2 = await contract.functions.echo_tuple_option(noneInput).call(); - const { value: noneValue } = await call2.waitForResult(); - expect(noneValue).toStrictEqual(noneInput); - const mixedInput = [U8_MAX, undefined]; - + const mixedInput = [U8_MAX, undefined] as DoubleTupleOptions; const call3 = await contract.functions.echo_tuple_option(mixedInput).call(); - const { value: mixedValue } = await call3.waitForResult(); - expect(mixedValue).toStrictEqual(mixedInput); }); it('echoes enum option', async () => { - const someInput = { a: U8_MAX }; - using contract = await launchOptionsContract(); + const someInput = { a: U8_MAX }; const call1 = await contract.functions.echo_enum_option(someInput).call(); - const { value: someValue } = await call1.waitForResult(); - expect(someValue).toStrictEqual(someInput); const noneInput = { b: undefined }; - const call2 = await contract.functions.echo_enum_option(noneInput).call(); - const { value: noneValue } = await call2.waitForResult(); - expect(noneValue).toStrictEqual(noneInput); }); it('echos array option', async () => { - const someInput = [U8_MAX, U16_MAX, 123]; - using contract = await launchOptionsContract(); + const someInput = [U8_MAX, U16_MAX, 123] as TripleTupleOptions; const call1 = await contract.functions.echo_array_option(someInput).call(); const { value: someValue } = await call1.waitForResult(); - expect(someValue).toStrictEqual(someInput); - const noneInput = [undefined, undefined, undefined]; - + const noneInput = [undefined, undefined, undefined] as TripleTupleOptions; const call2 = await contract.functions.echo_array_option(noneInput).call(); const { value: noneValue } = await call2.waitForResult(); - expect(noneValue).toStrictEqual(noneInput); - const mixedInput = [U8_MAX, undefined, 123]; - + const mixedInput = [U8_MAX, undefined, 123] as TripleTupleOptions; const call3 = await contract.functions.echo_array_option(mixedInput).call(); const { value: mixedValue } = await call3.waitForResult(); - expect(mixedValue).toStrictEqual(mixedInput); }); it('echoes deeply nested option', async () => { - const input = { + using contract = await launchOptionsContract(); + + const input: DeepStructInput = { DeepEnum: { a: [true, [U8_MAX, undefined, 123]], }, }; - - using contract = await launchOptionsContract(); const { waitForResult } = await contract.functions.echo_deeply_nested_option(input).call(); - const { value } = await waitForResult(); - expect(value).toStrictEqual(input); }); @@ -216,38 +186,59 @@ describe('Options Tests', () => { wallets: [wallet], } = launched; - const { waitForResult } = await contract.functions - .get_some_struct({ Address: { bits: wallet.address.toB256() } }) - .call(); - + const input = { Address: { bits: wallet.address.toB256() } }; + const { waitForResult } = await contract.functions.get_some_struct(input).call(); const { value } = await waitForResult(); - expect(value).toStrictEqual(undefined); }); it('echoes option enum diff sizes', async () => { using contract = await launchOptionsContract(); - const call1 = await contract.functions.echo_enum_diff_sizes(undefined).call(); - const { value } = await call1.waitForResult(); - - expect(value).toStrictEqual(undefined); + const call1 = await contract.functions.echo_enum_diff_sizes().call(); + const { value: value1 } = await call1.waitForResult(); + expect(value1).toStrictEqual(undefined); - const call2 = await contract.functions.echo_enum_diff_sizes({ a: U8_MAX }).call(); + const call2 = await contract.functions.echo_enum_diff_sizes(undefined).call(); const { value: value2 } = await call2.waitForResult(); + expect(value2).toStrictEqual(undefined); - expect(value2).toStrictEqual({ a: U8_MAX }); + const call3 = await contract.functions.echo_enum_diff_sizes({ a: U8_MAX }).call(); + const { value: value3 } = await call3.waitForResult(); + expect(value3).toStrictEqual({ a: U8_MAX }); - const call3 = await contract.functions + const call4 = await contract.functions .echo_enum_diff_sizes({ b: '0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c', }) .call(); - - const { value: value3 } = await call3.waitForResult(); - - expect(value3).toStrictEqual({ + const { value: value4 } = await call4.waitForResult(); + expect(value4).toStrictEqual({ b: '0x9ae5b658754e096e4d681c548daf46354495a437cc61492599e33fc64dcdc30c', }); }); + + it('should handle Option::None', async () => { + using contract = await launchOptionsContract(); + + const optionNone: Option = undefined; + const call1 = await contract.functions.type_then_option_then_type(42, optionNone, 43).call(); + const { value: value1 } = await call1.waitForResult(); + expect(value1).toStrictEqual(optionNone); + }); + + it('should handle optional options', async () => { + using contract = await launchOptionsContract(); + + const optionNone: Option = undefined; + const call1 = await contract.functions.option_then_type_then_option(optionNone, 42).call(); + const { value: value1 } = await call1.waitForResult(); + expect(value1).toStrictEqual(optionNone); + + const call2 = await contract.functions + .option_then_type_then_option(optionNone, 42, optionNone) + .call(); + const { value: value2 } = await call2.waitForResult(); + expect(value2).toStrictEqual(optionNone); + }); }); diff --git a/packages/fuel-gauge/src/predicate/predicate-input-data.test.ts b/packages/fuel-gauge/src/predicate/predicate-input-data.test.ts index 3e04ed4fb0e..dbbb192712e 100644 --- a/packages/fuel-gauge/src/predicate/predicate-input-data.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-input-data.test.ts @@ -1,4 +1,4 @@ -import { Predicate, Wallet } from 'fuels'; +import { Wallet } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; import { PredicateInputDataAbi__factory } from '../../test/typegen'; @@ -22,12 +22,7 @@ describe('Predicate', () => { const amountToPredicate = 200_000; const amountToReceiver = 50; - const predicate = new Predicate({ - bytecode: PredicateInputDataAbi__factory.bin, - abi: PredicateInputDataAbi__factory.abi, - provider, - inputData: [true], - }); + const predicate = PredicateInputDataAbi__factory.createInstance(provider); await fundPredicate(wallet, predicate, amountToPredicate); diff --git a/packages/fuel-gauge/src/script-with-options.test.ts b/packages/fuel-gauge/src/script-with-options.test.ts new file mode 100644 index 00000000000..a53f14e922c --- /dev/null +++ b/packages/fuel-gauge/src/script-with-options.test.ts @@ -0,0 +1,49 @@ +import type { BigNumberish } from 'fuels'; +import { bn } from 'fuels'; +import { launchTestNode } from 'fuels/test-utils'; + +import { ScriptWithOptionsAbi__factory } from '../test/typegen'; +import type { Option } from '../test/typegen/contracts/common'; + +/** + * @group node + * @group browser + */ +describe('Script With Options', () => { + it('should call script with optional arguments', async () => { + using launched = await launchTestNode(); + + const { + wallets: [wallet], + } = launched; + + const scriptWithOptions = ScriptWithOptionsAbi__factory.createInstance(wallet); + const expectedValue = true; + const OPTION_SOME: Option = bn(1); + const OPTION_NONE: Option = undefined; + + // Script with no inputs + const { waitForResult: call1 } = await scriptWithOptions.functions.main().call(); + const { value: value1 } = await call1(); + expect(value1).toBe(expectedValue); + + // Script with single input + const { waitForResult: call2 } = await scriptWithOptions.functions.main(OPTION_SOME).call(); + const { value: value2 } = await call2(); + expect(value2).toBe(expectedValue); + + // Script with three input + const { waitForResult: call3 } = await scriptWithOptions.functions + .main(OPTION_SOME, OPTION_SOME, OPTION_SOME) + .call(); + const { value: value3 } = await call3(); + expect(value3).toBe(expectedValue); + + // Script with mix of optional input + const { waitForResult: call4 } = await scriptWithOptions.functions + .main(OPTION_SOME, OPTION_NONE, OPTION_NONE) + .call(); + const { value: value4 } = await call4(); + expect(value4).toBe(expectedValue); + }); +}); diff --git a/packages/fuel-gauge/src/void.test.ts b/packages/fuel-gauge/src/void.test.ts new file mode 100644 index 00000000000..4f5a04b6b81 --- /dev/null +++ b/packages/fuel-gauge/src/void.test.ts @@ -0,0 +1,84 @@ +import { launchTestNode } from 'fuels/test-utils'; + +import { VoidAbi__factory } from '../test/typegen'; +import type { NativeEnumInput } from '../test/typegen/contracts/VoidAbi'; +import VoidAbiHex from '../test/typegen/contracts/VoidAbi.hex'; +import type { Option } from '../test/typegen/contracts/common'; + +/** + * @group node + * @group browser + */ +describe('Void Tests', () => { + const contractsConfigs = [ + { + deployer: VoidAbi__factory, + bytecode: VoidAbiHex, + }, + ]; + + it('should handle Option::None', async () => { + using launched = await launchTestNode({ contractsConfigs }); + const { + contracts: [voidContract], + } = launched; + + const optionNone: Option = undefined; + const { waitForResult } = await voidContract.functions.echo_void(optionNone).call(); + const { value } = await waitForResult(); + + expect(value).toEqual(optionNone); + }); + + it('should handle NativeEnum', async () => { + using launched = await launchTestNode({ contractsConfigs }); + + const { + contracts: [voidContract], + } = launched; + + const enumValue: NativeEnumInput = 'C' as NativeEnumInput; + + const { waitForResult } = await voidContract.functions.echo_native_enum(enumValue).call(); + const { value } = await waitForResult(); + + expect(value).toEqual(enumValue); + }); + + it('should handle input arguments of type [42, void]', async () => { + using launched = await launchTestNode({ contractsConfigs }); + + const { + contracts: [voidContract], + } = launched; + + const voidTypeValue: undefined = undefined; + + const { waitForResult: call1 } = await voidContract.functions.type_then_void(42).call(); + const { value: value1 } = await call1(); + expect(value1).toEqual(undefined); + + const { waitForResult: call2 } = await voidContract.functions + .type_then_void(42, undefined) + .call(); + const { value: value2 } = await call2(); + expect(value2).toEqual(voidTypeValue); + }); + + it('should handle input arguments of type [42, void, 43]', async () => { + using launched = await launchTestNode({ contractsConfigs }); + + const { + contracts: [voidContract], + } = launched; + + const voidTypeValue: undefined = undefined; + + const { waitForResult } = await voidContract.functions + .type_then_void_then_type(42, voidTypeValue, 43) + .call(); + const { value } = await waitForResult(); + + expect(value).toEqual(voidTypeValue); + }); +}); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index b017851d107..225e7cb7614 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml @@ -1,44 +1,44 @@ [workspace] members = [ + "advanced-logging", "advanced-logging-abi", - "advanced-logging-other-contract-abi", "advanced-logging-other-contract", - "advanced-logging", + "advanced-logging-other-contract-abi", "auth_testing_abi", "auth_testing_contract", "bytecode-sway-lib", "bytes", "call-test-contract", "collision_in_fn_names", + "complex-predicate", + "complex-script", "configurable-contract", "coverage-contract", "generic-types-contract", "multi-token-contract", + "options", "payable-annotation", "predicate-address", + "predicate-assert-number", + "predicate-assert-value", "predicate-bytes", "predicate-conditional-inputs", "predicate-false", + "predicate-input-data", "predicate-main-args-struct", "predicate-main-args-vector", "predicate-multi-args", "predicate-raw-slice", - "predicate-str-slice", "predicate-std-lib-string", - "predicate-input-data", + "predicate-str-slice", + "predicate-sum", "predicate-triple-sig", "predicate-true", "predicate-u32", + "predicate-validate-transfer", "predicate-vector-types", - "predicate-assert-value", - "predicate-assert-number", - "predicate-sum", "predicate-with-configurable", - "predicate-validate-transfer", - "complex-predicate", - "complex-script", "raw-slice", - "str-slice", "reentrant-bar", "reentrant-foo", "reentrant-foobar-abi", @@ -50,20 +50,22 @@ members = [ "script-main-return-struct", "script-main-two-args", "script-raw-slice", - "script-str-slice", "script-std-lib-string", + "script-str-slice", "script-with-array", "script-with-configurable", + "script-with-vector", + "script-with-options", "script-with-vector-advanced", "script-with-vector-mixed", - "script-with-vector", "small-bytes", "std-lib-string", "storage-test-contract", + "str-slice", "token-abi", "token-contract", "vector-types-contract", "vector-types-script", "vectors", - "options", + "void", ] 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 index 95cfda5a5ae..009e5bcc54c 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/options/src/main.sw +++ b/packages/fuel-gauge/test/fixtures/forc-projects/options/src/main.sw @@ -77,6 +77,8 @@ abi OptionContract { fn print_enum_option_array() -> GardenVector; fn echo_deeply_nested_option(arg: DeepStruct) -> DeepStruct; fn echo_enum_diff_sizes(arg: Option) -> Option; + fn type_then_option_then_type(x: u8, y: Option, z: u8) -> Option; + fn option_then_type_then_option(x: Option, y: u8, z: Option) -> Option; } impl OptionContract for Contract { @@ -120,4 +122,20 @@ impl OptionContract for Contract { fn echo_enum_diff_sizes(arg: Option) -> Option { arg } + + fn type_then_option_then_type(x: u8, y: Option, z: u8) -> Option { + assert_eq(x, 42); + assert_eq(y, Option::None); + assert_eq(z, 43); + + y + } + + fn option_then_type_then_option(x: Option, y: u8, z: Option) -> Option { + assert_eq(x, Option::None); + assert_eq(y, 42); + assert_eq(z, Option::None); + + z + } } diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/Forc.toml new file mode 100644 index 00000000000..06a9ef676ce --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "script-with-options" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/src/main.sw new file mode 100644 index 00000000000..b34aa4b2c8c --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-options/src/main.sw @@ -0,0 +1,5 @@ +script; + +fn main(x: Option, y: Option, z: Option) -> bool { + true +} diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/void/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/void/Forc.toml new file mode 100644 index 00000000000..dea821231ae --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/void/Forc.toml @@ -0,0 +1,4 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "void" diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/void/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/void/src/main.sw new file mode 100644 index 00000000000..d626dc64fe5 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/void/src/main.sw @@ -0,0 +1,45 @@ +contract; + +enum NativeEnum { + A: (), + B: (), + C: (), +} + +abi VoidContract { + fn echo_void(value: Option) -> Option; + fn echo_native_enum(value: NativeEnum) -> NativeEnum; + fn type_then_void(x: u8, y: ()) -> (); + fn type_then_void_then_type(x: u8, y: (), z: u8) -> (); +} + +impl VoidContract for Contract { + fn echo_void(value: Option) -> Option { + assert_eq(value, Option::None); + + value + } + + fn echo_native_enum(value: NativeEnum) -> NativeEnum { + match value { + NativeEnum::A => assert(false), + NativeEnum::B => assert(false), + NativeEnum::C => assert(true), + }; + + value + } + + fn type_then_void(x: u8, y: ()) -> () { + assert_eq(x, 42); + + y + } + + fn type_then_void_then_type(x: u8, y: (), z: u8) -> () { + assert_eq(x, 42); + assert_eq(z, 43); + + y + } +}