Skip to content

Commit

Permalink
feat(aztec-js): Return contract instance when awaiting deploy tx (#1360)
Browse files Browse the repository at this point in the history
Overrides the `send` method of `DeployMethod` so it returns a
`DeploySentTx`. This tx can be `wait`ed to return a receipt with the
contract instance, and also provides a `deployed` method that returns
just the contract. The wallet used for the instance can be set when
creating the `DeployMethod` or passed as an argument to
`wait`/`deployed`. Typed contracts return a typed version of
DeployMethod that returns a typed contract instance.
  • Loading branch information
spalladino authored Aug 2, 2023
1 parent 7ec6b32 commit e9c945c
Show file tree
Hide file tree
Showing 26 changed files with 167 additions and 113 deletions.
2 changes: 1 addition & 1 deletion circuits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ See [CODING_STANDARD.md](./CODING_STANDARD.md)\*\* before contributing!

## Repository Overview

The [`aztec3-circpuits`](https://github.com/AztecProtocol/aztec3-packages) circuits folder contains circuits and related C++ code (`cpp/`) for Aztec3 along with Typescript wrappers (`ts/`).
The [`aztec3-circuits`](https://github.com/AztecProtocol/aztec3-packages) circuits folder contains circuits and related C++ code (`cpp/`) for Aztec3 along with Typescript wrappers (`ts/`).

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async function deployAllContracts(owner: AztecAddress) {

// deploy l2 uniswap contract and attach to portal
const tx = UniswapContract.deploy(aztecRpcClient).send({ portalContract: uniswapPortalAddress });
await tx.isMined(0, 0.5);
await tx.isMined({ interval: 0.5 });
const receipt = await tx.getReceipt();
const uniswapL2Contract = await UniswapContract.create(receipt.contractAddress!, wallet);
await uniswapL2Contract.attach(uniswapPortalAddress);
Expand Down Expand Up @@ -158,7 +158,7 @@ const transferWethOnL2 = async (
const transferTx = wethL2Contract.methods
.transfer(transferAmount, ownerAddress, receiver)
.send({ origin: ownerAddress });
await transferTx.isMined(0, 0.5);
await transferTx.isMined({ interval: 0.5 });
const transferReceipt = await transferTx.getReceipt();
// expect(transferReceipt.status).toBe(TxStatus.MINED);
logger(`WETH to L2 Transfer Receipt status: ${transferReceipt.status} should be ${TxStatus.MINED}`);
Expand Down Expand Up @@ -226,7 +226,7 @@ async function main() {
const consumptionTx = wethL2Contract.methods
.mint(wethAmountToBridge, owner, messageKey, secret, ethAccount.toField())
.send({ origin: owner });
await consumptionTx.isMined(0, 0.5);
await consumptionTx.isMined({ interval: 0.5 });
const consumptionReceipt = await consumptionTx.getReceipt();
// expect(consumptionReceipt.status).toBe(TxStatus.MINED);
logger(`Consumption Receipt status: ${consumptionReceipt.status} should be ${TxStatus.MINED}`);
Expand Down Expand Up @@ -261,7 +261,7 @@ async function main() {
ethAccount.toField(),
)
.send({ origin: owner });
await withdrawTx.isMined(0, 0.5);
await withdrawTx.isMined({ interval: 0.5 });
const withdrawReceipt = await withdrawTx.getReceipt();
// expect(withdrawReceipt.status).toBe(TxStatus.MINED);
logger(`Withdraw receipt status: ${withdrawReceipt.status} should be ${TxStatus.MINED}`);
Expand Down Expand Up @@ -312,7 +312,7 @@ async function main() {
const daiMintTx = daiL2Contract.methods
.mint(daiAmountToBridge, owner, depositDaiMessageKey, secret, ethAccount.toField())
.send({ origin: owner });
await daiMintTx.isMined(0, 0.5);
await daiMintTx.isMined({ interval: 0.5 });
const daiMintTxReceipt = await daiMintTx.getReceipt();
// expect(daiMintTxReceipt.status).toBe(TxStatus.MINED);
logger(`DAI mint TX status: ${daiMintTxReceipt.status} should be ${TxStatus.MINED}`);
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec-sandbox/src/examples/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function deployAndInitializeNonNativeL2TokenContracts(
const tx = NonNativeTokenContract.deploy(wallet, initialBalance, owner).send({
portalContract: tokenPortalAddress,
});
await tx.isMined(0, 0.1);
await tx.isMined({ interval: 0.1 });
const receipt = await tx.getReceipt();
const l2Contract = await NonNativeTokenContract.create(receipt.contractAddress!, wallet);
await l2Contract.attach(tokenPortalAddress);
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/aztec-sandbox/src/examples/zk_token_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function main() {
// Mint more tokens
logger(`Minting ${SECONDARY_AMOUNT} more coins`);
const mintTx = zkContract.methods.mint(SECONDARY_AMOUNT, ownerAddress).send({ origin: ownerAddress });
await mintTx.isMined(0, 0.5);
await mintTx.isMined({ interval: 0.5 });
const balanceAfterMint = await getBalance(zkContract, ownerAddress);
logger(`Owner's balance is now: ${balanceAfterMint}`);

Expand All @@ -70,7 +70,7 @@ async function main() {
const transferTx = zkContract.methods
.transfer(SECONDARY_AMOUNT, ownerAddress, address2)
.send({ origin: ownerAddress });
await transferTx.isMined(0, 0.5);
await transferTx.isMined({ interval: 0.5 });
const balanceAfterTransfer = await getBalance(zkContract, ownerAddress);
const receiverBalance = await getBalance(zkContract, address2);
logger(`Owner's balance is now ${balanceAfterTransfer}`);
Expand Down
34 changes: 23 additions & 11 deletions yarn-project/aztec.js/src/contract/sent_tx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { FieldsOf } from '@aztec/circuits.js';
import { retryUntil } from '@aztec/foundation/retry';
import { AztecRPC, TxHash, TxReceipt, TxStatus } from '@aztec/types';

/** Options related to waiting for a tx. */
export type WaitOpts = {
/** The maximum time (in seconds) to wait for the transaction to be mined. */
timeout?: number;
/** The time interval (in seconds) between retries to fetch the transaction receipt. */
interval?: number;
};

const DefaultWaitOpts: WaitOpts = {
timeout: 0,
interval: 1,
};

/**
* The SentTx class represents a sent transaction through the AztecRPCClient, providing methods to fetch
* its hash, receipt, and mining status.
Expand Down Expand Up @@ -32,12 +46,11 @@ export class SentTx {

/**
* Awaits for a tx to be mined and returns the receipt. Throws if tx is not mined.
* @param timeout - The maximum time (in seconds) to wait for the transaction to be mined. A value of 0 means no timeout.
* @param interval - The time interval (in seconds) between retries to fetch the transaction receipt.
* @param opts - Options for configuring the waiting for the tx to be mined.
* @returns The transaction receipt.
*/
public async wait(timeout = 0, interval = 1): Promise<TxReceipt> {
const receipt = await this.waitForReceipt(timeout, interval);
public async wait(opts?: WaitOpts): Promise<FieldsOf<TxReceipt>> {
const receipt = await this.waitForReceipt(opts);
if (receipt.status !== TxStatus.MINED)
throw new Error(`Transaction ${await this.getTxHash()} was ${receipt.status}`);
return receipt;
Expand All @@ -48,25 +61,24 @@ export class SentTx {
* Resolves to true if the transaction status is 'MINED', false otherwise.
* Throws an error if the transaction receipt cannot be fetched after the given timeout.
*
* @param timeout - The maximum time (in seconds) to wait for the transaction to be mined. A value of 0 means no timeout.
* @param interval - The time interval (in seconds) between retries to fetch the transaction receipt.
* @param opts - Options for configuring the waiting for the tx to be mined.
* @returns A Promise that resolves to a boolean indicating if the transaction is mined or not.
*/
public async isMined(timeout = 0, interval = 1): Promise<boolean> {
const receipt = await this.waitForReceipt(timeout, interval);
public async isMined(opts?: WaitOpts): Promise<boolean> {
const receipt = await this.waitForReceipt(opts);
return receipt.status === TxStatus.MINED;
}

protected async waitForReceipt(timeout = 0, interval = 1): Promise<TxReceipt> {
protected async waitForReceipt(opts?: WaitOpts): Promise<TxReceipt> {
const txHash = await this.getTxHash();
return await retryUntil(
async () => {
const txReceipt = await this.arc.getTxReceipt(txHash);
return txReceipt.status != TxStatus.PENDING ? txReceipt : undefined;
},
'isMined',
timeout,
interval,
opts?.timeout ?? DefaultWaitOpts.timeout,
opts?.interval ?? DefaultWaitOpts.interval,
);
}
}
14 changes: 8 additions & 6 deletions yarn-project/aztec.js/src/contract_deployer/deploy_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { Fr } from '@aztec/foundation/fields';
import { AztecRPC, ExecutionRequest, PackedArguments, PublicKey, Tx, TxExecutionRequest } from '@aztec/types';

import { BaseWallet, Wallet } from '../aztec_rpc_client/wallet.js';
import { Contract, ContractFunctionInteraction, SendMethodOptions } from '../contract/index.js';
import { Contract, ContractBase, ContractFunctionInteraction, SendMethodOptions } from '../contract/index.js';
import { DeploySentTx } from './deploy_sent_tx.js';

/**
* Options for deploying a contract on the Aztec network.
Expand Down Expand Up @@ -56,7 +57,7 @@ class DeployerWallet extends BaseWallet {
* Creates a TxRequest from a contract ABI, for contract deployment.
* Extends the ContractFunctionInteraction class.
*/
export class DeployMethod extends ContractFunctionInteraction {
export class DeployMethod<TContract extends ContractBase = Contract> extends ContractFunctionInteraction {
/**
* The partially computed contract address. Known after creation of the deployment transaction.
*/
Expand All @@ -67,7 +68,7 @@ export class DeployMethod extends ContractFunctionInteraction {
*/
public completeContractAddress?: AztecAddress = undefined;

constructor(private publicKey: PublicKey, arc: AztecRPC, private abi: ContractAbi, args: any[] = []) {
constructor(private publicKey: PublicKey, private arc: AztecRPC, private abi: ContractAbi, args: any[] = []) {
const constructorAbi = abi.functions.find(f => f.name === 'constructor');
if (!constructorAbi) {
throw new Error('Cannot find constructor in the ABI.');
Expand Down Expand Up @@ -126,10 +127,11 @@ export class DeployMethod extends ContractFunctionInteraction {
* allowing us to send a transaction specifically for contract deployment.
*
* @param options - An object containing various deployment options such as portalContract, contractAddressSalt, and from.
* @returns A Promise that resolves to the transaction receipt upon successful deployment.
* @returns A SentTx object that returns the receipt and the deployed contract instance.
*/
public send(options: DeployOptions = {}) {
return super.send(options);
public send(options: DeployOptions = {}): DeploySentTx<TContract> {
const txHashPromise = super.send(options).getTxHash();
return new DeploySentTx(this.abi, this.arc, txHashPromise);
}

/**
Expand Down
55 changes: 55 additions & 0 deletions yarn-project/aztec.js/src/contract_deployer/deploy_sent_tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FieldsOf } from '@aztec/circuits.js';
import { ContractAbi } from '@aztec/foundation/abi';
import { TxHash, TxReceipt } from '@aztec/types';

import { AztecAddress, AztecRPC, Contract, ContractBase, SentTx, WaitOpts, Wallet } from '../index.js';

/** Options related to waiting for a deployment tx. */
export type DeployedWaitOpts = WaitOpts & {
/** Wallet to use for creating a contract instance. Uses the one set in the deployer constructor if not set. */
wallet?: Wallet;
};

/** Extends a transaction receipt with a contract instance that represents the newly deployed contract. */
export type DeployTxReceipt<TContract extends ContractBase = Contract> = FieldsOf<TxReceipt> & {
/** Instance of the newly deployed contract. */
contract: TContract;
};

/**
* A contract deployment transaction sent to the network, extending SentTx with methods to create a contract instance.
*/
export class DeploySentTx<TContract extends ContractBase = Contract> extends SentTx {
constructor(private abi: ContractAbi, wallet: AztecRPC | Wallet, txHashPromise: Promise<TxHash>) {
super(wallet, txHashPromise);
}

/**
* Awaits for the tx to be mined and returns the contract instance. Throws if tx is not mined.
* @param opts - Options for configuring the waiting for the tx to be mined.
* @returns The deployed contract instance.
*/
public async deployed(opts?: DeployedWaitOpts): Promise<TContract> {
const receipt = await this.wait(opts);
return receipt.contract;
}

/**
* Awaits for the tx to be mined and returns the receipt along with a contract instance. Throws if tx is not mined.
* @param opts - Options for configuring the waiting for the tx to be mined.
* @returns The transaction receipt with the deployed contract instance.
*/
public async wait(opts?: DeployedWaitOpts): Promise<DeployTxReceipt<TContract>> {
const receipt = await super.wait(opts);
const contract = await this.getContractInstance(opts?.wallet, receipt.contractAddress);
return { ...receipt, contract };
}

protected getContractInstance(wallet?: Wallet, address?: AztecAddress): Promise<TContract> {
const isWallet = (rpc: AztecRPC | Wallet): rpc is Wallet => !!(rpc as Wallet).createAuthenticatedTxRequest;
const contractWallet = wallet ?? (isWallet(this.arc) && this.arc);
if (!contractWallet) throw new Error(`A wallet is required for creating a contract instance`);
if (!address) throw new Error(`Contract address is missing from transaction receipt`);
return Contract.create(address, this.abi, contractWallet) as Promise<TContract>;
}
}
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/utils/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function createAccounts(
await aztecRpcClient.addAccount(privKey, deploymentInfo.address, deploymentInfo.partialAddress);
const contractDeployer = new ContractDeployer(accountContractAbi, aztecRpcClient, publicKey);
const tx = contractDeployer.deploy().send({ contractAddressSalt: salt });
await tx.isMined(0, 0.5);
await tx.isMined({ interval: 0.5 });
const receipt = await tx.getReceipt();
if (receipt.status !== TxStatus.MINED) {
throw new Error(`Deployment tx not mined (status is ${receipt.status})`);
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/end-to-end/src/cross_chain/test_harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export class CrossChainTestHarness {
.transfer(transferAmount, this.ownerAddress, this.receiver)
.send({ origin: this.accounts[0] });

await transferTx.isMined(0, 0.1);
await transferTx.isMined({ interval: 0.1 });
const transferReceipt = await transferTx.getReceipt();

expect(transferReceipt.status).toBe(TxStatus.MINED);
Expand All @@ -170,7 +170,7 @@ export class CrossChainTestHarness {
.mint(bridgeAmount, this.ownerAddress, messageKey, secret, this.ethAccount.toField())
.send({ origin: this.ownerAddress });

await consumptionTx.isMined(0, 0.1);
await consumptionTx.isMined({ interval: 0.1 });
const consumptionReceipt = await consumptionTx.getReceipt();
expect(consumptionReceipt.status).toBe(TxStatus.MINED);
}
Expand All @@ -182,7 +182,7 @@ export class CrossChainTestHarness {
.mintPublic(bridgeAmount, this.ownerAddress, messageKey, secret, this.ethAccount.toField())
.send({ origin: this.ownerAddress });

await consumptionTx.isMined(0, 0.1);
await consumptionTx.isMined({ interval: 0.1 });
const consumptionReceipt = await consumptionTx.getReceipt();
expect(consumptionReceipt.status).toBe(TxStatus.MINED);
}
Expand Down Expand Up @@ -254,7 +254,7 @@ export class CrossChainTestHarness {
async shieldFundsOnL2(shieldAmount: bigint, secretHash: Fr) {
this.logger('Shielding funds on L2');
const shieldTx = this.l2Contract.methods.shield(shieldAmount, secretHash).send({ origin: this.ownerAddress });
await shieldTx.isMined(0, 0.1);
await shieldTx.isMined({ interval: 0.1 });
const shieldReceipt = await shieldTx.getReceipt();
expect(shieldReceipt.status).toBe(TxStatus.MINED);
}
Expand Down
10 changes: 5 additions & 5 deletions yarn-project/end-to-end/src/e2e_2_rpc_servers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('e2e_2_rpc_servers', () => {
logger(`Deploying ZkToken contract...`);
const tx = ZkTokenContract.deploy(aztecRpcServerA, initialBalance, owner).send();
const receipt = await tx.getReceipt();
await tx.isMined(0, 0.1);
await tx.isMined({ interval: 0.1 });
const minedReceipt = await tx.getReceipt();
expect(minedReceipt.status).toEqual(TxStatus.MINED);
logger('L2 contract deployed');
Expand Down Expand Up @@ -121,7 +121,7 @@ describe('e2e_2_rpc_servers', () => {
const contractWithWalletA = await ZkTokenContract.create(tokenAddress, walletA);
const txAToB = contractWithWalletA.methods.transfer(transferAmount1, userA, userB).send({ origin: userA });

await txAToB.isMined(0, 0.1);
await txAToB.isMined({ interval: 0.1 });
const receiptAToB = await txAToB.getReceipt();

expect(receiptAToB.status).toBe(TxStatus.MINED);
Expand All @@ -136,7 +136,7 @@ describe('e2e_2_rpc_servers', () => {
const contractWithWalletB = await ZkTokenContract.create(tokenAddress, walletB);
const txBToA = contractWithWalletB.methods.transfer(transferAmount2, userB, userA).send({ origin: userB });

await txBToA.isMined(0, 0.1);
await txBToA.isMined({ interval: 0.1 });
const receiptBToA = await txBToA.getReceipt();

expect(receiptBToA.status).toBe(TxStatus.MINED);
Expand All @@ -152,7 +152,7 @@ describe('e2e_2_rpc_servers', () => {
logger(`Deploying Child contract...`);
const tx = ChildContract.deploy(aztecRpcServerA).send();
const receipt = await tx.getReceipt();
await tx.isMined(0, 0.1);
await tx.isMined({ interval: 0.1 });
const minedReceipt = await tx.getReceipt();
expect(minedReceipt.status).toEqual(TxStatus.MINED);
logger('Child contract deployed');
Expand Down Expand Up @@ -188,7 +188,7 @@ describe('e2e_2_rpc_servers', () => {

const childContractWithWalletB = await ChildContract.create(childAddress, walletB);
const tx = childContractWithWalletB.methods.pubStoreValue(newValueToSet).send({ origin: userB });
await tx.isMined(0, 0.1);
await tx.isMined({ interval: 0.1 });

const receipt = await tx.getReceipt();
expect(receipt.status).toBe(TxStatus.MINED);
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/end-to-end/src/e2e_account_contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ function itShouldBehaveLikeAnAccountContract(
const { logger } = context;
logger('Calling private function...');
const tx = child.methods.value(42).send();
expect(await tx.isMined(0, 0.1)).toBeTruthy();
expect(await tx.isMined({ interval: 0.1 })).toBeTruthy();
}, 60_000);

it('calls a public function', async () => {
const { logger, aztecRpcServer } = context;
logger('Calling public function...');
const tx = child.methods.pubStoreValue(42).send();
expect(await tx.isMined(0, 0.1)).toBeTruthy();
expect(await tx.isMined({ interval: 0.1 })).toBeTruthy();
expect(toBigInt((await aztecRpcServer.getPublicStorageAt(child.address, new Fr(1)))!)).toEqual(42n);
}, 60_000);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('e2e_cross_chain_messaging', () => {
.withdraw(withdrawAmount, ownerAddress, ethAccount, EthAddress.ZERO.toField())
.send({ origin: ownerAddress });

await withdrawTx.isMined(0, 0.1);
await withdrawTx.isMined({ interval: 0.1 });
const withdrawReceipt = await withdrawTx.getReceipt();

expect(withdrawReceipt.status).toBe(TxStatus.MINED);
Expand Down
Loading

0 comments on commit e9c945c

Please sign in to comment.