Skip to content

Commit

Permalink
fix: typegen and storage slots integration (#3396)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedsalk authored Nov 21, 2024
1 parent 29f5135 commit 2cef020
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 70 deletions.
7 changes: 7 additions & 0 deletions .changeset/cyan-panthers-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fuel-ts/abi-typegen": patch
"@fuel-ts/contract": patch
"fuels": patch
---

fix: typegen and storage slots integration
2 changes: 1 addition & 1 deletion internal/benchmarks/src/cost-estimation.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('Cost Estimation Benchmarks', () => {
const wallet = new WalletUnlocked(process.env.DEVNET_WALLET_PVT_KEY as string, provider);

const contractFactory = new CallTestContractFactory(wallet);
const { waitForResult } = await contractFactory.deploy<CallTestContract>();
const { waitForResult } = await contractFactory.deploy();
const { contract: deployedContract } = await waitForResult();
contract = deployedContract;
} else {
Expand Down
26 changes: 11 additions & 15 deletions packages/abi-typegen/src/templates/contract/factory.hbs
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
{{header}}

import { Contract, ContractFactory, decompressBytecode } from "fuels";
import type { Provider, Account, DeployContractOptions, DeployContractResult } from "fuels";
import { ContractFactory, decompressBytecode } from "fuels";
import type { Provider, Account, DeployContractOptions } from "fuels";

import { {{capitalizedName}} } from "./{{capitalizedName}}";

const bytecode = decompressBytecode("{{compressedBytecode}}");

export class {{capitalizedName}}Factory extends ContractFactory {
export class {{capitalizedName}}Factory extends ContractFactory<{{capitalizedName}}> {

static readonly bytecode = bytecode;

constructor(accountOrProvider: Account | Provider) {
super(bytecode, {{capitalizedName}}.abi, accountOrProvider);
super(
bytecode,
{{capitalizedName}}.abi,
accountOrProvider,
{{capitalizedName}}.storageSlots
);
}

override deploy<TContract extends Contract = Contract>(
deployOptions?: DeployContractOptions
): Promise<DeployContractResult<TContract>> {
return super.deploy({
storageSlots: {{capitalizedName}}.storageSlots,
...deployOptions,
});
}

static async deploy (
static deploy (
wallet: Account,
options: DeployContractOptions = {}
): Promise<DeployContractResult<{{capitalizedName}}>> {
) {
const factory = new {{capitalizedName}}Factory(wallet);
return factory.deploy(options);
}
Expand Down
26 changes: 11 additions & 15 deletions packages/abi-typegen/test/fixtures/templates/contract/factory.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,30 @@
Fuel-Core version: 33.33.33
*/

import { Contract, ContractFactory, decompressBytecode } from "fuels";
import type { Provider, Account, DeployContractOptions, DeployContractResult } from "fuels";
import { ContractFactory, decompressBytecode } from "fuels";
import type { Provider, Account, DeployContractOptions } from "fuels";

import { MyContract } from "./MyContract";

const bytecode = decompressBytecode("0x-bytecode-here");

export class MyContractFactory extends ContractFactory {
export class MyContractFactory extends ContractFactory<MyContract> {

static readonly bytecode = bytecode;

constructor(accountOrProvider: Account | Provider) {
super(bytecode, MyContract.abi, accountOrProvider);
super(
bytecode,
MyContract.abi,
accountOrProvider,
MyContract.storageSlots
);
}

override deploy<TContract extends Contract = Contract>(
deployOptions?: DeployContractOptions
): Promise<DeployContractResult<TContract>> {
return super.deploy({
storageSlots: MyContract.storageSlots,
...deployOptions,
});
}

static async deploy (
static deploy (
wallet: Account,
options: DeployContractOptions = {}
): Promise<DeployContractResult<MyContract>> {
) {
const factory = new MyContractFactory(wallet);
return factory.deploy(options);
}
Expand Down
36 changes: 21 additions & 15 deletions packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ export type DeployContractResult<TContract extends Contract = Contract> = {
/**
* `ContractFactory` provides utilities for deploying and configuring contracts.
*/
export default class ContractFactory {
export default class ContractFactory<TContract extends Contract = Contract> {
bytecode: BytesLike;
interface: Interface;
provider!: Provider | null;
account!: Account | null;
storageSlots: StorageSlot[];

/**
* Create a ContractFactory instance.
Expand All @@ -68,7 +69,8 @@ export default class ContractFactory {
constructor(
bytecode: BytesLike,
abi: JsonAbi | Interface,
accountOrProvider: Account | Provider | null = null
accountOrProvider: Account | Provider | null = null,
storageSlots: StorageSlot[] = []
) {
// Force the bytecode to be a byte array
this.bytecode = arrayify(bytecode);
Expand Down Expand Up @@ -99,6 +101,8 @@ export default class ContractFactory {
this.provider = accountOrProvider;
this.account = null;
}

this.storageSlots = storageSlots;
}

/**
Expand All @@ -118,17 +122,19 @@ export default class ContractFactory {
* @returns The CreateTransactionRequest object for deploying the contract.
*/
createTransactionRequest(deployOptions?: DeployContractOptions & { bytecode?: BytesLike }) {
const storageSlots = deployOptions?.storageSlots
?.map(({ key, value }) => ({
const storageSlots = (deployOptions?.storageSlots ?? [])
.concat(this.storageSlots)
.map(({ key, value }) => ({
key: hexlifyWithPrefix(key),
value: hexlifyWithPrefix(value),
}))
.filter((el, index, self) => self.findIndex((s) => s.key === el.key) === index)
.sort(({ key: keyA }, { key: keyB }) => keyA.localeCompare(keyB));

const options = {
salt: randomBytes(32),
...deployOptions,
storageSlots: storageSlots || [],
...(deployOptions ?? {}),
storageSlots,
};

if (!this.provider) {
Expand Down Expand Up @@ -192,16 +198,16 @@ export default class ContractFactory {
* @param deployOptions - Options for deploying the contract.
* @returns A promise that resolves to the deployed contract instance.
*/
async deploy<TContract extends Contract = Contract>(
async deploy<T extends Contract = TContract>(
deployOptions: DeployContractOptions = {}
): Promise<DeployContractResult<TContract>> {
): Promise<DeployContractResult<T>> {
const account = this.getAccount();
const { consensusParameters } = account.provider.getChain();
const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber();

return this.bytecode.length > maxContractSize
? this.deployAsBlobTx(deployOptions)
: this.deployAsCreateTx(deployOptions);
: this.deployAsCreateTx<T>(deployOptions);
}

/**
Expand All @@ -210,9 +216,9 @@ export default class ContractFactory {
* @param deployOptions - Options for deploying the contract.
* @returns A promise that resolves to the deployed contract instance.
*/
async deployAsCreateTx<TContract extends Contract = Contract>(
async deployAsCreateTx<T extends Contract = TContract>(
deployOptions: DeployContractOptions = {}
): Promise<DeployContractResult<TContract>> {
): Promise<DeployContractResult<T>> {
const account = this.getAccount();
const { consensusParameters } = account.provider.getChain();
const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber();
Expand All @@ -230,7 +236,7 @@ export default class ContractFactory {

const waitForResult = async () => {
const transactionResult = await transactionResponse.waitForResult<TransactionType.Create>();
const contract = new Contract(contractId, this.interface, account) as TContract;
const contract = new Contract(contractId, this.interface, account) as T;

return { contract, transactionResult };
};
Expand All @@ -248,11 +254,11 @@ export default class ContractFactory {
* @param deployOptions - Options for deploying the contract.
* @returns A promise that resolves to the deployed contract instance.
*/
async deployAsBlobTx<TContract extends Contract = Contract>(
async deployAsBlobTx<T extends Contract = TContract>(
deployOptions: DeployContractOptions = {
chunkSizeMultiplier: CHUNK_SIZE_MULTIPLIER,
}
): Promise<DeployContractResult<TContract>> {
): Promise<DeployContractResult<T>> {
const account = this.getAccount();
const { configurableConstants, chunkSizeMultiplier } = deployOptions;
if (configurableConstants) {
Expand Down Expand Up @@ -362,7 +368,7 @@ export default class ContractFactory {
txIdResolver(createRequest.getTransactionId(account.provider.getChainId()));
const transactionResponse = await account.sendTransaction(createRequest);
const transactionResult = await transactionResponse.waitForResult<TransactionType.Create>();
const contract = new Contract(contractId, this.interface, account) as TContract;
const contract = new Contract(contractId, this.interface, account) as T;

return { contract, transactionResult };
};
Expand Down
12 changes: 6 additions & 6 deletions packages/fuel-gauge/src/contract-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describe('Contract Factory', () => {
const factory = new ContractFactory(LargeContractFactory.bytecode, LargeContract.abi, wallet);
expect(factory.bytecode.length % 8 === 0).toBe(true);

const deploy = await factory.deployAsBlobTx<LargeContract>();
const deploy = await factory.deployAsBlobTx();

const { contract } = await deploy.waitForResult();

Expand Down Expand Up @@ -311,7 +311,7 @@ describe('Contract Factory', () => {
} = launched;

const factory = new ContractFactory(LargeContractFactory.bytecode, LargeContract.abi, wallet);
const deploy = await factory.deployAsBlobTx<LargeContract>();
const deploy = await factory.deployAsBlobTx();
const initTxId = deploy.waitForTransactionId();
expect(initTxId).toStrictEqual(new Promise(() => {}));
const { contract } = await deploy.waitForResult();
Expand Down Expand Up @@ -339,7 +339,7 @@ describe('Contract Factory', () => {
const bytecode = concat([arrayify(LargeContractFactory.bytecode), new Uint8Array(3)]);
const factory = new ContractFactory(bytecode, LargeContract.abi, wallet);
expect(factory.bytecode.length % 8 === 0).toBe(false);
const deploy = await factory.deployAsBlobTx<LargeContract>({ chunkSizeMultiplier: 0.5 });
const deploy = await factory.deployAsBlobTx({ chunkSizeMultiplier: 0.5 });

const { contract } = await deploy.waitForResult();
expect(contract.id).toBeDefined();
Expand All @@ -361,7 +361,7 @@ describe('Contract Factory', () => {
const chunkSizeMultiplier = 2;

await expectToThrowFuelError(
() => factory.deployAsBlobTx<LargeContract>({ chunkSizeMultiplier }),
() => factory.deployAsBlobTx({ chunkSizeMultiplier }),
new FuelError(
ErrorCode.INVALID_CHUNK_SIZE_MULTIPLIER,
'Chunk size multiplier must be between 0 and 1'
Expand Down Expand Up @@ -534,12 +534,12 @@ describe('Contract Factory', () => {
const sendTransactionSpy = vi.spyOn(wallet, 'sendTransaction');
const factory = new ContractFactory(LargeContractFactory.bytecode, LargeContract.abi, wallet);

const firstDeploy = await factory.deployAsBlobTx<LargeContract>({
const firstDeploy = await factory.deployAsBlobTx({
salt: concat(['0x01', new Uint8Array(31)]),
});
const { contract: firstContract } = await firstDeploy.waitForResult();
const firstDeployCalls = sendTransactionSpy.mock.calls.length;
const secondDeploy = await factory.deployAsBlobTx<LargeContract>({
const secondDeploy = await factory.deployAsBlobTx({
salt: concat(['0x02', new Uint8Array(31)]),
});
const { contract: secondContract } = await secondDeploy.waitForResult();
Expand Down
70 changes: 67 additions & 3 deletions packages/fuel-gauge/src/storage-test-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,16 @@ describe('StorageTestContract', () => {

it('should allow for overriding storage slots', async () => {
const { storageSlots } = StorageTestContract;
const expectedStorageSlots = storageSlots.map(({ key }) => ({

expect(storageSlots.length).toBeGreaterThan(2);
const modifiedStorageSlots = storageSlots.slice(1).map(({ key }) => ({
key: `0x${key}`,
value: ZeroBytes32,
}));
const expectedStorageSlots = [
{ key: `0x${storageSlots[0].key}`, value: `0x${storageSlots[0].value}` },
...modifiedStorageSlots,
];

using launched = await launchTestNode();

Expand All @@ -138,18 +144,76 @@ describe('StorageTestContract', () => {
// via constructor
const storageContractFactory = new StorageTestContractFactory(wallet);
const deployConstructor = await storageContractFactory.deploy({
storageSlots: expectedStorageSlots,
storageSlots: modifiedStorageSlots,
});
const { transactionResult: transactionResultConstructor } =
await deployConstructor.waitForResult();
expect(transactionResultConstructor.transaction.storageSlots).toEqual(expectedStorageSlots);

// via static deploy
const deployStatically = await StorageTestContractFactory.deploy(wallet, {
storageSlots: expectedStorageSlots,
storageSlots: modifiedStorageSlots,
});
const { transactionResult: transactionResultStatically } =
await deployStatically.waitForResult();
expect(transactionResultStatically.transaction.storageSlots).toEqual(expectedStorageSlots);

// via deployAsBlobTx
const deployBlob = await storageContractFactory.deployAsBlobTx({
storageSlots: modifiedStorageSlots,
});

const { transactionResult: txResultBlob } = await deployBlob.waitForResult();
expect(txResultBlob.transaction.storageSlots).toEqual(expectedStorageSlots);

// via deployAsCreateTx
const deployCreate = await storageContractFactory.deployAsBlobTx({
storageSlots: modifiedStorageSlots,
});

const { transactionResult: txResultCreate } = await deployCreate.waitForResult();
expect(txResultCreate.transaction.storageSlots).toEqual(expectedStorageSlots);
});

test('automatically loads storage slots when using deployAsCreateTx', async () => {
const { storageSlots } = StorageTestContract;
const expectedStorageSlots = storageSlots.map(({ key, value }) => ({
key: `0x${key}`,
value: `0x${value}`,
}));

using launched = await launchTestNode();

const {
wallets: [wallet],
} = launched;

// via constructor
const storageContractFactory = new StorageTestContractFactory(wallet);
const deployConstructor = await storageContractFactory.deployAsCreateTx();
const { transactionResult: transactionResultConstructor } =
await deployConstructor.waitForResult();
expect(transactionResultConstructor.transaction.storageSlots).toEqual(expectedStorageSlots);
});

test('automatically loads storage slots when using deployAsBlobTx', async () => {
const { storageSlots } = StorageTestContract;
const expectedStorageSlots = storageSlots.map(({ key, value }) => ({
key: `0x${key}`,
value: `0x${value}`,
}));

using launched = await launchTestNode();

const {
wallets: [wallet],
} = launched;

// via constructor
const storageContractFactory = new StorageTestContractFactory(wallet);
const deployConstructor = await storageContractFactory.deployAsBlobTx();
const { transactionResult: transactionResultConstructor } =
await deployConstructor.waitForResult();
expect(transactionResultConstructor.transaction.storageSlots).toEqual(expectedStorageSlots);
});
});
Loading

0 comments on commit 2cef020

Please sign in to comment.