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 configurable constants contracts #991

Merged
merged 20 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/rich-actors-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/abi-coder": minor
"@fuel-ts/contract": minor
---

support configurable constants for contracts
3 changes: 2 additions & 1 deletion apps/docs-snippets/contracts/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ members = [
"log-values",
"echo-values",
"simple-token",
"simple-token-abi",
"return-context",
"token-depositor",
"simple-token-abi",
"echo-configurables",
"transfer-to-address",
]
7 changes: 7 additions & 0 deletions apps/docs-snippets/contracts/echo-configurables/Forc.toml
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 = "echo-configurables"

[dependencies]
35 changes: 35 additions & 0 deletions apps/docs-snippets/contracts/echo-configurables/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// #region configurable-constants-1
contract;

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

struct MyStruct {
x: u8,
y: u8,
state: MyEnum
}

configurable {
age: u8 = 25,
tag: str[4] = "fuel",
grades: [u8; 4] = [3, 4, 3, 2],
my_struct: MyStruct = MyStruct {
x: 1,
y: 2,
state: MyEnum::Pending
}
}

abi EchoConfigurables {
fn echo_configurables() -> (u8, str[4], [u8; 4], MyStruct);
}

impl EchoConfigurables for Contract {
fn echo_configurables() -> (u8, str[4], [u8; 4], MyStruct) {
(age, tag, grades, my_struct)
}
}
// #endregion configurable-constants-1
1 change: 1 addition & 0 deletions apps/docs-snippets/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum SnippetContractEnum {
SIMPLE_TOKEN = 'simple-token',
TOKEN_DEPOSITOR = 'token-depositor',
TRANSFER_TO_ADDRESS = 'transfer-to-address',
ECHO_CONFIGURABLES = 'echo-configurables',
}

const getSnippetContractPath = (contract: SnippetContractEnum) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { WalletUnlocked } from 'fuels';
import { ContractFactory } from 'fuels';

import { getSnippetContractArtifacts, SnippetContractEnum } from '../../../contracts';
import { getTestWallet } from '../../utils';

