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 1 commit
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/cyan-panthers-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/abi-typegen": patch
"@fuel-ts/contract": patch
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
---

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
22 changes: 9 additions & 13 deletions packages/abi-typegen/src/templates/contract/factory.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,23 @@ 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,
{ storageSlots: {{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}}>> {
): ReturnType<{{capitalizedName}}Factory['deploy']> {
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
const factory = new {{capitalizedName}}Factory(wallet);
return factory.deploy(options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,23 @@ 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,
{ storageSlots: 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>> {
): ReturnType<MyContractFactory['deploy']> {
const factory = new MyContractFactory(wallet);
return factory.deploy(options);
}
Expand Down
42 changes: 24 additions & 18 deletions packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { bn } from '@fuel-ts/math';
import { Contract } from '@fuel-ts/program';
import type { StorageSlot } from '@fuel-ts/transactions';
import { arrayify, isDefined } from '@fuel-ts/utils';
import { mergeDeepRight } from 'ramda';

import { getLoaderInstructions, getContractChunks } from './loader';
import { getContractId, getContractStorageRoot, hexlifyWithPrefix } from './util';
Expand Down Expand Up @@ -52,11 +53,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;
deployOptions: DeployContractOptions;
nedsalk marked this conversation as resolved.
Show resolved Hide resolved

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

this.deployOptions = deployOptions;
}

/**
Expand All @@ -118,7 +123,8 @@ export default class ContractFactory {
* @returns The CreateTransactionRequest object for deploying the contract.
*/
createTransactionRequest(deployOptions?: DeployContractOptions & { bytecode?: BytesLike }) {
const storageSlots = deployOptions?.storageSlots
const mergedOptions = mergeDeepRight(this.deployOptions, deployOptions ?? {});
const storageSlots = mergedOptions?.storageSlots
?.map(({ key, value }) => ({
key: hexlifyWithPrefix(key),
value: hexlifyWithPrefix(value),
Expand All @@ -127,7 +133,7 @@ export default class ContractFactory {

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

Expand All @@ -138,7 +144,7 @@ export default class ContractFactory {
);
}

const bytecode = deployOptions?.bytecode || this.bytecode;
const bytecode = options?.bytecode || this.bytecode;
const stateRoot = options.stateRoot || getContractStorageRoot(options.storageSlots);
const contractId = getContractId(bytecode, options.salt, stateRoot);
const transactionRequest = new CreateTransactionRequest({
Expand Down Expand Up @@ -192,7 +198,7 @@ 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(
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
deployOptions: DeployContractOptions = {}
): Promise<DeployContractResult<TContract>> {
const account = this.getAccount();
Expand All @@ -210,9 +216,11 @@ 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(
deployOptions: DeployContractOptions = {}
): Promise<DeployContractResult<TContract>> {
const options = mergeDeepRight(this.deployOptions, deployOptions);

const account = this.getAccount();
const { consensusParameters } = account.provider.getChain();
const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber();
Expand All @@ -224,7 +232,7 @@ export default class ContractFactory {
);
}

const { contractId, transactionRequest } = await this.prepareDeploy(deployOptions);
const { contractId, transactionRequest } = await this.prepareDeploy(options);

const transactionResponse = await account.sendTransaction(transactionRequest);

Expand All @@ -248,22 +256,23 @@ 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(
deployOptions: DeployContractOptions = {
chunkSizeMultiplier: CHUNK_SIZE_MULTIPLIER,
}
): Promise<DeployContractResult<TContract>> {
const options = mergeDeepRight(this.deployOptions, deployOptions);
const account = this.getAccount();
const { configurableConstants, chunkSizeMultiplier } = deployOptions;
const { configurableConstants, chunkSizeMultiplier } = options;
if (configurableConstants) {
this.setConfigurableConstants(configurableConstants);
}

// Generate the chunks based on the maximum chunk size and create blob txs
const chunkSize = this.getMaxChunkSize(deployOptions, chunkSizeMultiplier);
const chunkSize = this.getMaxChunkSize(options, chunkSizeMultiplier);
const chunks = getContractChunks(arrayify(this.bytecode), chunkSize).map((c) => {
const transactionRequest = this.blobTransactionRequest({
...deployOptions,
...options,
bytecode: c.bytecode,
});
return {
Expand All @@ -278,7 +287,7 @@ export default class ContractFactory {
const loaderBytecode = getLoaderInstructions(blobIds);
const { contractId, transactionRequest: createRequest } = this.createTransactionRequest({
bytecode: loaderBytecode,
...deployOptions,
...options,
});

// BlobIDs only need to be uploaded once and we can check if they exist on chain
Expand Down Expand Up @@ -329,10 +338,7 @@ export default class ContractFactory {
// Deploy the chunks as blob txs
for (const { blobId, transactionRequest } of chunks) {
if (!uploadedBlobs.includes(blobId) && blobIdsToUpload.includes(blobId)) {
const fundedBlobRequest = await this.fundTransactionRequest(
transactionRequest,
deployOptions
);
const fundedBlobRequest = await this.fundTransactionRequest(transactionRequest, options);

let result: TransactionResult<TransactionType.Blob>;

Expand All @@ -358,7 +364,7 @@ export default class ContractFactory {
}
}

await this.fundTransactionRequest(createRequest, deployOptions);
await this.fundTransactionRequest(createRequest, options);
txIdResolver(createRequest.getTransactionId(account.provider.getChainId()));
const transactionResponse = await account.sendTransaction(createRequest);
const transactionResult = await transactionResponse.waitForResult<TransactionType.Create>();
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
58 changes: 58 additions & 0 deletions packages/fuel-gauge/src/storage-test-contract.test.ts
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,63 @@ describe('StorageTestContract', () => {
const { transactionResult: transactionResultStatically } =
await deployStatically.waitForResult();
expect(transactionResultStatically.transaction.storageSlots).toEqual(expectedStorageSlots);

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

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

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

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