Skip to content

Commit

Permalink
feat(aztec-js): remove sender from execution request and add batching (
Browse files Browse the repository at this point in the history
…#1415)

A few clean ups in aztec-js:
- Remove `from` in `ExecutionRequest` and rename it to `FunctionCall` to
avoid confusion with `TxExecutionRequest` and
`CallStackItem.isExecutionRequest`.
- Remove `TxContext` argument from `AccountImp.createAuthenticatedTx`
and rename it to `createTxExecutionRequest`.
- Remove unused `nonce` in method send options.
- Remove unused function to get a contract instance in deploy method
given we now have #1360.
- Extracts a base class for `contract_function_interaction` and
`deploy_method` so we can remove the `DeployWallet`.
- Adds a new `BatchCall` method for nicer batch calls. Fixes #1343.
spalladino authored Aug 4, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 801f6a5 commit 05b6e86
Showing 27 changed files with 253 additions and 303 deletions.
12 changes: 7 additions & 5 deletions yarn-project/acir-simulator/src/client/simulator.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { ExecutionRequest, TxExecutionRequest } from '@aztec/types';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';

import { PackedArgsCache } from '../packed_args_cache.js';
import { ClientTxExecutionContext } from './client_execution_context.js';
@@ -87,14 +87,16 @@ export class AcirSimulator {
/**
* Runs an unconstrained function.
* @param request - The transaction request.
* @param origin - The sender of the request.
* @param entryPointABI - The ABI of the entry point function.
* @param contractAddress - The address of the contract.
* @param portalContractAddress - The address of the portal contract.
* @param historicRoots - The historic roots.
* @returns The return values of the function.
*/
public async runUnconstrained(
request: ExecutionRequest,
request: FunctionCall,
origin: AztecAddress,
entryPointABI: FunctionAbi,
contractAddress: AztecAddress,
portalContractAddress: EthAddress,
@@ -104,7 +106,7 @@ export class AcirSimulator {
throw new Error(`Cannot run ${entryPointABI.functionType} function as constrained`);
}
const callContext = new CallContext(
request.from!,
origin,
contractAddress,
portalContractAddress,
false,
@@ -150,15 +152,15 @@ export class AcirSimulator {
const preimageLen = (abi.parameters[3].type as ArrayType).length;
const extendedPreimage = notePreimage.concat(Array(preimageLen - notePreimage.length).fill(Fr.ZERO));

const execRequest: ExecutionRequest = {
from: AztecAddress.ZERO,
const execRequest: FunctionCall = {
to: AztecAddress.ZERO,
functionData: FunctionData.empty(),
args: encodeArguments(abi, [contractAddress, nonce, storageSlot, extendedPreimage]),
};

const [[innerNoteHash, siloedNoteHash, uniqueSiloedNoteHash, innerNullifier]] = await this.runUnconstrained(
execRequest,
AztecAddress.ZERO,
abi,
AztecAddress.ZERO,
EthAddress.ZERO,
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { ZkTokenContractAbi } from '@aztec/noir-contracts/artifacts';
import { ExecutionRequest } from '@aztec/types';
import { FunctionCall } from '@aztec/types';

import { mock } from 'jest-mock-extended';

@@ -74,15 +74,15 @@ describe('Unconstrained Execution test suite', () => {
})),
);

const execRequest: ExecutionRequest = {
from: AztecAddress.random(),
const execRequest: FunctionCall = {
to: contractAddress,
functionData: new FunctionData(Buffer.alloc(4), false, true, true),
args: encodeArguments(abi, [owner]),
};

const result = await acirSimulator.runUnconstrained(
execRequest,
AztecAddress.random(),
abi,
AztecAddress.random(),
EthAddress.ZERO,
29 changes: 10 additions & 19 deletions yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ import {
ContractData,
ContractPublicData,
DeployedContract,
ExecutionRequest,
FunctionCall,
KeyStore,
L2BlockL2Logs,
LogType,
@@ -193,9 +193,8 @@ export class AztecRPCServer implements AztecRPC {
}

public async viewTx(functionName: string, args: any[], to: AztecAddress, from?: AztecAddress) {
const txRequest = await this.#getExecutionRequest(functionName, args, to, from ?? AztecAddress.ZERO);

const executionResult = await this.#simulateUnconstrained(txRequest);
const functionCall = await this.#getFunctionCall(functionName, args, to);
const executionResult = await this.#simulateUnconstrained(functionCall, from);

// TODO - Return typed result based on the function abi.
return executionResult;
@@ -254,12 +253,7 @@ export class AztecRPCServer implements AztecRPC {
return await this.node.getLogs(from, limit, LogType.UNENCRYPTED);
}

async #getExecutionRequest(
functionName: string,
args: any[],
to: AztecAddress,
from: AztecAddress,
): Promise<ExecutionRequest> {
async #getFunctionCall(functionName: string, args: any[], to: AztecAddress): Promise<FunctionCall> {
const contract = await this.db.getContract(to);
if (!contract) {
throw new Error(`Unknown contract ${to}: add it to Aztec RPC server by calling server.addContracts(...)`);
@@ -272,7 +266,6 @@ export class AztecRPCServer implements AztecRPC {

return {
args: encodeArguments(functionDao, args),
from,
functionData: FunctionData.fromAbi(functionDao),
to,
};
@@ -311,10 +304,10 @@ export class AztecRPCServer implements AztecRPC {
* @returns An object containing the contract address, function ABI, portal contract address, and historic tree roots.
*/
async #getSimulationParameters(
execRequest: ExecutionRequest | TxExecutionRequest,
execRequest: FunctionCall | TxExecutionRequest,
contractDataOracle: ContractDataOracle,
) {
const contractAddress = (execRequest as ExecutionRequest).to ?? (execRequest as TxExecutionRequest).origin;
const contractAddress = (execRequest as FunctionCall).to ?? (execRequest as TxExecutionRequest).origin;
const functionAbi = await contractDataOracle.getFunctionAbi(
contractAddress,
execRequest.functionData.functionSelectorBuffer,
@@ -369,14 +362,11 @@ export class AztecRPCServer implements AztecRPC {
* Returns the simulation result containing the outputs of the unconstrained function.
*
* @param execRequest - The transaction request object containing the target contract and function data.
* @param contractDataOracle - Optional instance of ContractDataOracle for fetching and caching contract information.
* @param from - The origin of the request.
* @returns The simulation result containing the outputs of the unconstrained function.
*/
async #simulateUnconstrained(execRequest: ExecutionRequest, contractDataOracle?: ContractDataOracle) {
if (!contractDataOracle) {
contractDataOracle = new ContractDataOracle(this.db, this.node);
}

async #simulateUnconstrained(execRequest: FunctionCall, from?: AztecAddress) {
const contractDataOracle = new ContractDataOracle(this.db, this.node);
const { contractAddress, functionAbi, portalContract, historicRoots } = await this.#getSimulationParameters(
execRequest,
contractDataOracle,
@@ -387,6 +377,7 @@ export class AztecRPCServer implements AztecRPC {
this.log('Executing unconstrained simulator...');
const result = await simulator.runUnconstrained(
execRequest,
from ?? AztecAddress.ZERO,
functionAbi,
contractAddress,
portalContract,
17 changes: 8 additions & 9 deletions yarn-project/aztec.js/src/account_impl/account_collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AztecAddress, TxContext } from '@aztec/circuits.js';
import { ExecutionRequest, TxExecutionRequest } from '@aztec/types';
import { AztecAddress } from '@aztec/circuits.js';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';

import { AccountImplementation } from './index.js';
import { AccountImplementation, CreateTxRequestOpts } from './index.js';

/**
* A concrete account implementation that manages multiple accounts.
@@ -23,14 +23,13 @@ export class AccountCollection implements AccountImplementation {
return AztecAddress.fromString(this.accounts.keys().next().value as string);
}

public createAuthenticatedTxRequest(
executions: ExecutionRequest[],
txContext: TxContext,
public createTxExecutionRequest(
executions: FunctionCall[],
opts: CreateTxRequestOpts = {},
): Promise<TxExecutionRequest> {
// TODO: Check all executions have the same origin
const sender = executions[0].from;
const sender = opts.origin ?? this.getAddress();
const impl = this.accounts.get(sender.toString());
if (!impl) throw new Error(`No account implementation registered for ${sender}`);
return impl.createAuthenticatedTxRequest(executions, txContext);
return impl.createTxExecutionRequest(executions, opts);
}
}
10 changes: 5 additions & 5 deletions yarn-project/aztec.js/src/account_impl/entrypoint_payload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CircuitsWasm, Fr } from '@aztec/circuits.js';
import { padArrayEnd } from '@aztec/foundation/collection';
import { sha256 } from '@aztec/foundation/crypto';
import { ExecutionRequest, PackedArguments, emptyExecutionRequest } from '@aztec/types';
import { FunctionCall, PackedArguments, emptyFunctionCall } from '@aztec/types';

// These must match the values defined in yarn-project/noir-libs/noir-aztec/src/entrypoint.nr
const ACCOUNT_MAX_PRIVATE_CALLS = 2;
@@ -24,8 +24,8 @@ export type EntrypointPayload = {

/** Assembles an entrypoint payload from a set of private and public function calls */
export async function buildPayload(
privateCalls: ExecutionRequest[],
publicCalls: ExecutionRequest[],
privateCalls: FunctionCall[],
publicCalls: FunctionCall[],
): Promise<{
/** The payload for the entrypoint function */
payload: EntrypointPayload;
@@ -35,8 +35,8 @@ export async function buildPayload(
const nonce = Fr.random();

const calls = [
...padArrayEnd(privateCalls, emptyExecutionRequest(), ACCOUNT_MAX_PRIVATE_CALLS),
...padArrayEnd(publicCalls, emptyExecutionRequest(), ACCOUNT_MAX_PUBLIC_CALLS),
...padArrayEnd(privateCalls, emptyFunctionCall(), ACCOUNT_MAX_PRIVATE_CALLS),
...padArrayEnd(publicCalls, emptyFunctionCall(), ACCOUNT_MAX_PUBLIC_CALLS),
];

const packedArguments = [];
18 changes: 12 additions & 6 deletions yarn-project/aztec.js/src/account_impl/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { AztecAddress, TxContext } from '@aztec/circuits.js';
import { ExecutionRequest, TxExecutionRequest } from '@aztec/types';
import { AztecAddress } from '@aztec/circuits.js';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';

export * from './account_collection.js';
export * from './single_key_account_contract.js';
export * from './stored_key_account_contract.js';
export * from './account_collection.js';

/** Options for creating a tx request out of a set of function calls. */
export type CreateTxRequestOpts = {
/** Origin of the tx. Needs to be an address managed by this account. */
origin?: AztecAddress;
};

/** Represents an implementation for a user account contract. Knows how to encode and sign a tx for that particular implementation. */
export interface AccountImplementation {
@@ -15,9 +21,9 @@ export interface AccountImplementation {

/**
* Generates an authenticated request out of set of intents
* @param executions - The execution intent to be authenticated.
* @param txContext - The tx context under with the execution is to be made.
* @param executions - The execution intents to be run.
* @param opts - Options.
* @returns The authenticated transaction execution request.
*/
createAuthenticatedTxRequest(executions: ExecutionRequest[], txContext: TxContext): Promise<TxExecutionRequest>;
createTxExecutionRequest(executions: FunctionCall[], opts?: CreateTxRequestOpts): Promise<TxExecutionRequest>;
}
Original file line number Diff line number Diff line change
@@ -8,14 +8,15 @@ import {
} from '@aztec/circuits.js';
import { Signer } from '@aztec/circuits.js/barretenberg';
import { ContractAbi, encodeArguments } from '@aztec/foundation/abi';
import { ExecutionRequest, PackedArguments, TxExecutionRequest } from '@aztec/types';
import { FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/types';

import partition from 'lodash.partition';

import SchnorrSingleKeyAccountContractAbi from '../abis/schnorr_single_key_account_contract.json' assert { type: 'json' };
import { generatePublicKey } from '../index.js';
import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from '../utils/defaults.js';
import { buildPayload, hashPayload } from './entrypoint_payload.js';
import { AccountImplementation } from './index.js';
import { AccountImplementation, CreateTxRequestOpts } from './index.js';

/**
* Account contract implementation that uses a single key for signing and encryption. This public key is not
@@ -28,18 +29,21 @@ export class SingleKeyAccountContract implements AccountImplementation {
private partialContractAddress: PartialContractAddress,
private privateKey: PrivateKey,
private signer: Signer,
private chainId: number = DEFAULT_CHAIN_ID,
private version: number = DEFAULT_VERSION,
) {}

getAddress(): AztecAddress {
return this.address;
}

async createAuthenticatedTxRequest(
executions: ExecutionRequest[],
txContext: TxContext,
async createTxExecutionRequest(
executions: FunctionCall[],
opts: CreateTxRequestOpts = {},
): Promise<TxExecutionRequest> {
this.checkSender(executions);
this.checkIsNotDeployment(txContext);
if (opts.origin && !opts.origin.equals(this.address)) {
throw new Error(`Sender ${opts.origin.toString()} does not match account address ${this.address.toString()}`);
}

const [privateCalls, publicCalls] = partition(executions, exec => exec.functionData.isPrivate);
const wasm = await CircuitsWasm.get();
@@ -55,7 +59,7 @@ export class SingleKeyAccountContract implements AccountImplementation {
argsHash: packedArgs.hash,
origin: this.address,
functionData: FunctionData.fromAbi(abi),
txContext,
txContext: TxContext.empty(this.chainId, this.version),
packedArguments: [...callsPackedArguments, packedArgs],
});

@@ -70,19 +74,4 @@ export class SingleKeyAccountContract implements AccountImplementation {
if (!abi) throw new Error(`Entrypoint abi for account contract not found`);
return abi;
}

private checkIsNotDeployment(txContext: TxContext) {
if (txContext.isContractDeploymentTx) {
throw new Error(`Cannot yet deploy contracts from an account contract`);
}
}

private checkSender(executions: ExecutionRequest[]) {
const wrongSender = executions.find(e => !e.from.equals(this.address));
if (wrongSender) {
throw new Error(
`Sender ${wrongSender.from.toString()} does not match account address ${this.address.toString()}`,
);
}
}
}
Original file line number Diff line number Diff line change
@@ -2,13 +2,14 @@ import { AztecAddress, CircuitsWasm, FunctionData, PrivateKey, TxContext } from
import { Signer } from '@aztec/circuits.js/barretenberg';
import { ContractAbi, encodeArguments } from '@aztec/foundation/abi';
import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { ExecutionRequest, PackedArguments, TxExecutionRequest } from '@aztec/types';
import { FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/types';

import partition from 'lodash.partition';

import EcdsaAccountContractAbi from '../abis/ecdsa_account_contract.json' assert { type: 'json' };
import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from '../utils/defaults.js';
import { buildPayload, hashPayload } from './entrypoint_payload.js';
import { AccountImplementation } from './index.js';
import { AccountImplementation, CreateTxRequestOpts } from './index.js';

/**
* Account contract implementation that keeps a signing public key in storage, and is retrieved on
@@ -17,20 +18,27 @@ import { AccountImplementation } from './index.js';
export class StoredKeyAccountContract implements AccountImplementation {
private log: DebugLogger;

constructor(private address: AztecAddress, private privateKey: PrivateKey, private signer: Signer) {
constructor(
private address: AztecAddress,
private privateKey: PrivateKey,
private signer: Signer,
private chainId: number = DEFAULT_CHAIN_ID,
private version: number = DEFAULT_VERSION,
) {
this.log = createDebugLogger('aztec:client:accounts:stored_key');
}

getAddress(): AztecAddress {
return this.address;
}

async createAuthenticatedTxRequest(
executions: ExecutionRequest[],
txContext: TxContext,
async createTxExecutionRequest(
executions: FunctionCall[],
opts: CreateTxRequestOpts = {},
): Promise<TxExecutionRequest> {
this.checkSender(executions);
this.checkIsNotDeployment(txContext);
if (opts.origin && !opts.origin.equals(this.address)) {
throw new Error(`Sender ${opts.origin.toString()} does not match account address ${this.address.toString()}`);
}

const [privateCalls, publicCalls] = partition(executions, exec => exec.functionData.isPrivate);
const wasm = await CircuitsWasm.get();
@@ -46,7 +54,7 @@ export class StoredKeyAccountContract implements AccountImplementation {
argsHash: packedArgs.hash,
origin: this.address,
functionData: FunctionData.fromAbi(abi),
txContext,
txContext: TxContext.empty(this.chainId, this.version),
packedArguments: [...callsPackedArguments, packedArgs],
});

@@ -61,19 +69,4 @@ export class StoredKeyAccountContract implements AccountImplementation {
if (!abi) throw new Error(`Entrypoint abi for account contract not found`);
return abi;
}

private checkIsNotDeployment(txContext: TxContext) {
if (txContext.isContractDeploymentTx) {
throw new Error(`Cannot yet deploy contracts from an account contract`);
}
}

private checkSender(executions: ExecutionRequest[]) {
const wrongSender = executions.find(e => !e.from.equals(this.address));
if (wrongSender) {
throw new Error(
`Sender ${wrongSender.from.toString()} does not match account address ${this.address.toString()}`,
);
}
}
}
17 changes: 8 additions & 9 deletions yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AztecAddress, Fr, PartialContractAddress, PrivateKey, PublicKey, TxContext } from '@aztec/circuits.js';
import { AztecAddress, Fr, PartialContractAddress, PrivateKey, PublicKey } from '@aztec/circuits.js';
import {
AztecRPC,
ContractData,
ContractPublicData,
DeployedContract,
ExecutionRequest,
FunctionCall,
L2BlockL2Logs,
NodeInfo,
SyncStatus,
@@ -14,7 +14,7 @@ import {
TxReceipt,
} from '@aztec/types';

import { AccountImplementation } from '../account_impl/index.js';
import { AccountImplementation, CreateTxRequestOpts } from '../account_impl/index.js';

/**
* The wallet interface.
@@ -26,11 +26,10 @@ export type Wallet = AccountImplementation & AztecRPC;
*/
export abstract class BaseWallet implements Wallet {
constructor(protected readonly rpc: AztecRPC) {}

abstract getAddress(): AztecAddress;
abstract createAuthenticatedTxRequest(
executions: ExecutionRequest[],
txContext: TxContext,
): Promise<TxExecutionRequest>;
abstract createTxExecutionRequest(execs: FunctionCall[], opts?: CreateTxRequestOpts): Promise<TxExecutionRequest>;

addAccount(privKey: PrivateKey, address: AztecAddress, partialContractAddress: Fr): Promise<AztecAddress> {
return this.rpc.addAccount(privKey, address, partialContractAddress);
}
@@ -110,7 +109,7 @@ export class AccountWallet extends BaseWallet {
getAddress(): AztecAddress {
return this.accountImpl.getAddress();
}
createAuthenticatedTxRequest(executions: ExecutionRequest[], txContext: TxContext): Promise<TxExecutionRequest> {
return this.accountImpl.createAuthenticatedTxRequest(executions, txContext);
createTxExecutionRequest(executions: FunctionCall[], opts: CreateTxRequestOpts = {}): Promise<TxExecutionRequest> {
return this.accountImpl.createTxExecutionRequest(executions, opts);
}
}
62 changes: 62 additions & 0 deletions yarn-project/aztec.js/src/contract/base_contract_interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { AztecAddress } from '@aztec/circuits.js';
import { AztecRPC, Tx, TxExecutionRequest } from '@aztec/types';

import { SentTx } from './sent_tx.js';

/**
* Represents options for calling a (constrained) function in a contract.
* Allows the user to specify the sender address and nonce for a transaction.
*/
export interface SendMethodOptions {
/**
* Sender's address initiating the transaction.
*/
origin?: AztecAddress;
}

/**
* Base class for an interaction with a contract, be it a deployment, a function call, or a batch.
* Implements the sequence create/simulate/send.
*/
export abstract class BaseContractInteraction {
protected tx?: Tx;
protected txRequest?: TxExecutionRequest;

constructor(protected rpc: AztecRPC) {}

/**
* Create a transaction execution request ready to be simulated.
* @param options - An optional object containing additional configuration for the transaction.
* @returns A transaction execution request.
*/
public abstract create(options?: SendMethodOptions): Promise<TxExecutionRequest>;

/**
* Simulates a transaction execution request and returns a tx object ready to be sent.
* @param options - optional arguments to be used in the creation of the transaction
* @returns The resulting transaction
*/
public async simulate(options: SendMethodOptions = {}): Promise<Tx> {
const txRequest = this.txRequest ?? (await this.create(options));
this.tx = await this.rpc.simulateTx(txRequest);
return this.tx;
}

/**
* Sends a transaction to the contract function with the specified options.
* This function throws an error if called on an unconstrained function.
* It creates and signs the transaction if necessary, and returns a SentTx instance,
* which can be used to track the transaction status, receipt, and events.
* @param options - An optional object containing 'from' property representing
* the AztecAddress of the sender. If not provided, the default address is used.
* @returns A SentTx instance for tracking the transaction status and information.
*/
public send(options: SendMethodOptions = {}) {
const promise = (async () => {
const tx = this.tx ?? (await this.simulate(options));
return this.rpc.sendTx(tx);
})();

return new SentTx(this.rpc, promise);
}
}
22 changes: 22 additions & 0 deletions yarn-project/aztec.js/src/contract/batch_call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FunctionCall, TxExecutionRequest, Wallet } from '../index.js';
import { BaseContractInteraction, SendMethodOptions } from './base_contract_interaction.js';

/** A batch of function calls to be sent as a single transaction through a wallet. */
export class BatchCall extends BaseContractInteraction {
constructor(protected wallet: Wallet, protected calls: FunctionCall[]) {
super(wallet);
}

/**
* Create a transaction execution request that represents this batch, encoded and authenticated by the
* user's wallet, ready to be simulated.
* @param options - An optional object containing additional configuration for the transaction.
* @returns A Promise that resolves to a transaction instance.
*/
public async create(options?: SendMethodOptions | undefined): Promise<TxExecutionRequest> {
if (!this.txRequest) {
this.txRequest = await this.wallet.createTxExecutionRequest(this.calls, options);
}
return this.txRequest;
}
}
12 changes: 4 additions & 8 deletions yarn-project/aztec.js/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
@@ -93,7 +93,7 @@ describe('Contract Class', () => {

beforeEach(() => {
wallet = mock<Wallet>();
wallet.createAuthenticatedTxRequest.mockResolvedValue(mockTxRequest);
wallet.createTxExecutionRequest.mockResolvedValue(mockTxRequest);
wallet.isContractDeployed.mockResolvedValue(true);
wallet.sendTx.mockResolvedValue(mockTxHash);
wallet.viewTx.mockResolvedValue(mockViewResultValue);
@@ -115,7 +115,7 @@ describe('Contract Class', () => {

expect(txHash).toBe(mockTxHash);
expect(receipt).toBe(mockTxReceipt);
expect(wallet.createAuthenticatedTxRequest).toHaveBeenCalledTimes(1);
expect(wallet.createTxExecutionRequest).toHaveBeenCalledTimes(1);
expect(wallet.sendTx).toHaveBeenCalledTimes(1);
expect(wallet.sendTx).toHaveBeenCalledWith(mockTx);
});
@@ -130,13 +130,9 @@ describe('Contract Class', () => {
expect(result).toBe(mockViewResultValue);
});

it('should not call send on an unconstrained function', async () => {
it('should not call create on an unconstrained function', async () => {
const fooContract = await Contract.create(contractAddress, defaultAbi, wallet);
expect(() =>
fooContract.methods.qux().send({
origin: account,
}),
).toThrow();
await expect(fooContract.methods.qux().create({ origin: account })).rejects.toThrow();
});

it('should not call view on a secret or open function', async () => {
101 changes: 14 additions & 87 deletions yarn-project/aztec.js/src/contract/contract_function_interaction.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
import { AztecAddress, Fr, FunctionData, TxContext } from '@aztec/circuits.js';
import { AztecAddress, FunctionData } from '@aztec/circuits.js';
import { FunctionAbi, FunctionType, encodeArguments } from '@aztec/foundation/abi';
import { ExecutionRequest, Tx, TxExecutionRequest } from '@aztec/types';
import { FunctionCall, TxExecutionRequest } from '@aztec/types';

import { Wallet } from '../aztec_rpc_client/wallet.js';
import { SentTx } from './sent_tx.js';
import { BaseContractInteraction, SendMethodOptions } from './base_contract_interaction.js';

/**
* Represents options for calling a (constrained) function in a contract.
* Allows the user to specify the sender address and nonce for a transaction.
*/
export interface SendMethodOptions {
/**
* Sender's address initiating the transaction.
*/
origin?: AztecAddress;
/**
* The nonce representing the order of transactions sent by the address.
*/
nonce?: Fr;
}
export { SendMethodOptions };

/**
* Represents the options for a view method in a contract function interaction.
@@ -33,29 +20,24 @@ export interface ViewMethodOptions {

/**
* This is the class that is returned when calling e.g. `contract.methods.myMethod(arg0, arg1)`.
* It contains available interactions one can call on a method.
* It contains available interactions one can call on a method, including view.
*/
export class ContractFunctionInteraction {
protected tx?: Tx;
protected txRequest?: TxExecutionRequest;

export class ContractFunctionInteraction extends BaseContractInteraction {
constructor(
protected wallet: Wallet,
protected contractAddress: AztecAddress,
protected functionDao: FunctionAbi,
protected args: any[],
) {
super(wallet);
if (args.some(arg => arg === undefined || arg === null)) {
throw new Error('All function interaction arguments must be defined and not null. Received: ' + args);
}
}

/**
* Create an Aztec transaction instance by combining the transaction request and its signature.
* This function will first check if a signature exists, and if not, it will call the `sign` method
* to obtain the signature before creating the transaction. Throws an error if the function is
* of unconstrained type or if the transaction request and signature are missing.
*
* Create a transaction execution request that represents this call, encoded and authenticated by the
* user's wallet, ready to be simulated.
* @param options - An optional object containing additional configuration for the transaction.
* @returns A Promise that resolves to a transaction instance.
*/
@@ -64,82 +46,27 @@ export class ContractFunctionInteraction {
throw new Error("Can't call `create` on an unconstrained function.");
}
if (!this.txRequest) {
const executionRequest = await this.getExecutionRequest(this.contractAddress, options.origin);
const nodeInfo = await this.wallet.getNodeInfo();
const txContext = TxContext.empty(new Fr(nodeInfo.chainId), new Fr(nodeInfo.version));
const txRequest = await this.wallet.createAuthenticatedTxRequest([executionRequest], txContext);
this.txRequest = txRequest;
this.txRequest = await this.wallet.createTxExecutionRequest([this.request()], options);
}
return this.txRequest;
}

/**
* Simulates a transaction's execution.
* @param options - optional arguments to be used in the creation of the transaction
* @returns The resulting transaction
*/
public async simulate(options: SendMethodOptions = {}): Promise<Tx> {
const txRequest = this.txRequest ?? (await this.create(options));
this.tx = await this.wallet.simulateTx(txRequest);
return this.tx;
}

/**
* Returns an execution request that represents this operation. Useful as a building
* block for constructing batch requests.
* @param options - An optional object containing additional configuration for the transaction.
* @returns An execution request wrapped in promise.
*/
public request(options: SendMethodOptions = {}): Promise<ExecutionRequest> {
return this.getExecutionRequest(this.contractAddress, options.origin);
}

protected async getExecutionRequest(to: AztecAddress, from?: AztecAddress): Promise<ExecutionRequest> {
const flatArgs = encodeArguments(this.functionDao, this.args);
from = from ?? this.wallet.getAddress();

const accounts = await this.wallet.getAccounts();
// Zero address is used during deployment and does not need to be in the wallet
if (!from.equals(AztecAddress.ZERO) && !accounts.some(acc => from!.equals(acc))) {
throw new Error(`The specified 'from' address ${from} is not in the wallet's accounts.`);
}

return {
args: flatArgs,
functionData: FunctionData.fromAbi(this.functionDao),
to,
from,
};
}

/**
* Sends a transaction to the contract function with the specified options.
* This function throws an error if called on an unconstrained function.
* It creates and signs the transaction if necessary, and returns a SentTx instance,
* which can be used to track the transaction status, receipt, and events.
*
* @param options - An optional object containing 'from' property representing
* the AztecAddress of the sender. If not provided, the default address is used.
* @returns A SentTx instance for tracking the transaction status and information.
*/
public send(options: SendMethodOptions = {}) {
if (this.functionDao.functionType === FunctionType.UNCONSTRAINED) {
throw new Error("Can't call `send` on an unconstrained function.");
}

const promise = (async () => {
const tx = this.tx ?? (await this.simulate(options));
return this.wallet.sendTx(tx);
})();

return new SentTx(this.wallet, promise);
public request(): FunctionCall {
const args = encodeArguments(this.functionDao, this.args);
const functionData = FunctionData.fromAbi(this.functionDao);
return { args, functionData, to: this.contractAddress };
}

/**
* Execute a view (read-only) transaction on an unconstrained function.
* This method is used to call functions that do not modify the contract state and only return data.
* Throws an error if called on a non-unconstrained function.
*
* @param options - An optional object containing additional configuration for the transaction.
* @returns The result of the view transaction as returned by the contract function.
*/
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/contract/index.ts
Original file line number Diff line number Diff line change
@@ -2,3 +2,4 @@ export * from './contract.js';
export * from './contract_function_interaction.js';
export * from './sent_tx.js';
export * from './contract_base.js';
export * from './batch_call.js';
93 changes: 31 additions & 62 deletions yarn-project/aztec.js/src/contract_deployer/deploy_method.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {
CircuitsWasm,
ContractDeploymentData,
FunctionData,
PartialContractAddress,
TxContext,
getContractDeploymentInfo,
} from '@aztec/circuits.js';
import { ContractAbi } from '@aztec/foundation/abi';
import { ContractAbi, FunctionAbi, encodeArguments } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { AztecRPC, ExecutionRequest, PackedArguments, PublicKey, Tx, TxExecutionRequest } from '@aztec/types';
import { AztecRPC, PackedArguments, PublicKey, Tx, TxExecutionRequest } from '@aztec/types';

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

/**
@@ -30,51 +30,25 @@ export interface DeployOptions extends SendMethodOptions {
contractAddressSalt?: Fr;
}

/**
* Simple wallet implementation for use when deploying contracts only.
*/
class DeployerWallet extends BaseWallet {
getAddress(): AztecAddress {
return AztecAddress.ZERO;
}
async createAuthenticatedTxRequest(
executions: ExecutionRequest[],
txContext: TxContext,
): Promise<TxExecutionRequest> {
if (executions.length !== 1) {
throw new Error(`Deployer wallet can only run one execution at a time (requested ${executions.length})`);
}
const [execution] = executions;
const wasm = await CircuitsWasm.get();
const packedArguments = await PackedArguments.fromArgs(execution.args, wasm);
return Promise.resolve(
new TxExecutionRequest(execution.to, execution.functionData, packedArguments.hash, txContext, [packedArguments]),
);
}
}

/**
* Creates a TxRequest from a contract ABI, for contract deployment.
* Extends the ContractFunctionInteraction class.
*/
export class DeployMethod<TContract extends ContractBase = Contract> extends ContractFunctionInteraction {
/**
* The partially computed contract address. Known after creation of the deployment transaction.
*/
export class DeployMethod<TContract extends ContractBase = Contract> extends BaseContractInteraction {
/** The partially computed contract address. Known after creation of the deployment transaction. */
public partialContractAddress?: PartialContractAddress = undefined;

/**
* The complete contract address.
*/
/** The complete contract address. */
public completeContractAddress?: AztecAddress = undefined;

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.');
}
/** Constructor function to call. */
private constructorAbi: FunctionAbi;

super(new DeployerWallet(arc), AztecAddress.ZERO, constructorAbi, args);
constructor(private publicKey: PublicKey, private arc: AztecRPC, private abi: ContractAbi, private args: any[] = []) {
super(arc);
const constructorAbi = abi.functions.find(f => f.name === 'constructor');
if (!constructorAbi) throw new Error('Cannot find constructor in the ABI.');
this.constructorAbi = constructorAbi;
}

/**
@@ -90,7 +64,7 @@ export class DeployMethod<TContract extends ContractBase = Contract> extends Con
const portalContract = options.portalContract ?? EthAddress.ZERO;
const contractAddressSalt = options.contractAddressSalt ?? Fr.random();

const { chainId, version } = await this.wallet.getNodeInfo();
const { chainId, version } = await this.rpc.getNodeInfo();

const { address, constructorHash, functionTreeRoot, partialAddress } = await getContractDeploymentInfo(
this.abi,
@@ -108,15 +82,25 @@ export class DeployMethod<TContract extends ContractBase = Contract> extends Con
);

const txContext = new TxContext(false, false, true, contractDeploymentData, new Fr(chainId), new Fr(version));
const executionRequest = await this.getExecutionRequest(address, AztecAddress.ZERO);
const txRequest = await this.wallet.createAuthenticatedTxRequest([executionRequest], txContext);
const args = encodeArguments(this.constructorAbi, this.args);
const functionData = FunctionData.fromAbi(this.constructorAbi);
const execution = { args, functionData, to: address };
const packedArguments = await PackedArguments.fromArgs(execution.args);

const txRequest = TxExecutionRequest.from({
origin: execution.to,
functionData: execution.functionData,
argsHash: packedArguments.hash,
txContext,
packedArguments: [packedArguments],
});

this.txRequest = txRequest;
this.partialContractAddress = partialAddress;
this.completeContractAddress = address;

// TODO: Should we add the contracts to the DB here, or once the tx has been sent or mined?
await this.wallet.addContracts([{ abi: this.abi, address, portalContract }]);
await this.rpc.addContracts([{ abi: this.abi, address, portalContract }]);

return this.txRequest;
}
@@ -139,22 +123,7 @@ export class DeployMethod<TContract extends ContractBase = Contract> extends Con
* @param options - Deployment options.
* @returns The simulated tx.
*/
public async simulate(options: DeployOptions): Promise<Tx> {
const txRequest = this.txRequest ?? (await this.create(options));

this.tx = await this.wallet.simulateTx(txRequest);
return this.tx;
}

/**
* Creates a contract abstraction given a wallet.
* @param withWallet - The wallet to provide to the contract abstraction
* @returns - The generated contract abstraction.
*/
public async getContract(withWallet: Wallet) {
if (!this.completeContractAddress) {
throw new Error(`Cannot get a contract instance for a contract not yet deployed`);
}
return await Contract.create(this.completeContractAddress, this.abi, withWallet);
public simulate(options: DeployOptions): Promise<Tx> {
return super.simulate(options);
}
}
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ export class DeploySentTx<TContract extends ContractBase = Contract> extends Sen
}

protected getContractInstance(wallet?: Wallet, address?: AztecAddress): Promise<TContract> {
const isWallet = (rpc: AztecRPC | Wallet): rpc is Wallet => !!(rpc as Wallet).createAuthenticatedTxRequest;
const isWallet = (rpc: AztecRPC | Wallet): rpc is Wallet => !!(rpc as Wallet).createTxExecutionRequest;
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`);
4 changes: 2 additions & 2 deletions yarn-project/aztec.js/src/index.ts
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ export {
ContractDeploymentTx,
ContractPublicData,
DeployedContract,
ExecutionRequest,
FunctionCall,
L2BlockL2Logs,
NodeInfo,
PackedArguments,
@@ -26,7 +26,7 @@ export {
TxHash,
TxReceipt,
TxStatus,
emptyExecutionRequest,
emptyFunctionCall,
} from '@aztec/types';

export { createDebugLogger } from '@aztec/foundation/log';
4 changes: 4 additions & 0 deletions yarn-project/aztec.js/src/utils/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Default L1 chain ID to use when constructing txs (matches hardhat and anvil's default). */
export const DEFAULT_CHAIN_ID = 31337;
/** Default protocol version to use. */
export const DEFAULT_VERSION = 1;
4 changes: 2 additions & 2 deletions yarn-project/circuits.js/src/structs/tx_context.ts
Original file line number Diff line number Diff line change
@@ -136,8 +136,8 @@ export class TxContext {
);
}

static empty(chainId: Fr = Fr.ZERO, version: Fr = Fr.ZERO) {
return new TxContext(false, false, false, ContractDeploymentData.empty(), chainId, version);
static empty(chainId: Fr | number = 0, version: Fr | number = 0) {
return new TxContext(false, false, false, ContractDeploymentData.empty(), new Fr(chainId), new Fr(version));
}

/**
20 changes: 5 additions & 15 deletions yarn-project/end-to-end/src/e2e_escrow_contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { AztecNodeService } from '@aztec/aztec-node';
import { AztecRPCServer } from '@aztec/aztec-rpc';
import { AztecAddress, SentTx, Wallet, generatePublicKey } from '@aztec/aztec.js';
import { Fr, PrivateKey, TxContext, getContractDeploymentInfo } from '@aztec/circuits.js';
import { AztecAddress, BatchCall, Wallet, generatePublicKey } from '@aztec/aztec.js';
import { Fr, PrivateKey, getContractDeploymentInfo } from '@aztec/circuits.js';
import { generateFunctionSelector } from '@aztec/foundation/abi';
import { toBufferBE } from '@aztec/foundation/bigint-buffer';
import { DebugLogger } from '@aztec/foundation/log';
import { retryUntil } from '@aztec/foundation/retry';
import { EscrowContractAbi, ZkTokenContractAbi } from '@aztec/noir-contracts/artifacts';
import { EscrowContract, ZkTokenContract } from '@aztec/noir-contracts/types';
import { AztecRPC, PublicKey } from '@aztec/types';
@@ -93,20 +92,11 @@ describe('e2e_escrow_contract', () => {
await expectBalance(owner, 50n);

const actions = [
await zkTokenContract.methods.transfer(10, owner, recipient).request(),
await escrowContract.methods.withdraw(zkTokenContract.address, 20, recipient).request(),
zkTokenContract.methods.transfer(10, owner, recipient).request(),
escrowContract.methods.withdraw(zkTokenContract.address, 20, recipient).request(),
];

// TODO: We need a nicer interface for batch actions
const nodeInfo = await wallet.getNodeInfo();
const txContext = TxContext.empty(new Fr(nodeInfo.chainId), new Fr(nodeInfo.version));
const txRequest = await wallet.createAuthenticatedTxRequest(actions, txContext);
logger(`Executing batch transfer from ${wallet.getAddress()}`);
const tx = await wallet.simulateTx(txRequest);
const sentTx = new SentTx(aztecRpcServer, wallet.sendTx(tx));
await sentTx.isMined();

await retryUntil(() => aztecRpcServer.isAccountSynchronised(recipient), 'account sync', 30);
await new BatchCall(wallet, actions).send().wait();
await expectBalance(recipient, 30n);
}, 120_000);
});
9 changes: 4 additions & 5 deletions yarn-project/end-to-end/src/e2e_non_contract_account.test.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { CircuitsWasm, Fr, PrivateKey, TxContext } from '@aztec/circuits.js';
import { DebugLogger } from '@aztec/foundation/log';
import { retryUntil } from '@aztec/foundation/retry';
import { PokeableTokenContract } from '@aztec/noir-contracts/types';
import { AztecRPC, ExecutionRequest, PackedArguments, TxExecutionRequest, TxStatus } from '@aztec/types';
import { AztecRPC, FunctionCall, PackedArguments, TxExecutionRequest, TxStatus } from '@aztec/types';

import { expectsNumOfEncryptedLogsInTheLastBlockToBe, setup } from './utils.js';

@@ -17,16 +17,15 @@ class SignerlessWallet extends BaseWallet {
getAddress(): AztecAddress {
return AztecAddress.ZERO;
}
async createAuthenticatedTxRequest(
executions: ExecutionRequest[],
txContext: TxContext,
): Promise<TxExecutionRequest> {
async createTxExecutionRequest(executions: FunctionCall[]): Promise<TxExecutionRequest> {
if (executions.length !== 1) {
throw new Error(`Unexpected number of executions. Expected 1, received ${executions.length})`);
}
const [execution] = executions;
const wasm = await CircuitsWasm.get();
const packedArguments = await PackedArguments.fromArgs(execution.args, wasm);
const { chainId, version } = await this.rpc.getNodeInfo();
const txContext = TxContext.empty(chainId, version);
return Promise.resolve(
new TxExecutionRequest(execution.to, execution.functionData, packedArguments.hash, txContext, [packedArguments]),
);
2 changes: 2 additions & 0 deletions yarn-project/foundation/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ const contexts = [
'ClassDeclaration[implements.length=0] MethodDefinition[accessibility=public]',
// TODO: All methods public by default in a class that does not implement an interface
// 'ClassDeclaration[implements.length=0] MethodDefinition[accessibility=undefined][key.type=Identifier]',
// TODO: All export const from the top level of a file
// 'ExportNamedDeclaration[declaration.type=VariableDeclaration]',
// Legacy contexts (needs review)
'TSParameterProperty[accessibility=public]',
'TSPropertySignature',
7 changes: 4 additions & 3 deletions yarn-project/foundation/src/fields/fields.ts
Original file line number Diff line number Diff line change
@@ -18,9 +18,10 @@ export class Fr {
*/
public readonly value: bigint;

constructor(value: bigint | number) {
this.value = BigInt(value);
if (value > Fr.MAX_VALUE) {
constructor(value: bigint | number | Fr) {
const isFr = (value: bigint | number | Fr): value is Fr => !!(value as Fr).toBigInt;
this.value = isFr(value) ? value.toBigInt() : BigInt(value);
if (this.value > Fr.MAX_VALUE) {
throw new Error(`Fr out of range ${value}.`);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PublicExecution, PublicExecutionResult, PublicExecutor } from '@aztec/acir-simulator';
import {
ARGS_LENGTH,
AztecAddress,
CallContext,
CircuitsWasm,
EthAddress,
@@ -27,7 +28,7 @@ import {
ContractDataSource,
ContractPublicData,
EncodedContractFunction,
ExecutionRequest,
FunctionCall,
FunctionL2Logs,
SiblingPath,
Tx,
@@ -169,8 +170,7 @@ describe('public_processor', () => {

const publicExecutionResult = makePublicExecutionResultFromRequest(callRequest);
publicExecutionResult.nestedExecutions = [
makePublicExecutionResult({
from: publicExecutionResult.execution.contractAddress,
makePublicExecutionResult(publicExecutionResult.execution.contractAddress, {
to: makeAztecAddress(30),
functionData: new FunctionData(makeSelector(5), false, false, false),
args: new Array(ARGS_LENGTH).fill(Fr.ZERO),
@@ -203,10 +203,11 @@ function makePublicExecutionResultFromRequest(item: PublicCallRequest): PublicEx
}

function makePublicExecutionResult(
tx: ExecutionRequest,
from: AztecAddress,
tx: FunctionCall,
nestedExecutions: PublicExecutionResult[] = [],
): PublicExecutionResult {
const callContext = new CallContext(tx.from, tx.to, EthAddress.ZERO, false, false, false);
const callContext = new CallContext(from, tx.to, EthAddress.ZERO, false, false, false);
const execution: PublicExecution = {
callContext,
contractAddress: tx.to,
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { AztecAddress, Fr, FunctionData } from '@aztec/circuits.js';

/** A request to call a function on a contract from a given address. */
export type ExecutionRequest = {
/** The sender of the call */
from: AztecAddress;
export type FunctionCall = {
/** The recipient contract */
to: AztecAddress;
/** The function being called */
@@ -13,12 +11,11 @@ export type ExecutionRequest = {
};

/**
* Creates an empty execution request.
* @returns an empty execution request.
* Creates an empty function call.
* @returns an empty function call.
*/
export function emptyExecutionRequest() {
export function emptyFunctionCall() {
return {
from: AztecAddress.ZERO,
to: AztecAddress.ZERO,
functionData: FunctionData.empty(),
args: [],
2 changes: 1 addition & 1 deletion yarn-project/types/src/index.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ export * from './constants.js';
export * from './contract_dao.js';
export * from './contract_database.js';
export * from './contract_data.js';
export * from './execution_request.js';
export * from './function_call.js';
export * from './keys/index.js';
export * from './l1_addresses.js';
export * from './l1_to_l2_message.js';
4 changes: 2 additions & 2 deletions yarn-project/types/src/packed_arguments.ts
Original file line number Diff line number Diff line change
@@ -25,8 +25,8 @@ export class PackedArguments {
return new PackedArguments(...PackedArguments.getFields(fields));
}

static async fromArgs(args: Fr[], wasm: CircuitsWasm) {
return new PackedArguments(args, await computeVarArgsHash(wasm, args));
static async fromArgs(args: Fr[], wasm?: CircuitsWasm) {
return new PackedArguments(args, await computeVarArgsHash(wasm ?? (await CircuitsWasm.get()), args));
}

toBuffer() {

0 comments on commit 05b6e86

Please sign in to comment.