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!: add a non-blocking call for deploy contract #2597

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1649707
create helper getAccount
Torres-ssf Jun 24, 2024
7051478
create helper prepareDeploy
Torres-ssf Jun 24, 2024
80303b8
made deployContract use prepareDeploy
Torres-ssf Jun 24, 2024
5f6ffdd
implement method deployContractAsync
Torres-ssf Jun 24, 2024
188474f
add test case
Torres-ssf Jun 24, 2024
ea3fd88
improve docs
Torres-ssf Jun 24, 2024
f54f398
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 24, 2024
5ea4cec
add changeset
Torres-ssf Jun 24, 2024
1ff12dd
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 24, 2024
9fa3296
fix typo
Torres-ssf Jun 24, 2024
5d5a85d
Merge branch 'st/feat/make-deploy-contract-async' of github.com:FuelL…
Torres-ssf Jun 24, 2024
7eabf88
refact deployContract method
Torres-ssf Jun 24, 2024
c23e76e
adjust launch test node types
Torres-ssf Jun 24, 2024
db54c12
adjust test utilities
Torres-ssf Jun 24, 2024
7aecce8
adjust typegen template
Torres-ssf Jun 24, 2024
8ee880e
export type DeployContractResult
Torres-ssf Jun 24, 2024
c727f37
adjusting tests
Torres-ssf Jun 24, 2024
647f906
refact error message
Torres-ssf Jun 24, 2024
6eb8dcb
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 24, 2024
7c800e1
update docs
Torres-ssf Jun 24, 2024
5626f9a
fix changeset
Torres-ssf Jun 24, 2024
84a9540
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 24, 2024
f8fa58a
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 25, 2024
972118c
Merge branch 'master' into st/feat/make-deploy-contract-async
maschad Jun 25, 2024
ffd8aa8
Merge branch 'master' into st/feat/make-deploy-contract-async
arboleya Jun 25, 2024
c41de19
improve doc paragraph
Torres-ssf Jun 26, 2024
0d8cb33
improve doc paragraph
Torres-ssf Jun 26, 2024
26c5dbe
improve doc paragraph
Torres-ssf Jun 26, 2024
f528fac
remove unused import within factory hbs template
Torres-ssf Jun 26, 2024
15968e7
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 26, 2024
86508f9
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 27, 2024
17b9832
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 27, 2024
922ca4a
improve docs
Torres-ssf Jun 27, 2024
b313e3c
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 28, 2024
b9c0ed9
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 28, 2024
d2e0195
Merge branch 'master' into st/feat/make-deploy-contract-async
Torres-ssf Jun 29, 2024
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
5 changes: 5 additions & 0 deletions .changeset/giant-ads-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/contract": patch
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
---

feat: add a non-blocking call for deploy contract
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,29 @@ describe(__filename, () => {
expect(value).toBe(15);
// #endregion contract-setup-4
});

it('should successfully deploy a contract async and execute contract function', async () => {
const provider = await Provider.create(FUEL_NETWORK_URL);

const wallet = Wallet.fromPrivateKey(PRIVATE_KEY, provider);

const byteCodePath = join(projectsPath, `${contractName}/out/release/${contractName}.bin`);
const byteCode = readFileSync(byteCodePath);

const abiJsonPath = join(projectsPath, `${contractName}/out/release/${contractName}-abi.json`);
const abi = JSON.parse(readFileSync(abiJsonPath, 'utf8'));

// #region contract-async-1
const factory = new ContractFactory(byteCode, abi, wallet);

const { contract, transactionResponse } = await factory.deployContractAsync();

const deployResult = await transactionResponse.waitForResult();
// #endregion contract-async-1

const { value } = await contract.functions.echo_u8(15).simulate();

expect(deployResult).toBeDefined();
expect(value).toBe(15);
});
});
16 changes: 14 additions & 2 deletions apps/docs/src/guide/contracts/deploying-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,23 @@ Load the contract bytecode and JSON ABI, generated from the Sway source, into th

## 4. Deploying the Contract

Initialize a [`ContractFactory`](../../api/Contract/ContractFactory.md) with the bytecode, ABI, and wallet. Deploy the contract and use its methods.
To deploy the contract we can instantiate the [`ContractFactory`](../../api/Contract/ContractFactory.md) with the bytecode, ABI, and wallet. The we can call `deployContract` method. This method returns a promise that will resolve to a `Contract` instance, ready to be used to interact with the deployed smart contract.
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved

<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-setup-3{ts:line-numbers}

## 5. Executing a Contract Call
The `deployContract` method will only resolve after the deploy contract transaction has been processed and the smart contract is deployed on the blockchain. As a result, this call can take several seconds to resolve and will block any code execution that follows.

If this situation is inappropriate for your dApp, you can use the `deployContractAsync` method.

## 5. Deploying the Contract Asynchronously

In some cases, you may not want to wait for the deploy contract transaction to finish processing. In these instances, you can use `deployContractAsync`. This method resolves as soon as the transaction to deploy the contract is submitted and returns two objects: a `TransactionResponse` and a `Contract` instance.
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved

<<< @/../../docs-snippets/src/guide/contracts/deploying-contracts.test.ts#contract-async-1{ts:line-numbers}

It's important to note that the `contract` instance can only be safely used after the promise from the call to `transactionResponse.waitForResult` is resolved. To avoid blocking the rest of your code, you can attach this promise to a hook or listener that will use the contract only after it is fully deployed.

