Skip to content

Commit

Permalink
feat!: improve () and Option<T> type handling (#2777)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
petertonysmith94 and maschad committed Jul 30, 2024
1 parent 9156c02 commit 9c07b00
Show file tree
Hide file tree
Showing 35 changed files with 825 additions and 211 deletions.
6 changes: 6 additions & 0 deletions .changeset/tender-birds-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/abi-coder": patch
"@fuel-ts/abi-typegen": patch
---

feat!: improve `()` and `Option<T>` type handling
31 changes: 21 additions & 10 deletions apps/docs-snippets/src/guide/types/options.test.ts
Original file line number Diff line number Diff line change
@@ -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<u8>
// #region options-3
Expand All @@ -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;

Expand Down
81 changes: 19 additions & 62 deletions packages/abi-coder/src/FunctionFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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,
Expand Down
12 changes: 3 additions & 9 deletions packages/abi-coder/src/encoding/coders/EnumCoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TCoders extends Record<string, Coder>> = RequireExactlyOne<{
[P in keyof TCoders]: TypesOfCoder<TCoders[P]>['Input'];
Expand Down Expand Up @@ -42,14 +41,9 @@ export class EnumCoder<TCoders extends Record<string, Coder>> 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 {
Expand Down
38 changes: 38 additions & 0 deletions packages/abi-coder/src/encoding/coders/VoidCoder.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
17 changes: 17 additions & 0 deletions packages/abi-coder/src/encoding/coders/VoidCoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { VOID_TYPE } from '../../utils/constants';

import { Coder } from './AbstractCoder';

export class VoidCoder extends Coder<undefined, undefined> {
constructor() {
super('void', VOID_TYPE, 0);
}

encode(_value: undefined): Uint8Array {
return new Uint8Array([]);
}

decode(_data: Uint8Array, offset: number): [undefined, number] {
return [undefined, offset];
}
}
4 changes: 4 additions & 0 deletions packages/abi-coder/src/encoding/strategies/getCoderV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
U64_CODER_TYPE,
U8_CODER_TYPE,
VEC_CODER_TYPE,
VOID_TYPE,
arrayRegEx,
enumRegEx,
stringRegEx,
Expand All @@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/abi-coder/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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\[(?<length>[0-9]+)\]/;
export const arrayRegEx = /\[(?<item>[\w\s\\[\]]+);\s*(?<length>[0-9]+)\]/;
export const structRegEx = /^struct (?<name>\w+)$/;
Expand Down
Loading

0 comments on commit 9c07b00

Please sign in to comment.