Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support mixed native and non-native enums #2501

Merged
merged 8 commits into from
Jun 13, 2024
5 changes: 5 additions & 0 deletions .changeset/perfect-shrimps-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/abi-coder": patch
---

feat: support mixed native and non-native enums
32 changes: 32 additions & 0 deletions packages/abi-coder/src/encoding/coders/EnumCoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { U64_MAX } from '../../../test/utils/constants';
import { BigNumberCoder } from './BigNumberCoder';
import { BooleanCoder } from './BooleanCoder';
import { EnumCoder } from './EnumCoder';
import { TupleCoder } from './TupleCoder';

/**
* @group node
Expand All @@ -32,6 +33,19 @@ describe('EnumCoder', () => {
expect(actual).toStrictEqual(expected);
});

it('encodes a native enum', () => {
const nativeCoder = new EnumCoder('Native', {
One: new BooleanCoder(),
Two: new BigNumberCoder('u64'),
Three: new TupleCoder([]),
});

const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2]);
// @ts-expect-error native
const actual = nativeCoder.encode('Three');
expect(actual).toStrictEqual(expected);
});

it('should throw an error when encoding if no enum key is provided', async () => {
await expectToThrowFuelError(
() => coder.encode({} as never),
Expand Down Expand Up @@ -72,6 +86,24 @@ describe('EnumCoder', () => {
expect(actualLength).toBe(expectedLength);
});

it('decodes a native enum', () => {
const nativeCoder = new EnumCoder('TestEnum', {
a: new BooleanCoder(),
b: new BigNumberCoder('u64'),
c: new TupleCoder([]),
});

const expectedValue = 'c';
const expectedLength = 8;
const [actualValue, actualLength] = nativeCoder.decode(
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2]),
0
);

expect(actualValue).toStrictEqual(expectedValue);
expect(actualLength).toBe(expectedLength);
});

it('should throw an error when decoded value accesses an invalid index', async () => {
const input = new Uint8Array(Array.from(Array(8).keys()));

Expand Down
19 changes: 12 additions & 7 deletions packages/abi-coder/src/encoding/coders/EnumCoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 All @@ -17,12 +18,6 @@ export type DecodedValueOf<TCoders extends Record<string, Coder>> = RequireExact
[P in keyof TCoders]: TypesOfCoder<TCoders[P]>['Decoded'];
}>;

const isFullyNativeEnum = (enumCoders: { [s: string]: unknown } | ArrayLike<unknown>): boolean =>
Object.values(enumCoders).every(
// @ts-expect-error complicated types
({ type, coders }) => type === '()' && JSON.stringify(coders) === JSON.stringify([])
);

export class EnumCoder<TCoders extends Record<string, Coder>> extends Coder<
InputValueOf<TCoders>,
DecodedValueOf<TCoders>
Expand All @@ -47,6 +42,16 @@ 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.
#isNativeEnum(coder: Coder): boolean {
if (this.type !== OPTION_CODER_TYPE && coder.type === '()') {
const tupleCoder = coder as TupleCoder<[]>;
return tupleCoder.coders.length === 0;
}
return false;
}

#encodeNativeEnum(value: string): Uint8Array {
const valueCoder = this.coders[value];
const encodedValue = valueCoder.encode([]);
Expand Down Expand Up @@ -112,7 +117,7 @@ export class EnumCoder<TCoders extends Record<string, Coder>> extends Coder<

const [decoded, newOffset] = valueCoder.decode(data, offsetAndCase);

if (isFullyNativeEnum(this.coders)) {
if (this.#isNativeEnum(this.coders[caseKey])) {
return this.#decodeNativeEnum(caseKey, newOffset);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/fuel-gauge/src/coverage-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ enum ColorEnumOutput {
Blue = 'Blue',
}

enum MixedNativeEnum {
Native = 'Native',
NotNative = 12,
}

/**
* @group node
*/
Expand Down Expand Up @@ -556,6 +561,22 @@ describe('Coverage Contract', () => {
expect(value).toStrictEqual(OUTPUT);
});

it('should test mixed native enum [Native->NotNative]', async () => {
const input = MixedNativeEnum.Native;
const expected = { NotNative: MixedNativeEnum.NotNative };

const { value } = await contractInstance.functions.mixed_native_enum(input).call();
expect(value).toStrictEqual(expected);
});

it('should test mixed native enum [NotNative->Native]', async () => {
const input = { NotNative: MixedNativeEnum.NotNative };
const expected = 'Native';

const { value } = await contractInstance.functions.mixed_native_enum(input).call();
expect(value).toStrictEqual(expected);
});

danielbate marked this conversation as resolved.
Show resolved Hide resolved
it('should try vec_as_only_param', async () => {
const { value } = await contractInstance.functions
.vec_as_only_param([100, 450, 202, 340])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ pub enum ColorEnum {
Blue: (),
}

pub enum MixedNativeEnum {
Native: (),
NotNative: u32,
}

enum MyContractError {
DivisionByZero: (),
}
Expand Down Expand Up @@ -109,6 +114,7 @@ abi CoverageContract {
fn echo_struct_vector_first(vector: Vec<BigStruct>) -> BigStruct;
fn echo_struct_vector_last(vector: Vec<ComplexStruct>) -> ComplexStruct;
fn color_enum(input: ColorEnum) -> ColorEnum;
fn mixed_native_enum(input: MixedNativeEnum) -> MixedNativeEnum;
fn vec_as_only_param(input: Vec<u64>) -> (u64, Option<u64>, Option<u64>, Option<u64>);
fn u32_and_vec_params(foo: u32, input: Vec<u64>) -> (u64, Option<u64>, Option<u64>, Option<u64>);
fn vec_in_vec(arg: Vec<Vec<u32>>);
Expand Down Expand Up @@ -408,6 +414,13 @@ impl CoverageContract for Contract {
}
}

fn mixed_native_enum(input: MixedNativeEnum) -> MixedNativeEnum {
match input {
MixedNativeEnum::Native => MixedNativeEnum::NotNative(12),
MixedNativeEnum::NotNative => MixedNativeEnum::Native,
}
}

fn vec_as_only_param(input: Vec<u64>) -> (u64, Option<u64>, Option<u64>, Option<u64>) {
(input.len(), input.get(0), input.get(1), input.get(2))
}
Expand Down
Loading