describe(__filename, () => {
let wallet: WalletUnlocked;

const { abi, bin } = getSnippetContractArtifacts(SnippetContractEnum.ECHO_CONFIGURABLES);

const defaultValues = {
age: 25,
tag: 'fuel',
grades: [3, 4, 3, 2],
my_struct: {
x: 1,
y: 2,
state: 'Pending',
},
};

beforeAll(async () => {
wallet = await getTestWallet();
});

it('should successfully set new values for all configurable constants', async () => {
// #region configurable-constants-2
const configurableConstants: typeof defaultValues = {
age: 30,
tag: 'leuf',
grades: [10, 9, 8, 9],
my_struct: {
x: 11,
y: 22,
state: 'Checked',
},
};

const factory = new ContractFactory(bin, abi, wallet);

const contract = await factory.deployContract({
configurableConstants,
});
// #endregion configurable-constants-2

const { value } = await contract.functions.echo_configurables().get();

expect(value[0]).toEqual(configurableConstants.age);
expect(value[1]).toEqual(configurableConstants.tag);
expect(value[2]).toStrictEqual(configurableConstants.grades);
expect(value[3]).toStrictEqual(configurableConstants.my_struct);
});

it('should successfully set new value for one configurable constant', async () => {
// #region configurable-constants-3
const configurableConstants = {
age: 10,
};

const factory = new ContractFactory(bin, abi, wallet);

const contract = await factory.deployContract({
configurableConstants,
});
// #endregion configurable-constants-3

const { value } = await contract.functions.echo_configurables().get();

expect(value[0]).toEqual(configurableConstants.age);
expect(value[1]).toEqual(defaultValues.tag);
expect(value[2]).toEqual(defaultValues.grades);
expect(value[3]).toEqual(defaultValues.my_struct);
});

it('should throw when not properly setting new values for structs', async () => {
// #region configurable-constants-4
const configurableConstants = {
my_struct: {
x: 2,
},
};

const factory = new ContractFactory(bin, abi, wallet);

await expect(factory.deployContract({ configurableConstants })).rejects.toThrowError();
// #endregion configurable-constants-4
});
});
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ export default defineConfig({
text: 'Calls With Different Wallets',
link: '/guide/contracts/calls-with-different-wallets',
},
{
text: 'Configurable Constants',
link: '/guide/contracts/configurable-constants',
},
{
text: 'Logs',
link: '/guide/contracts/logs',
Expand Down
33 changes: 33 additions & 0 deletions apps/docs/src/guide/contracts/configurable-constants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Configurable Constants

Sway introduces a powerful feature: configurable constants. When creating a contract, you can define constants, each assigned with a default value.

Before deploying the contract, you can then redefine the value for these constants, it can be all of them or as many as you need.

This feature provides flexibility for dynamic contract environments. It allows a high level of customization, leading to more efficient and adaptable smart contracts.

## Defining Configurable Constants

Below is an example of a contract in which we declare four configurable constants:

<<< @/../../docs-snippets/contracts/echo-configurables/src/main.sw#configurable-constants-1{rust:line-numbers}

In this contract, we have a function `echo_configurables` that returns the values of the configurable constants.

If each of these constants has new values that have been assigned to them, the function will return the updated values. Otherwise, the function will return the default values.

## Setting New Values For Configurable Constants

During contract deployment, you can define new values for the configurable constants. This is achieved as follows:

<<< @/../../docs-snippets/src/guide/contracts/configurable-constants.test.ts#configurable-constants-2{ts:line-numbers}

You can assign new values to any of these configurable constants.

If you wish to assign a new value to just one constant, you can do the following:

<<< @/../../docs-snippets/src/guide/contracts/configurable-constants.test.ts#configurable-constants-3{ts:line-numbers}

Please note that when assigning new values for a `Struct`, all properties of the `Struct` must be defined. Failing to do so will result in an error:

<<< @/../../docs-snippets/src/guide/contracts/configurable-constants.test.ts#configurable-constants-4{ts:line-numbers}
66 changes: 65 additions & 1 deletion packages/abi-coder/src/interface.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { configurableFragmentMock, jsonFlatAbiMock } from '../test/fixtures/mocks';

import FunctionFragment from './fragments/function-fragment';
import * as interfaceMod from './interface';
import Interface from './interface';
import type { ConfigurableFragment, JsonAbiFragment } from './json-abi';
import * as jsonAbiMod from './json-abi';

describe('Interface', () => {
const jsonFragment = {
afterEach(jest.restoreAllMocks);

const { convertConfigurablesToDict } = interfaceMod;

const jsonFragment: JsonAbiFragment = {
type: 'function',
inputs: [{ name: 'arg', type: 'u64' }],
name: 'entry_one',
outputs: [],
};

const fragment = FunctionFragment.fromObject(jsonFragment);
let functionInterface: Interface;

Expand Down Expand Up @@ -41,4 +51,58 @@ describe('Interface', () => {
'Types/values length mismatch'
);
});

it('should ensure `convertConfigurablesToDict` creates dictionary just fine', () => {
const result = convertConfigurablesToDict(configurableFragmentMock);

expect(configurableFragmentMock.length).toBeGreaterThan(0);

configurableFragmentMock.forEach((value, i) => {
expect(result[value.name]).toStrictEqual(configurableFragmentMock[i]);
});
});

it('should ensure constructor calls `convertConfigurablesToDict` (CALLED W/ JsonAbiFragment)', () => {
const abiUnflattenConfigurablesSpy = jest
.spyOn(jsonAbiMod.ABI.prototype, 'unflattenConfigurables')
.mockImplementation(() => configurableFragmentMock);

const mockedConfigurableDict: { [name: string]: ConfigurableFragment } = {
dummy: configurableFragmentMock[0],
};
const convertConfigurablesToDictSpy = jest
.spyOn(interfaceMod, 'convertConfigurablesToDict')
.mockReturnValue(mockedConfigurableDict);

const abiInterface = new Interface([jsonFragment]);

expect(abiUnflattenConfigurablesSpy).not.toHaveBeenCalled();

expect(convertConfigurablesToDictSpy).toHaveBeenCalledTimes(1);
expect(convertConfigurablesToDictSpy).toHaveBeenCalledWith([]);

expect(abiInterface.configurables).toStrictEqual(mockedConfigurableDict);
});

it('should ensure constructor calls `convertConfigurablesToDict` (CALLED W/ JsonFlatAbi)', () => {
const abiUnflattenConfigurablesSpy = jest
.spyOn(jsonAbiMod.ABI.prototype, 'unflattenConfigurables')
.mockImplementation(() => configurableFragmentMock);

const mockedConfigurableDict: { [name: string]: ConfigurableFragment } = {
dummy: configurableFragmentMock[1],
};
const convertConfigurablesToDictSpy = jest
.spyOn(interfaceMod, 'convertConfigurablesToDict')
.mockReturnValue(mockedConfigurableDict);

const abiInterface = new Interface(jsonFlatAbiMock);

expect(abiUnflattenConfigurablesSpy).toHaveBeenCalledTimes(1);

expect(convertConfigurablesToDictSpy).toHaveBeenCalledTimes(1);
expect(convertConfigurablesToDictSpy).toHaveBeenCalledWith(configurableFragmentMock);

expect(abiInterface.configurables).toStrictEqual(mockedConfigurableDict);
});
});
16 changes: 16 additions & 0 deletions packages/abi-coder/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
JsonFlatAbiFragmentType,
JsonAbi,
JsonAbiLogFragment,
ConfigurableFragment,
} from './json-abi';
import { isFlatJsonAbi, ABI } from './json-abi';

Expand All @@ -31,9 +32,22 @@ const coerceFragments = (value: ReadonlyArray<JsonAbiFragment>): Array<Fragment>
return fragments;
};

