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

fix: typegen and storage slots integration #3396

Merged
merged 18 commits into from
Nov 21, 2024
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
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
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
"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