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: auto-loading storage slots based on ABI filepath #1346

Merged
merged 12 commits into from
Oct 17, 2023
5 changes: 5 additions & 0 deletions .changeset/gold-insects-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/abi-typegen": patch
---

Auto-loading `*-storage_slots.json` based on `*-abi.json` filepaths
7 changes: 1 addition & 6 deletions apps/demo-typegen/src/demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { generateTestWallet } from '@fuel-ts/wallet/test-utils';
import type { BN } from 'fuels';
import { ContractFactory, Provider, toHex, BaseAssetId, Wallet, FUEL_NETWORK_URL } from 'fuels';

import storageSlots from '../contract/out/debug/demo-contract-storage_slots.json';

import { DemoContractAbi__factory } from './generated-types';
import bytecode from './generated-types/DemoContractAbi.hex';

Expand Down Expand Up @@ -43,10 +41,7 @@ describe('ExampleContract', () => {
const wallet = await generateTestWallet(provider, [[500_000, BaseAssetId]]);

// Deploy
const contract = await DemoContractAbi__factory.deployContract(bytecode, wallet, {
gasPrice,
storageSlots,
});
const contract = await DemoContractAbi__factory.deployContract(bytecode, wallet, { gasPrice });

// Call
const { value } = await contract.functions.return_input(1337).txParams({ gasPrice }).call();
Expand Down
12 changes: 8 additions & 4 deletions apps/docs/src/guide/abi-typegen/using-generated-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const { transactionId, value } = await contract.functions.my_fn(1).call();
console.log(transactionId, value);
```

## Using the Generated Contract Factory to Deploy a Contract
## Contract

Let's use the Contract class to deploy a contract:

```ts
import { Wallet } from "fuels";
Expand All @@ -40,7 +42,9 @@ const contract = await MyContract__factory.deployContract(bytecode, wallet);
console.log(contract.id);
```

You can also pass in [`DeployContractOptions`](https://github.com/FuelLabs/fuels-ts/blob/a64b67b9fb2d7f764ab9151a21d2266bf2df3643/packages/contract/src/contract-factory.ts#L19-L24) like storage slots and configurable constants to the `deployContract` method:
### Autoloading of Storage Slots

Typegen tries to resolve, auto-load, and embed the [Storage Slots](../contracts//storage-slots.md) for your Contract within the `MyContract__factory` class. Still, you can override it alongside other options from [`DeployContractOptions`](https://github.com/FuelLabs/fuels-ts/blob/a64b67b9fb2d7f764ab9151a21d2266bf2df3643/packages/contract/src/contract-factory.ts#L19-L24), when calling the `deployContract` method:

```ts
import storageSlots from "../contract/out/debug/storage-slots.json";
Expand All @@ -50,7 +54,7 @@ const contract = await MyContract__factory.deployContract(bytecode, wallet, {
});
```

## Using Generated Script Types
## Script

After generating types via:

Expand All @@ -74,7 +78,7 @@ const { value, logs } = await script.functions.main(1).call();
console.log({ value, logs });
```

## Using Generated Predicate Types
## Predicate

Consider the following predicate:

Expand Down
6 changes: 6 additions & 0 deletions apps/docs/src/guide/contracts/storage-slots.md
arboleya marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ When deploying a contract, you can specify the custom storage slots that you wan

<<< @/../../../packages/fuel-gauge/src/storage-test-contract.test.ts#contract-deployment-storage-slots{ts:line-numbers}

## Using plain Javascript

In the above example, we directly imported the storage slots from a JSON file generated by the Sway compiler.

Instead of importing from a file, you can also specify the custom storage slots directly in your code:

<<< @/../../../packages/fuel-gauge/src/storage-test-contract.test.ts#contract-deployment-storage-slots-inline{ts:line-numbers}

## Auto-load of Storage Slots

Code generated using [Typegen](../abi-typegen//generating-types-from-abi.md) automatically [load](../abi-typegen/using-generated-types.md#autoloading-of-storage-slots) Storage Slots for you.
11 changes: 10 additions & 1 deletion packages/abi-typegen/src/AbiTypeGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,36 @@ export class AbiTypeGen {
public readonly abis: Abi[];
public readonly abiFiles: IFile[];
public readonly binFiles: IFile[];
public readonly storageSlotsFiles: IFile[];
public readonly outputDir: string;

public readonly files: IFile[];

constructor(params: {
abiFiles: IFile[];
binFiles: IFile[];
storageSlotsFiles: IFile[];
outputDir: string;
programType: ProgramTypeEnum;
}) {
const { abiFiles, binFiles, outputDir, programType } = params;
const { abiFiles, binFiles, outputDir, programType, storageSlotsFiles } = params;

this.outputDir = outputDir;

this.abiFiles = abiFiles;
this.binFiles = binFiles;
this.storageSlotsFiles = storageSlotsFiles;

// Creates a `Abi` for each abi file
this.abis = this.abiFiles.map((abiFile) => {
const binFilepath = abiFile.path.replace('-abi.json', '.bin');
const relatedBinFile = this.binFiles.find(({ path }) => path === binFilepath);

const storageSlotFilepath = abiFile.path.replace('-abi.json', '-storage_slots.json');
const relatedStorageSlotsFile = this.storageSlotsFiles.find(
({ path }) => path === storageSlotFilepath
);

if (!relatedBinFile) {
validateBinFile({
abiFilepath: abiFile.path,
Expand All @@ -50,6 +58,7 @@ export class AbiTypeGen {
filepath: abiFile.path,
rawContents: JSON.parse(abiFile.contents as string),
hexlifiedBinContents: relatedBinFile?.contents,
storageSlotsContents: relatedStorageSlotsFile?.contents,
outputDir,
programType,
});
Expand Down
12 changes: 11 additions & 1 deletion packages/abi-typegen/src/abi/Abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class Abi {

public rawContents: IRawAbi;
public hexlifiedBinContents?: string;
public storageSlotsContents?: string;

public types: IType[];
public functions: IFunction[];
Expand All @@ -34,9 +35,17 @@ export class Abi {
programType: ProgramTypeEnum;
rawContents: IRawAbi;
hexlifiedBinContents?: string;
storageSlotsContents?: string;
outputDir: string;
}) {
const { filepath, outputDir, rawContents, hexlifiedBinContents, programType } = params;
const {
filepath,
outputDir,
rawContents,
hexlifiedBinContents,
programType,
storageSlotsContents,
} = params;

const abiNameRegex = /([^/]+)-abi\.json$/m;
const abiName = filepath.match(abiNameRegex);
Expand All @@ -58,6 +67,7 @@ export class Abi {
this.filepath = filepath;
this.rawContents = rawContents;
this.hexlifiedBinContents = hexlifiedBinContents;
this.storageSlotsContents = storageSlotsContents;
this.outputDir = outputDir;

const { types, functions, configurables } = this.parse();
Expand Down
4 changes: 4 additions & 0 deletions packages/abi-typegen/src/runTypegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AbiTypeGen } from './AbiTypeGen';
import type { ProgramTypeEnum } from './types/enums/ProgramTypeEnum';
import type { IFile } from './types/interfaces/IFile';
import { collectBinFilepaths } from './utils/collectBinFilePaths';
import { collectStorageSlotsFilepaths } from './utils/collectStorageSlotsFilePaths';

export interface IGenerateFilesParams {
cwd: string;
Expand Down Expand Up @@ -58,13 +59,16 @@ export function runTypegen(params: IGenerateFilesParams) {

const binFiles = collectBinFilepaths({ filepaths, programType });

const storageSlotsFiles = collectStorageSlotsFilepaths({ filepaths, programType });

/*
Starting the engine
*/
const abiTypeGen = new AbiTypeGen({
outputDir: output,
abiFiles,
binFiles,
storageSlotsFiles,
programType,
});

Expand Down
22 changes: 18 additions & 4 deletions packages/abi-typegen/src/templates/contract/factory.hbs
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
{{header}}

import { Interface, Contract, ContractFactory } from "fuels";
import type { Provider, Account, AbstractAddress, BytesLike, DeployContractOptions } from "fuels";
import type { Provider, Account, AbstractAddress, BytesLike, DeployContractOptions, StorageSlot } from "fuels";
import type { {{capitalizedName}}, {{capitalizedName}}Interface } from "../{{capitalizedName}}";

const _abi = {{abiJsonString}}
const _abi = {{abiJsonString}};

const _storageSlots: StorageSlot[] = {{storageSlotsJsonString}};

export class {{capitalizedName}}__factory {
static readonly abi = _abi
static readonly abi = _abi;

static readonly storageSlots = _storageSlots;

static createInterface(): {{capitalizedName}}Interface {
return new Interface(_abi) as unknown as {{capitalizedName}}Interface
}

static connect(
id: string | AbstractAddress,
accountOrProvider: Account | Provider
): {{capitalizedName}} {
return new Contract(id, _abi, accountOrProvider) as unknown as {{capitalizedName}}
}

static async deployContract(
bytecode: BytesLike,
wallet: Account,
options: DeployContractOptions = {}
): Promise<{{capitalizedName}}> {
const factory = new ContractFactory(bytecode, _abi, wallet);
const contract = await factory.deployContract(options);

const { storageSlots } = {{capitalizedName}}__factory;

const contract = await factory.deployContract({
storageSlots,
...options,
});

return contract as unknown as {{capitalizedName}};
}
}
5 changes: 3 additions & 2 deletions packages/abi-typegen/src/templates/contract/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { renderHbsTemplate } from '../renderHbsTemplate';
import factoryTemplate from './factory.hbs';

export function renderFactoryTemplate(params: { abi: Abi }) {
const { name: capitalizedName, rawContents } = params.abi;
const { name: capitalizedName, rawContents, storageSlotsContents } = params.abi;
const abiJsonString = JSON.stringify(rawContents, null, 2);
const storageSlotsJsonString = storageSlotsContents ?? '[]';

const text = renderHbsTemplate({
template: factoryTemplate,
data: { capitalizedName, abiJsonString },
data: { capitalizedName, abiJsonString, storageSlotsJsonString },
});

return text;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getProjectResources, ForcProjectsEnum } from '../../test/fixtures/forc-projects';
import { ProgramTypeEnum } from '../types/enums/ProgramTypeEnum';

import { collectStorageSlotsFilepaths } from './collectStorageSlotsFilePaths';

describe('collectStorageSlotsFilePaths.ts', () => {
const script = getProjectResources(ForcProjectsEnum.SCRIPT);
const predicate = getProjectResources(ForcProjectsEnum.PREDICATE);
const contract = getProjectResources(ForcProjectsEnum.MINIMAL);

afterEach(jest.restoreAllMocks);

test('should collect storage slot files', () => {
const contractStorageSlots = collectStorageSlotsFilepaths({
filepaths: [contract.abiPath],
programType: ProgramTypeEnum.CONTRACT,
});

const predicateStorageSlots = collectStorageSlotsFilepaths({
filepaths: [predicate.abiPath],
programType: ProgramTypeEnum.PREDICATE,
});

const scriptStorageSlots = collectStorageSlotsFilepaths({
filepaths: [script.abiPath],
programType: ProgramTypeEnum.SCRIPT,
});

expect(contractStorageSlots.length).toEqual(1);
expect(predicateStorageSlots.length).toEqual(0);
expect(scriptStorageSlots.length).toEqual(0);
});
});
35 changes: 35 additions & 0 deletions packages/abi-typegen/src/utils/collectStorageSlotsFilePaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { existsSync, readFileSync } from 'fs';

import { ProgramTypeEnum } from '../types/enums/ProgramTypeEnum';
import type { IFile } from '../types/interfaces/IFile';

export const collectStorageSlotsFilepaths = (params: {
filepaths: string[];
programType: ProgramTypeEnum;
}) => {
const { filepaths, programType } = params;

// collect filepaths for storage slots JSON files
const storageSlotsFiles: IFile[] = [];

// abort unless we're dealing with contract types
if (programType !== ProgramTypeEnum.CONTRACT) {
return storageSlotsFiles;
}

filepaths.forEach((abiFilepath) => {
const storageSlotsFilepath = abiFilepath.replace('-abi.json', '-storage_slots.json');
const storageSlotsExists = existsSync(storageSlotsFilepath);

if (storageSlotsExists) {
const storageSlots: IFile = {
path: storageSlotsFilepath,
contents: readFileSync(storageSlotsFilepath, 'utf-8'),
};

storageSlotsFiles.push(storageSlots);
}
});

return storageSlotsFiles;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { Interface, Contract, ContractFactory } from "fuels";
import type { Provider, Account, AbstractAddress, BytesLike, DeployContractOptions } from "fuels";
import type { Provider, Account, AbstractAddress, BytesLike, DeployContractOptions, StorageSlot } from "fuels";
import type { MyContractAbi, MyContractAbiInterface } from "../MyContractAbi";

const _abi = {
Expand Down Expand Up @@ -54,26 +54,40 @@ const _abi = {
"loggedTypes": [],
"messagesTypes": [],
"configurables": []
}
};

const _storageSlots: StorageSlot[] = [];

export class MyContractAbi__factory {
static readonly abi = _abi
static readonly abi = _abi;

static readonly storageSlots = _storageSlots;

static createInterface(): MyContractAbiInterface {
return new Interface(_abi) as unknown as MyContractAbiInterface
}

static connect(
id: string | AbstractAddress,
accountOrProvider: Account | Provider
): MyContractAbi {
return new Contract(id, _abi, accountOrProvider) as unknown as MyContractAbi
}

static async deployContract(
bytecode: BytesLike,
wallet: Account,
options: DeployContractOptions = {}
): Promise<MyContractAbi> {
const factory = new ContractFactory(bytecode, _abi, wallet);
const contract = await factory.deployContract(options);

const { storageSlots } = MyContractAbi__factory;

const contract = await factory.deployContract({
storageSlots,
...options,
});

return contract as unknown as MyContractAbi;
}
}
Loading
Loading