## 6. Executing a Contract Call

Now that the contract is deployed, you can interact with it. In the following steps, you'll learn how to execute contract calls.

Expand Down
91 changes: 64 additions & 27 deletions packages/contract/src/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,39 +132,35 @@ export default class ContractFactory {
* @returns A promise that resolves to the deployed contract instance.
*/
async deployContract(deployContractOptions: DeployContractOptions = {}) {
if (!this.account) {
throw new FuelError(ErrorCode.ACCOUNT_REQUIRED, 'Cannot deploy Contract without account.');
}

const { configurableConstants } = deployContractOptions;

if (configurableConstants) {
this.setConfigurableConstants(configurableConstants);
}
const { contractId, transactionRequest } = await this.prepareDeploy(deployContractOptions);
const account = this.getAccount();

const { contractId, transactionRequest } = this.createTransactionRequest(deployContractOptions);

const txCost = await this.account.provider.getTransactionCost(transactionRequest);
await account.sendTransaction(transactionRequest, {
awaitExecution: true,
});

const { maxFee: setMaxFee } = deployContractOptions;
return new Contract(contractId, this.interface, account);
}

if (isDefined(setMaxFee)) {
if (txCost.maxFee.gt(setMaxFee)) {
throw new FuelError(
ErrorCode.MAX_FEE_TOO_LOW,
`Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.`
);
}
} else {
transactionRequest.maxFee = txCost.maxFee;
}
/**
* Deploys a contract asynchronously.
*
* @param deployContractOptions - The options for deploying the contract (optional).
* @returns An object containing the deployed contract and the transaction response.
*/
async deployContractAsync(deployContractOptions: DeployContractOptions = {}) {
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
const { contractId, transactionRequest } = await this.prepareDeploy(deployContractOptions);
const account = this.getAccount();

await this.account.fund(transactionRequest, txCost);
await this.account.sendTransaction(transactionRequest, {
awaitExecution: true,
const transactionResponse = await account.sendTransaction(transactionRequest, {
awaitExecution: false,
});

return new Contract(contractId, this.interface, this.account);
const contract = new Contract(contractId, this.interface, account);
return {
contract,
transactionResponse,
};
}

/**
Expand Down Expand Up @@ -202,4 +198,45 @@ export default class ContractFactory {
);
}
}

private getAccount(): Account {
if (!this.account) {
throw new FuelError(ErrorCode.ACCOUNT_REQUIRED, 'Cannot deploy Contract without account.');
Torres-ssf marked this conversation as resolved.
Show resolved Hide resolved
}
return this.account;
}

private async prepareDeploy(deployContractOptions: DeployContractOptions) {
const { configurableConstants } = deployContractOptions;

if (configurableConstants) {
this.setConfigurableConstants(configurableConstants);
}

const { contractId, transactionRequest } = this.createTransactionRequest(deployContractOptions);

const account = this.getAccount();

const txCost = await account.provider.getTransactionCost(transactionRequest);

const { maxFee: setMaxFee } = deployContractOptions;

if (isDefined(setMaxFee)) {
if (txCost.maxFee.gt(setMaxFee)) {
throw new FuelError(
ErrorCode.MAX_FEE_TOO_LOW,
`Max fee '${deployContractOptions.maxFee}' is lower than the required: '${txCost.maxFee}'.`
);
}
} else {
transactionRequest.maxFee = txCost.maxFee;
}

await account.fund(transactionRequest, txCost);

return {
contractId,
transactionRequest,
};
}
}
17 changes: 17 additions & 0 deletions packages/fuel-gauge/src/contract-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ describe('Contract Factory', () => {
});
});

it('can deploy a contract asynchronously without waiting for the TX to be processed', async () => {
const factory = await createContractFactory();

const { transactionResponse, contract } = await factory.deployContractAsync();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the expected behaviour when calling a contract function before the transaction has been resolved via waitForResult?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petertonysmith94 An error will be thrown because the contract was not deployed yet, therefore, it does not exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthy of mentioning is that if the awaitExecution = true argument is passed, then there is no need to call waitForResult. Whoever uses the non-blocking awaitExecution = false should accept the nature of blockchain and do a waitForResult before running contract calls.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the rationale and that this is a delicate issue.

Still, I suspect this error case can be counterintuitive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, I suspect this error case can be counterintuitive.

If we want to still not fail a contract call and not have this error case, we could couple deployment to contract calls and wait for deployment to finish before running the actual contract call:

// mock internals of a contract call
await this.deploymentTransactionResponse?.waitForResult(); 
return call();

Then we can just explain in the documentation that, for awaitExecution=false, the first contract call might take a bit longer because the contract might not have been deployed yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Torres-ssf what would be the error type thrown if one calls the contract before it's deployed? I think a better path would be to document that error so that consumers can error handle appropriately and retry when necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider splitting the functionality.

expect(transactionResponse.gqlTransaction).toBeUndefined();

const deployResult = await transactionResponse.waitForResult();

expect(deployResult.isStatusSuccess).toBeTruthy();
expect(transactionResponse.gqlTransaction).toBeDefined();

const { value } = await contract.functions.increment_counter(1).call();

expect(value.toNumber()).toEqual(1);
});

it('Creates a factory from inputs that can prepare call data', async () => {
const factory = await createContractFactory();

Expand Down
Loading