Skip to content

Commit

Permalink
Merge branch 'torres/docs/add-dedicated-package-code-snippets-contrac…
Browse files Browse the repository at this point in the history
…ts' into dp/estimate-tx-deps
  • Loading branch information
arboleya authored Apr 25, 2023
2 parents 50f0837 + ee46b7c commit 67653fe
Show file tree
Hide file tree
Showing 18 changed files with 228 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .changeset/calm-geckos-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/abi-coder": minor
"@fuel-ts/abi-typegen": minor
---

Added improved enum support using TypeScript enums
28 changes: 28 additions & 0 deletions packages/abi-coder/src/coders/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ 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 default class EnumCoder<TCoders extends Record<string, Coder>> extends Coder<
InputValueOf<TCoders>,
DecodedValueOf<TCoders>
Expand All @@ -35,7 +41,20 @@ export default class EnumCoder<TCoders extends Record<string, Coder>> extends Co
this.#encodedValueSize = encodedValueSize;
}

#encodeNativeEnum(value: string): Uint8Array {
const valueCoder = this.coders[value];
const encodedValue = valueCoder.encode([]);
const caseIndex = Object.keys(this.coders).indexOf(value);

const padding = new Uint8Array(this.#encodedValueSize - valueCoder.encodedLength);
return concat([this.#caseIndexCoder.encode(caseIndex), padding, encodedValue]);
}

encode(value: InputValueOf<TCoders>): Uint8Array {
if (typeof value === 'string' && this.coders[value]) {
return this.#encodeNativeEnum(value);
}

const [caseKey, ...empty] = Object.keys(value);
if (!caseKey) {
throw new Error('A field for the case must be provided');
Expand All @@ -51,6 +70,10 @@ export default class EnumCoder<TCoders extends Record<string, Coder>> extends Co
return concat([this.#caseIndexCoder.encode(caseIndex), padding, encodedValue]);
}

#decodeNativeEnum(caseKey: string, newOffset: number): [DecodedValueOf<TCoders>, number] {
return [caseKey as unknown as DecodedValueOf<TCoders>, newOffset];
}

decode(data: Uint8Array, offset: number): [DecodedValueOf<TCoders>, number] {
let newOffset = offset;

Expand All @@ -61,11 +84,16 @@ export default class EnumCoder<TCoders extends Record<string, Coder>> extends Co
if (!caseKey) {
throw new Error(`Invalid caseIndex "${caseIndex}". Valid cases: ${Object.keys(this.coders)}`);
}

const valueCoder = this.coders[caseKey];
const padding = this.#encodedValueSize - valueCoder.encodedLength;
newOffset += padding;
[decoded, newOffset] = valueCoder.decode(data, newOffset);

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

return [{ [caseKey]: decoded } as DecodedValueOf<TCoders>, newOffset];
}
}
28 changes: 28 additions & 0 deletions packages/abi-typegen/src/abi/types/EnumType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@ describe('EnumType.ts', () => {
expect(outputs).toEqual('Checked: [], Pending: []');
});

test('should properly parse type attributes for: use native TS enum on a simple enum', () => {
const { types } = getTypesForContract(ForcProjectsEnum.ENUM_SIMPLE_NATIVE);

const myEnum = findType({ types, typeId: 1 }) as EnumType;

validateCommonEnumAttributes({ enum: myEnum });

const inputs = myEnum.getNativeEnum({ types });
const outputs = myEnum.getNativeEnum({ types });

expect(inputs).toEqual("Checked = 'Checked', Pending = 'Pending'");
expect(outputs).toEqual("Checked = 'Checked', Pending = 'Pending'");
});

test('should properly parse type attributes for: use SDK enum on a complex enum', () => {
const { types } = getTypesForContract(ForcProjectsEnum.ENUM_OF_STRUCTS);

const myEnum = findType({ types, typeId: 0 }) as EnumType;

validateCommonEnumAttributes({ enum: myEnum });

const inputs = myEnum.getNativeEnum({ types });
const outputs = myEnum.getNativeEnum({ types });

expect(inputs).toEqual(undefined);
expect(outputs).toEqual(undefined);
});

test('should properly parse type attributes for: enums of enums', () => {
const { types } = getTypesForContract(ForcProjectsEnum.ENUM_OF_ENUMS);

Expand Down
23 changes: 23 additions & 0 deletions packages/abi-typegen/src/abi/types/EnumType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ export class EnumType extends AType implements IType {
return name;
}

public getNativeEnum(params: { types: IType[] }) {
const { types } = params;

const typeHash: { [key: number]: IType['rawAbiType'] } = types.reduce(
(hash, row) => ({
...hash,
[row.rawAbiType.typeId]: row,
}),
{}
);

const { components } = this.rawAbiType;

// `components` array guaranteed to always exist for structs/enums
const enumComponents = components as IRawAbiTypeComponent[];

if (!enumComponents.every(({ type }) => !typeHash[type])) {
return undefined;
}

return enumComponents.map(({ name }) => `${name} = '${name}'`).join(', ');
}

public getStructContents(params: { types: IType[]; target: TargetEnum }) {
const { types, target } = params;

Expand Down
13 changes: 11 additions & 2 deletions packages/abi-typegen/src/templates/contract/dts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ import type {
import type { {{commonTypesInUse}} } from "./common";
{{/if}}


{{#each enums}}
{{#if inputNativeValues}}
export enum {{structName}}Input { {{inputNativeValues}} };
{{else}}
export type {{structName}}Input = Enum<{ {{inputValues}} }>;
{{#if recycleRef}}
export type {{structName}}Output = {{structName}}Input;
{{/if}}
{{#if outputNativeValues}}
export enum {{structName}}Output { {{outputNativeValues}} };
{{else}}
{{#if recycleRef}}
export type {{structName}}Output = {{structName}}Input;
{{else}}
export type {{structName}}Output = Enum<{ {{outputValues}} }>;
{{/if}}
{{/if}}
{{/each}}

Expand Down
17 changes: 17 additions & 0 deletions packages/abi-typegen/src/templates/contract/dts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,21 @@ describe('templates/dts', () => {
const rendered = renderDtsTemplate({ abi });
expect(rendered).toMatch(/export type MyEnumOutput = MyEnumInput;$/m);
});

test('should not render same value for native identical enums', () => {
const project = getProjectResources(ForcProjectsEnum.ENUM_SIMPLE_NATIVE);
const { abiContents: rawContents } = project;

const abi = new Abi({
filepath: './my-contract-abi.json',
outputDir: 'stdout',
rawContents,
programType: ProgramTypeEnum.CONTRACT,
});

const rendered = renderDtsTemplate({ abi });
expect(rendered).toMatch(
/export enum MyEnumOutput { Checked = 'Checked', Pending = 'Pending' };$/m
);
});
});
12 changes: 10 additions & 2 deletions packages/abi-typegen/src/templates/predicate/factory.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ import type { {{commonTypesInUse}} } from "./common";


{{#each enums}}
{{#if inputNativeValues}}
export enum {{structName}}Input { {{inputNativeValues}} };
{{else}}
export type {{structName}}Input = Enum<{ {{inputValues}} }>;
{{#if recycleRef}}
export type {{structName}}Output = {{structName}}Input;
{{/if}}
{{#if outputNativeValues}}
export enum {{structName}}Output { {{outputNativeValues}} };
{{else}}
{{#if recycleRef}}
export type {{structName}}Output = {{structName}}Input;
{{else}}
export type {{structName}}Output = Enum<{ {{outputValues}} }>;
{{/if}}
{{/if}}
{{/each}}

Expand Down
12 changes: 10 additions & 2 deletions packages/abi-typegen/src/templates/script/factory.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ import type { {{commonTypesInUse}} } from "./common";


{{#each enums}}
{{#if inputNativeValues}}
export enum {{structName}}Input { {{inputNativeValues}} };
{{else}}
export type {{structName}}Input = Enum<{ {{inputValues}} }>;
{{#if recycleRef}}
export type {{structName}}Output = {{structName}}Input;
{{/if}}
{{#if outputNativeValues}}
export enum {{structName}}Output { {{outputNativeValues}} };
{{else}}
{{#if recycleRef}}
export type {{structName}}Output = {{structName}}Input;
{{else}}
export type {{structName}}Output = Enum<{ {{outputValues}} }>;
{{/if}}
{{/if}}
{{/each}}

Expand Down
4 changes: 4 additions & 0 deletions packages/abi-typegen/src/templates/utils/formatEnums.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ describe('formatEnums.ts', () => {
expect(enums).toStrictEqual([
{
structName: 'LetterEnum',
inputNativeValues: "a = 'a', b = 'b', c = 'c'",
inputValues: 'a: [], b: [], c: []',
outputNativeValues: "a = 'a', b = 'b', c = 'c'",
outputValues: 'a: [], b: [], c: []',
recycleRef: true,
},
{
structName: 'MyEnum',
inputNativeValues: undefined,
inputValues: 'letter: LetterEnumInput',
outputNativeValues: undefined,
outputValues: 'letter: LetterEnumOutput',
recycleRef: false,
},
Expand Down
5 changes: 5 additions & 0 deletions packages/abi-typegen/src/templates/utils/formatEnums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ export function formatEnums(params: { types: IType[] }) {
const structName = et.getStructName();
const inputValues = et.getStructContents({ types, target: TargetEnum.INPUT });
const outputValues = et.getStructContents({ types, target: TargetEnum.OUTPUT });
const inputNativeValues = et.getNativeEnum({ types });
const outputNativeValues = et.getNativeEnum({ types });

return {
structName,
inputValues,
outputValues,
recycleRef: inputValues === outputValues, // reduces duplication
inputNativeValues,
outputNativeValues,
};
});

Expand Down
1 change: 1 addition & 0 deletions packages/abi-typegen/test/fixtures/forc-projects/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"./enum-of-enums",
"./enum-of-structs",
"./enum-simple",
"./enum-simple-native",
"./fn-void",
"./full",
"./minimal",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["FuelLabs"]
entry = "main.sw"
license = "Apache-2.0"
name = "enum-simple-native"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
contract;

enum MyEnum {
Checked: (),
Pending: ()
}


abi MyContract {
fn main(x: MyEnum) -> MyEnum;
}

impl MyContract for Contract {
fn main(x: MyEnum) -> MyEnum { x }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
contract;

enum MyEnum {
Checked: (),
Pending: ()
Checked: b256,
Pending: b256
}


Expand Down
1 change: 1 addition & 0 deletions packages/abi-typegen/test/fixtures/forc-projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum ForcProjectsEnum {
ENUM_OF_ENUMS = 'enum-of-enums',
ENUM_OF_STRUCTS = 'enum-of-structs',
ENUM_SIMPLE = 'enum-simple',
ENUM_SIMPLE_NATIVE = 'enum-simple-native',
FN_VOID = 'fn-void',
FULL = 'full',
MINIMAL = 'minimal',
Expand Down
4 changes: 2 additions & 2 deletions packages/abi-typegen/test/fixtures/templates/contract/dts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import type {

import type { Option, Enum, Vec } from "./common";

export type MyEnumInput = Enum<{ Checked: [], Pending: [] }>;
export type MyEnumOutput = MyEnumInput;
export enum MyEnumInput { Checked = 'Checked', Pending = 'Pending' };
export enum MyEnumOutput { Checked = 'Checked', Pending = 'Pending' };

export type MyStructInput = { x: BigNumberish, y: BigNumberish, state: MyEnumInput };
export type MyStructOutput = { x: number, y: number, state: MyEnumOutput };
Expand Down
47 changes: 43 additions & 4 deletions packages/fuel-gauge/src/coverage-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ beforeAll(async () => {
contractInstance = await setupContract();
});

enum SmallEnum {
Empty = 'Empty',
}

enum ColorEnumInput {
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}
enum ColorEnumOutput {
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}

describe('Coverage Contract', () => {
it('can return outputs', async () => {
// Call contract methods
Expand All @@ -42,9 +57,9 @@ describe('Coverage Contract', () => {
bar: 42,
});
expect((await contractInstance.functions.get_large_array().call()).value).toStrictEqual([1, 2]);
expect((await contractInstance.functions.get_empty_enum().call()).value).toStrictEqual({
Empty: [],
});
expect((await contractInstance.functions.get_empty_enum().call()).value).toStrictEqual(
SmallEnum.Empty
);
expect((await contractInstance.functions.get_contract_id().call()).value).toStrictEqual({
value: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
});
Expand Down Expand Up @@ -178,7 +193,7 @@ describe('Coverage Contract', () => {

it('should test enum < 8 byte variable type', async () => {
// #region Enum-small
const INPUT = { Empty: [] };
const INPUT = SmallEnum.Empty;
// #endregion Enum-small
const { value } = await contractInstance.functions.echo_enum_small(INPUT).call();
expect(value).toStrictEqual(INPUT);
Expand Down Expand Up @@ -474,6 +489,30 @@ describe('Coverage Contract', () => {
expect(value.map((v: BN) => v.toNumber())).toStrictEqual([1, 2, 3]);
});

it('should test native enum [Red->Green]', async () => {
const INPUT: ColorEnumInput = ColorEnumInput.Red;
const OUTPUT: ColorEnumOutput = ColorEnumOutput.Green;
const { value } = await contractInstance.functions.color_enum(INPUT).call();

expect(value).toStrictEqual(OUTPUT);
});

it('should test native enum [Green->Blue]', async () => {
const INPUT: ColorEnumInput = ColorEnumInput.Green;
const OUTPUT: ColorEnumOutput = ColorEnumOutput.Blue;

const { value } = await contractInstance.functions.color_enum(INPUT).call();
expect(value).toStrictEqual(OUTPUT);
});

it('should test native enum [Blue->Red]', async () => {
const INPUT: ColorEnumInput = ColorEnumInput.Blue;
const OUTPUT: ColorEnumOutput = ColorEnumOutput.Red;

const { value } = await contractInstance.functions.color_enum(INPUT).call();
expect(value).toStrictEqual(OUTPUT);
});

it('should try vec_as_only_param', async () => {
const { value } = await contractInstance.functions
.vec_as_only_param([100, 450, 202, 340])
Expand Down
Loading

0 comments on commit 67653fe

Please sign in to comment.