export const convertConfigurablesToDict = (value: ReadonlyArray<ConfigurableFragment>) => {
const configurables: { [name: string]: ConfigurableFragment } = {};

value.forEach((v) => {
configurables[v.name] = {
...v,
};
});

return configurables;
};

export default class Interface {
readonly fragments: Array<Fragment>;
readonly functions: { [name: string]: FunctionFragment };
readonly configurables: { [name: string]: ConfigurableFragment };
readonly abiCoder: AbiCoder;
readonly abi: ABI | null;
readonly types: ReadonlyArray<JsonFlatAbiFragmentType>;
Expand All @@ -57,6 +71,8 @@ export default class Interface {
this.abiCoder = new AbiCoder();
this.functions = {};

this.configurables = convertConfigurablesToDict(this.abi?.unflattenConfigurables() || []);

this.fragments.forEach((fragment) => {
if (fragment instanceof FunctionFragment) {
const signature = fragment.getSignature();
Expand Down
34 changes: 33 additions & 1 deletion packages/abi-coder/src/json-abi.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
import { isPointerType } from './json-abi';
import { jsonFlatAbiMock } from '../test/fixtures/mocks';

import type { JsonAbiFragmentType } from './json-abi';
import { ABI, isPointerType } from './json-abi';

describe('JSON ABI', () => {
afterEach(jest.restoreAllMocks);

it.each(['u8', 'u16', 'u32', 'u64', 'bool'])(
`should return false when it's not reference type`,
(type) => {
expect(isPointerType(type)).toBeFalsy();
}
);

it.each(['str[3]', 'b256', '[str[3]; 3]', 'struct MyStruct', 'enum MyEnum'])(
`should return true when it's reference type`,
(type) => {
expect(isPointerType(type)).toBeTruthy();
}
);

it('should ensure `unflattenConfigurables` adds `fragmentType` to each `configurables` entry', () => {
const abi = new ABI(jsonFlatAbiMock);

const mockedValue: JsonAbiFragmentType = {
type: 'dummy',
name: 'dummy',
components: null,
typeArguments: null,
};

const spy = jest.spyOn(abi, 'parseInput').mockReturnValue(mockedValue);

const result = abi.unflattenConfigurables();

const expected1 = { ...abi.configurables[0], fragmentType: mockedValue };
const expected2 = { ...abi.configurables[1], fragmentType: mockedValue };

expect(result[0]).toStrictEqual(expected1);
expect(result[1]).toStrictEqual(expected2);

expect(spy).toHaveBeenCalledWith(jsonFlatAbiMock.configurables[0].configurableType);
expect(spy).toHaveBeenCalledWith(jsonFlatAbiMock.configurables[1].configurableType);

expect(spy).toHaveBeenCalledTimes(2);
});
});
Loading