From aca804f96ca9e74b6b553449333e195c0639b151 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 10 Apr 2024 10:22:02 +0100 Subject: [PATCH] feat!: pay fee for account init (#5601) This PR enables accounts to pay tx fees when they're deployed. To achieve this a new deployment method was added that's used by the `AccountManager` class to optionally register/publicly deploy and initialize the target account contract. Entrypoint classes now accept authwits/packed arguments alongside the normal function calls from before. This is needed so that authwits could be created in a parent context and then passed along as transient authwits to the transaction (see `ExecutionRequestInit` wrapper type) Initializing an account contract can use any of the three existing payment methods: - using bridged gas token from L1 - paying privately through a fee payment contract - paying publicly through a fee payment contract In order to use fee payment contracts this PR adds `noinitcheck` to `spend_private_authwit` and `spend_public_authwit` because it's not possible to read the init nullifier in the current tx (protocol limitation). Instead the contract relies on the note containing the public key to exist to validate that the contract has been initialized correctly. An extra payment flow is tested as well: a third party takes the account's public keys and deploys and initializes the account while paying the associated fee. This simulates the flow where a deployment service is used that takes payment through a side chain (e.g. fiat). Breaking change: moved `DefaultMultiCallEntrypoint` to aztec.js This PR supersedes #5540 and #5543. Fix #5190 #5191 #5544. --- .circleci/config.yml | 12 + noir-projects/aztec-nr/authwit/src/account.nr | 10 + .../aztec/src/context/private_context.nr | 15 + .../ecdsa_account_contract/src/main.nr | 8 + .../schnorr_account_contract/src/main.nr | 21 +- .../src/main.nr | 6 + .../src/main.nr | 6 + .../src/defaults/account_interface.ts | 8 +- yarn-project/aztec.js/src/account/contract.ts | 8 +- .../account_manager/deploy_account_method.ts | 71 ++++ .../account_manager/deploy_account_sent_tx.ts | 11 +- .../aztec.js/src/account_manager/index.ts | 60 ++-- .../aztec.js/src/contract/batch_call.ts | 5 +- .../contract/contract_function_interaction.ts | 5 +- .../aztec.js/src/contract/deploy_method.ts | 60 +++- .../src/entrypoint/default_entrypoint.ts | 26 +- .../default_multi_call_entrypoint.ts} | 23 +- .../aztec.js/src/entrypoint/entrypoint.ts | 38 +- .../aztec.js/src/entrypoint/payload.ts | 144 ++++++++ .../aztec.js/src/wallet/account_wallet.ts | 6 +- .../aztec.js/src/wallet/base_wallet.ts | 4 +- .../aztec.js/src/wallet/signerless_wallet.ts | 8 +- yarn-project/aztec/src/sandbox.ts | 4 +- yarn-project/cli/src/cmds/create_account.ts | 2 +- .../src/e2e_account_init_fees.test.ts | 337 ++++++++++++++++++ .../src/e2e_dapp_subscription.test.ts | 4 +- .../end-to-end/src/e2e_p2p_network.test.ts | 2 +- yarn-project/end-to-end/src/fixtures/utils.ts | 11 +- yarn-project/entrypoints/package.json | 3 +- .../entrypoints/src/account_entrypoint.ts | 20 +- .../entrypoints/src/dapp_entrypoint.ts | 17 +- .../entrypoints/src/entrypoint_payload.ts | 121 ------- yarn-project/foundation/src/abi/abi.ts | 20 +- 33 files changed, 834 insertions(+), 262 deletions(-) create mode 100644 yarn-project/aztec.js/src/account_manager/deploy_account_method.ts rename yarn-project/{entrypoints/src/multi_call_entrypoint.ts => aztec.js/src/entrypoint/default_multi_call_entrypoint.ts} (79%) create mode 100644 yarn-project/aztec.js/src/entrypoint/payload.ts create mode 100644 yarn-project/end-to-end/src/e2e_account_init_fees.test.ts delete mode 100644 yarn-project/entrypoints/src/entrypoint_payload.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 3fe7d604c70..7d46584e12d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -997,6 +997,16 @@ jobs: aztec_manifest_key: end-to-end <<: *defaults_e2e_test + e2e-account-init-fees: + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_account_init_fees.test.ts ENABLE_GAS=1 + aztec_manifest_key: end-to-end + <<: *defaults_e2e_test + e2e-dapp-subscription: steps: - *checkout @@ -1480,6 +1490,7 @@ workflows: - e2e-card-game: *e2e_test - e2e-avm-simulator: *e2e_test - e2e-fees: *e2e_test + - e2e-account-init-fees: *e2e_test - e2e-dapp-subscription: *e2e_test - pxe: *e2e_test - cli-docs-sandbox: *e2e_test @@ -1546,6 +1557,7 @@ workflows: - e2e-card-game - e2e-avm-simulator - e2e-fees + - e2e-account-init-fees - e2e-dapp-subscription - pxe - boxes-vanilla diff --git a/noir-projects/aztec-nr/authwit/src/account.nr b/noir-projects/aztec-nr/authwit/src/account.nr index 2e33c4a0823..0bc16963e25 100644 --- a/noir-projects/aztec-nr/authwit/src/account.nr +++ b/noir-projects/aztec-nr/authwit/src/account.nr @@ -70,6 +70,16 @@ impl AccountActions { } // docs:end:entrypoint + pub fn pay_init_fee(self, fee_payload: FeePayload) { + let valid_fn = self.is_valid_impl; + let mut private_context = self.context.private.unwrap(); + + let fee_hash = fee_payload.hash(); + assert(valid_fn(private_context, fee_hash)); + fee_payload.execute_calls(private_context); + private_context.capture_min_revertible_side_effect_counter(); + } + // docs:start:spend_private_authwit pub fn spend_private_authwit(self, inner_hash: Field) -> Field { let context = self.context.private.unwrap(); diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index f9de0291c38..c3ec9a75604 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -347,6 +347,19 @@ impl PrivateContext { assert_eq(item.public_inputs.start_side_effect_counter, self.side_effect_counter); self.side_effect_counter = item.public_inputs.end_side_effect_counter + 1; + // TODO (fees) figure out why this crashes the prover and enable it + // we need this in order to pay fees inside child call contexts + // assert( + // (item.public_inputs.min_revertible_side_effect_counter == 0 as u32) + // | (item.public_inputs.min_revertible_side_effect_counter + // > self.min_revertible_side_effect_counter) + // ); + + // if item.public_inputs.min_revertible_side_effect_counter + // > self.min_revertible_side_effect_counter { + // self.min_revertible_side_effect_counter = item.public_inputs.min_revertible_side_effect_counter; + // } + assert(contract_address.eq(item.contract_address)); assert(function_selector.eq(item.function_data.selector)); @@ -370,6 +383,8 @@ impl PrivateContext { ); } + // crate::oracle::debug_log::debug_log_array_with_prefix("Private call stack item", item.serialize()); + self.private_call_stack_hashes.push(item.hash()); item.public_inputs.return_values diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr index d7da24dbf67..e0cd1cdaf90 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr @@ -40,6 +40,14 @@ contract EcdsaAccount { } #[aztec(private)] + #[aztec(noinitcheck)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.pay_init_fee(fee_payload); + } + + #[aztec(private)] + #[aztec(noinitcheck)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); actions.spend_private_authwit(inner_hash) diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index bc1d49931d8..f0b3f2c8ddd 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -38,6 +38,7 @@ contract SchnorrAccount { // Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts file #[aztec(private)] + #[aztec(noinitcheck)] fn entrypoint(app_payload: pub AppPayload, fee_payload: pub FeePayload) { let actions = AccountActions::private( &mut context, @@ -48,6 +49,18 @@ contract SchnorrAccount { } #[aztec(private)] + #[aztec(noinitcheck)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private( + &mut context, + storage.approved_actions.storage_slot, + is_valid_impl + ); + actions.pay_init_fee(fee_payload); + } + + #[aztec(private)] + #[aztec(noinitcheck)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private( &mut context, @@ -58,6 +71,7 @@ contract SchnorrAccount { } #[aztec(public)] + #[aztec(noinitcheck)] fn spend_public_authwit(inner_hash: Field) -> Field { let actions = AccountActions::public( &mut context, @@ -75,6 +89,7 @@ contract SchnorrAccount { #[aztec(public)] #[aztec(internal)] + #[aztec(noinitcheck)] fn approve_public_authwit(outer_hash: Field) { let actions = AccountActions::public( &mut context, @@ -118,9 +133,9 @@ contract SchnorrAccount { * @param block_number The block number to check the nullifier against * @param check_private Whether to check the validity of the authwitness in private state or not * @param message_hash The message hash of the message to check the validity - * @return An array of two booleans, the first is the validity of the authwitness in the private state, + * @return An array of two booleans, the first is the validity of the authwitness in the private state, * the second is the validity of the authwitness in the public state - * Both values will be `false` if the nullifier is spent + * Both values will be `false` if the nullifier is spent */ unconstrained fn lookup_validity( myself: AztecAddress, @@ -148,7 +163,7 @@ contract SchnorrAccount { let valid_in_public = storage.approved_actions.at(message_hash).read(); // Compute the nullifier and check if it is spent - // This will BLINDLY TRUST the oracle, but the oracle is us, and + // This will BLINDLY TRUST the oracle, but the oracle is us, and // it is not as part of execution of the contract, so we are good. let siloed_nullifier = compute_siloed_nullifier(myself, message_hash); let lower_wit = get_low_nullifier_membership_witness(block_number, siloed_nullifier); diff --git a/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr index a660670a2a5..134a820b058 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr @@ -21,6 +21,12 @@ contract SchnorrHardcodedAccount { actions.entrypoint(app_payload, fee_payload); } + #[aztec(private)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.pay_init_fee(fee_payload); + } + #[aztec(private)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); diff --git a/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr index 9803ed4f15e..2a8678ce778 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr @@ -17,6 +17,12 @@ contract SchnorrSingleKeyAccount { actions.entrypoint(app_payload, fee_payload); } + #[aztec(private)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.pay_init_fee(fee_payload); + } + #[aztec(private)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); diff --git a/yarn-project/accounts/src/defaults/account_interface.ts b/yarn-project/accounts/src/defaults/account_interface.ts index 44e68db64e2..5d7fa311c6e 100644 --- a/yarn-project/accounts/src/defaults/account_interface.ts +++ b/yarn-project/accounts/src/defaults/account_interface.ts @@ -1,6 +1,6 @@ import { type AccountInterface, type AuthWitnessProvider } from '@aztec/aztec.js/account'; -import { type EntrypointInterface, type FeeOptions } from '@aztec/aztec.js/entrypoint'; -import { type AuthWitness, type FunctionCall, type TxExecutionRequest } from '@aztec/circuit-types'; +import { type EntrypointInterface, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { type AuthWitness, type TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecAddress, type CompleteAddress, Fr } from '@aztec/circuits.js'; import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; import { type NodeInfo } from '@aztec/types/interfaces'; @@ -29,8 +29,8 @@ export class DefaultAccountInterface implements AccountInterface { this.version = new Fr(nodeInfo.protocolVersion); } - createTxExecutionRequest(executions: FunctionCall[], fee?: FeeOptions): Promise { - return this.entrypoint.createTxExecutionRequest(executions, fee); + createTxExecutionRequest(execution: ExecutionRequestInit): Promise { + return this.entrypoint.createTxExecutionRequest(execution); } createAuthWit(messageHash: Fr): Promise { diff --git a/yarn-project/aztec.js/src/account/contract.ts b/yarn-project/aztec.js/src/account/contract.ts index 0711292bbac..6c49a3b5cf0 100644 --- a/yarn-project/aztec.js/src/account/contract.ts +++ b/yarn-project/aztec.js/src/account/contract.ts @@ -2,7 +2,7 @@ import { type CompleteAddress } from '@aztec/circuit-types'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { type NodeInfo } from '@aztec/types/interfaces'; -import { type AccountInterface } from './interface.js'; +import { type AccountInterface, type AuthWitnessProvider } from './interface.js'; // docs:start:account-contract-interface /** @@ -29,5 +29,11 @@ export interface AccountContract { * @returns An account interface instance for creating tx requests and authorizing actions. */ getInterface(address: CompleteAddress, nodeInfo: NodeInfo): AccountInterface; + + /** + * Returns the auth witness provider for the given address. + * @param address - Address for which to create auth witnesses. + */ + getAuthWitnessProvider(address: CompleteAddress): AuthWitnessProvider; } // docs:end:account-contract-interface diff --git a/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts b/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts new file mode 100644 index 00000000000..75a2c6bd0d1 --- /dev/null +++ b/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts @@ -0,0 +1,71 @@ +import { type PublicKey } from '@aztec/circuit-types'; +import { FunctionData } from '@aztec/circuits.js'; +import { + type ContractArtifact, + type FunctionArtifact, + encodeArguments, + getFunctionArtifact, +} from '@aztec/foundation/abi'; + +import { type AuthWitnessProvider } from '../account/interface.js'; +import { type Wallet } from '../account/wallet.js'; +import { type ExecutionRequestInit } from '../api/entrypoint.js'; +import { Contract } from '../contract/contract.js'; +import { DeployMethod, type DeployOptions } from '../contract/deploy_method.js'; +import { EntrypointPayload } from '../entrypoint/payload.js'; + +/** + * Contract interaction for deploying an account contract. Handles fee preparation and contract initialization. + */ +export class DeployAccountMethod extends DeployMethod { + #authWitnessProvider: AuthWitnessProvider; + #feePaymentArtifact: FunctionArtifact | undefined; + + constructor( + authWitnessProvider: AuthWitnessProvider, + publicKey: PublicKey, + wallet: Wallet, + artifact: ContractArtifact, + args: any[] = [], + constructorNameOrArtifact?: string | FunctionArtifact, + feePaymentNameOrArtifact?: string | FunctionArtifact, + ) { + super( + publicKey, + wallet, + artifact, + (address, wallet) => Contract.at(address, artifact, wallet), + args, + constructorNameOrArtifact, + ); + + this.#authWitnessProvider = authWitnessProvider; + this.#feePaymentArtifact = + typeof feePaymentNameOrArtifact === 'string' + ? getFunctionArtifact(artifact, feePaymentNameOrArtifact) + : feePaymentNameOrArtifact; + } + + protected async getInitializeFunctionCalls(options: DeployOptions): Promise { + const exec = await super.getInitializeFunctionCalls(options); + + if (options.fee && this.#feePaymentArtifact) { + const { address } = this.getInstance(); + const feePayload = await EntrypointPayload.fromFeeOptions(options?.fee); + + exec.calls.push({ + to: address, + args: encodeArguments(this.#feePaymentArtifact, [feePayload]), + functionData: FunctionData.fromAbi(this.#feePaymentArtifact), + }); + + exec.authWitnesses ??= []; + exec.packedArguments ??= []; + + exec.authWitnesses.push(await this.#authWitnessProvider.createAuthWit(feePayload.hash())); + exec.packedArguments.push(...feePayload.packedArguments); + } + + return exec; + } +} diff --git a/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts b/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts index cfaa577ea8e..ad299a68b5b 100644 --- a/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts +++ b/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts @@ -1,4 +1,4 @@ -import { type TxHash, type TxReceipt } from '@aztec/circuit-types'; +import { type PXE, type TxHash, type TxReceipt } from '@aztec/circuit-types'; import { type FieldsOf } from '@aztec/foundation/types'; import { type Wallet } from '../account/index.js'; @@ -15,8 +15,8 @@ export type DeployAccountTxReceipt = FieldsOf & { * A deployment transaction for an account contract sent to the network, extending SentTx with methods to get the resulting wallet. */ export class DeployAccountSentTx extends SentTx { - constructor(private wallet: Wallet, txHashPromise: Promise) { - super(wallet, txHashPromise); + constructor(pxe: PXE, txHashPromise: Promise, private getWalletPromise: Promise) { + super(pxe, txHashPromise); } /** @@ -36,7 +36,8 @@ export class DeployAccountSentTx extends SentTx { */ public async wait(opts: WaitOpts = DefaultWaitOpts): Promise { const receipt = await super.wait(opts); - await waitForAccountSynch(this.pxe, this.wallet.getCompleteAddress(), opts); - return { ...receipt, wallet: this.wallet }; + const wallet = await this.getWalletPromise; + await waitForAccountSynch(this.pxe, wallet.getCompleteAddress(), opts); + return { ...receipt, wallet }; } } diff --git a/yarn-project/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index d18bc8b38d2..a8c96b6887f 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -6,14 +6,20 @@ import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { type AccountContract } from '../account/contract.js'; import { type Salt } from '../account/index.js'; import { type AccountInterface } from '../account/interface.js'; -import { type DeployMethod } from '../contract/deploy_method.js'; +import { type DeployOptions } from '../contract/deploy_method.js'; import { DefaultWaitOpts, type WaitOpts } from '../contract/sent_tx.js'; -import { ContractDeployer } from '../deployment/contract_deployer.js'; +import { DefaultMultiCallEntrypoint } from '../entrypoint/default_multi_call_entrypoint.js'; import { waitForAccountSynch } from '../utils/account.js'; import { generatePublicKey } from '../utils/index.js'; import { AccountWalletWithPrivateKey, SignerlessWallet } from '../wallet/index.js'; +import { DeployAccountMethod } from './deploy_account_method.js'; import { DeployAccountSentTx } from './deploy_account_sent_tx.js'; +/** + * Options to deploy an account contract. + */ +export type DeployAccountOptions = Pick; + /** * Manages a user account. Provides methods for calculating the account's address, deploying the account contract, * and creating and registering the user wallet in the PXE Service. @@ -26,7 +32,7 @@ export class AccountManager { private completeAddress?: CompleteAddress; private instance?: ContractInstanceWithAddress; private encryptionPublicKey?: PublicKey; - private deployMethod?: DeployMethod; + private deployMethod?: DeployAccountMethod; constructor( private pxe: PXE, @@ -128,17 +134,22 @@ export class AccountManager { } await this.#register(); const encryptionPublicKey = this.getEncryptionPublicKey(); - // We use a signerless wallet so we hit the account contract directly and it deploys itself. + const { chainId, protocolVersion } = await this.pxe.getNodeInfo(); + const deployWallet = new SignerlessWallet(this.pxe, new DefaultMultiCallEntrypoint(chainId, protocolVersion)); + + // We use a signerless wallet with the multi call entrypoint in order to make multiple calls in one go // If we used getWallet, the deployment would get routed via the account contract entrypoint - // instead of directly hitting the initializer. - const deployWallet = new SignerlessWallet(this.pxe); - const deployer = new ContractDeployer( - this.accountContract.getContractArtifact(), - deployWallet, + // and it can't be used unless the contract is initialized + const args = this.accountContract.getDeploymentArgs() ?? []; + this.deployMethod = new DeployAccountMethod( + this.accountContract.getAuthWitnessProvider(this.getCompleteAddress()), encryptionPublicKey, + deployWallet, + this.accountContract.getContractArtifact(), + args, + 'constructor', + 'pay_init_fee', ); - const args = this.accountContract.getDeploymentArgs() ?? []; - this.deployMethod = deployer.deploy(...args); } return this.deployMethod; } @@ -148,18 +159,23 @@ export class AccountManager { * Does not register the associated class nor publicly deploy the instance by default. * Uses the salt provided in the constructor or a randomly generated one. * Registers the account in the PXE Service before deploying the contract. + * @param opts - Fee options to be used for the deployment. * @returns A SentTx object that can be waited to get the associated Wallet. */ - public async deploy(): Promise { - const deployMethod = await this.getDeployMethod(); - const wallet = await this.getWallet(); - const sentTx = deployMethod.send({ - contractAddressSalt: this.salt, - skipClassRegistration: true, - skipPublicDeployment: true, - universalDeploy: true, - }); - return new DeployAccountSentTx(wallet, sentTx.getTxHash()); + public deploy(opts?: DeployAccountOptions): DeployAccountSentTx { + const sentTx = this.getDeployMethod() + .then(deployMethod => + deployMethod.send({ + contractAddressSalt: this.salt, + skipClassRegistration: opts?.skipClassRegistration ?? true, + skipPublicDeployment: opts?.skipPublicDeployment ?? true, + skipInitialization: false, + universalDeploy: true, + fee: opts?.fee, + }), + ) + .then(tx => tx.getTxHash()); + return new DeployAccountSentTx(this.pxe, sentTx, this.getWallet()); } /** @@ -170,7 +186,7 @@ export class AccountManager { * @returns A Wallet instance. */ public async waitSetup(opts: WaitOpts = DefaultWaitOpts): Promise { - await (this.isDeployable() ? this.deploy().then(tx => tx.wait(opts)) : this.register()); + await (this.isDeployable() ? this.deploy().wait(opts) : this.register()); return this.getWallet(); } diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index b6a9c29f5c5..79cead71f4d 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -17,7 +17,10 @@ export class BatchCall extends BaseContractInteraction { */ public async create(opts?: SendMethodOptions): Promise { if (!this.txRequest) { - this.txRequest = await this.wallet.createTxExecutionRequest(this.calls, opts?.fee); + this.txRequest = await this.wallet.createTxExecutionRequest({ + calls: this.calls, + fee: opts?.fee, + }); } return this.txRequest; } diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index a79e8c652f4..be919ff40b4 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -47,7 +47,10 @@ export class ContractFunctionInteraction extends BaseContractInteraction { throw new Error("Can't call `create` on an unconstrained function."); } if (!this.txRequest) { - this.txRequest = await this.wallet.createTxExecutionRequest([this.request()], opts?.fee); + this.txRequest = await this.wallet.createTxExecutionRequest({ + calls: [this.request()], + fee: opts?.fee, + }); } return this.txRequest; } diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index eea033bce7f..32007df8945 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -14,6 +14,7 @@ import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { type Wallet } from '../account/index.js'; import { deployInstance } from '../deployment/deploy_instance.js'; import { registerContractClass } from '../deployment/register_class.js'; +import { type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; import { BaseContractInteraction, type SendMethodOptions } from './base_contract_interaction.js'; import { type Contract } from './contract.js'; import { type ContractBase } from './contract_base.js'; @@ -53,7 +54,7 @@ export class DeployMethod extends Bas private constructorArtifact: FunctionArtifact | undefined; /** Cached call to request() */ - private functionCalls: FunctionCall[] | undefined; + private functionCalls?: ExecutionRequestInit; private log = createDebugLogger('aztec:js:deploy_method'); @@ -80,10 +81,6 @@ export class DeployMethod extends Bas */ public async create(options: DeployOptions = {}): Promise { if (!this.txRequest) { - const calls = await this.request(options); - if (calls.length === 0) { - throw new Error(`No function calls needed to deploy contract ${this.artifact.name}`); - } this.txRequest = await this.wallet.createTxExecutionRequest(await this.request(options)); // TODO: Should we add the contracts to the DB here, or once the tx has been sent or mined? await this.pxe.registerContract({ artifact: this.artifact, instance: this.instance! }); @@ -99,20 +96,21 @@ export class DeployMethod extends Bas * @remarks This method does not have the same return type as the `request` in the ContractInteraction object, * it returns a promise for an array instead of a function call directly. */ - public async request(options: DeployOptions = {}): Promise { + public async request(options: DeployOptions = {}): Promise { if (!this.functionCalls) { - const { address } = this.getInstance(options); - const calls = await this.getDeploymentFunctionCalls(options); - if (this.constructorArtifact && !options.skipInitialization) { - const constructorCall = new ContractFunctionInteraction( - this.wallet, - address, - this.constructorArtifact, - this.args, - ); - calls.push(constructorCall.request()); + const deployment = await this.getDeploymentFunctionCalls(options); + const bootstrap = await this.getInitializeFunctionCalls(options); + + if (deployment.calls.length + bootstrap.calls.length === 0) { + throw new Error(`No function calls needed to deploy contract ${this.artifact.name}`); } - this.functionCalls = calls; + + this.functionCalls = { + calls: [...deployment.calls, ...bootstrap.calls], + authWitnesses: [...(deployment.authWitnesses ?? []), ...(bootstrap.authWitnesses ?? [])], + packedArguments: [...(deployment.packedArguments ?? []), ...(bootstrap.packedArguments ?? [])], + fee: options.fee, + }; } return this.functionCalls; } @@ -122,7 +120,7 @@ export class DeployMethod extends Bas * @param options - Deployment options. * @returns A function call array with potentially requests to the class registerer and instance deployer. */ - protected async getDeploymentFunctionCalls(options: DeployOptions = {}): Promise { + protected async getDeploymentFunctionCalls(options: DeployOptions = {}): Promise { const calls: FunctionCall[] = []; // Set contract instance object so it's available for populating the DeploySendTx object @@ -156,7 +154,31 @@ export class DeployMethod extends Bas calls.push(deployInstance(this.wallet, instance).request()); } - return calls; + return { + calls, + }; + } + + /** + * Returns the calls necessary to initialize the contract. + * @param options - Deployment options. + * @returns - An array of function calls. + */ + protected getInitializeFunctionCalls(options: DeployOptions): Promise { + const { address } = this.getInstance(options); + const calls: FunctionCall[] = []; + if (this.constructorArtifact && !options.skipInitialization) { + const constructorCall = new ContractFunctionInteraction( + this.wallet, + address, + this.constructorArtifact, + this.args, + ); + calls.push(constructorCall.request()); + } + return Promise.resolve({ + calls, + }); } /** diff --git a/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts index a88cfef9e7a..e7a43da5903 100644 --- a/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts @@ -1,7 +1,7 @@ -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { TxContext } from '@aztec/circuits.js'; -import { type EntrypointInterface } from './entrypoint.js'; +import { type EntrypointInterface, type ExecutionRequestInit } from './entrypoint.js'; /** * Default implementation of the entrypoint interface. It calls a function on a contract directly @@ -9,18 +9,24 @@ import { type EntrypointInterface } from './entrypoint.js'; export class DefaultEntrypoint implements EntrypointInterface { constructor(private chainId: number, private protocolVersion: number) {} - createTxExecutionRequest(executions: FunctionCall[]): Promise { - const [execution] = executions; - const packedArguments = PackedArguments.fromArgs(execution.args); + createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + const { calls, authWitnesses = [], packedArguments = [] } = exec; + + if (calls.length > 1) { + throw new Error(`Expected a single call, got ${calls.length}`); + } + + const call = calls[0]; + const entrypointPackedArguments = PackedArguments.fromArgs(call.args); const txContext = TxContext.empty(this.chainId, this.protocolVersion); return Promise.resolve( new TxExecutionRequest( - execution.to, - execution.functionData, - packedArguments.hash, + call.to, + call.functionData, + entrypointPackedArguments.hash, txContext, - [packedArguments], - [], + [...packedArguments, entrypointPackedArguments], + authWitnesses, ), ); } diff --git a/yarn-project/entrypoints/src/multi_call_entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts similarity index 79% rename from yarn-project/entrypoints/src/multi_call_entrypoint.ts rename to yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts index 86e3d82e0ea..1156e733fae 100644 --- a/yarn-project/entrypoints/src/multi_call_entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts @@ -1,35 +1,32 @@ -import { type EntrypointInterface } from '@aztec/aztec.js/entrypoint'; -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { type EntrypointInterface, EntrypointPayload, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecAddress, FunctionData, TxContext } from '@aztec/circuits.js'; import { type FunctionAbi, encodeArguments } from '@aztec/foundation/abi'; import { getCanonicalMultiCallEntrypointAddress } from '@aztec/protocol-contracts/multi-call-entrypoint'; -import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; -import { buildAppPayload } from './entrypoint_payload.js'; - /** * Implementation for an entrypoint interface that can execute multiple function calls in a single transaction */ export class DefaultMultiCallEntrypoint implements EntrypointInterface { constructor( + private chainId: number, + private version: number, private address: AztecAddress = getCanonicalMultiCallEntrypointAddress(), - private chainId: number = DEFAULT_CHAIN_ID, - private version: number = DEFAULT_VERSION, ) {} - createTxExecutionRequest(executions: FunctionCall[]): Promise { - const { payload: appPayload, packedArguments: appPackedArguments } = buildAppPayload(executions); - + createTxExecutionRequest(executions: ExecutionRequestInit): Promise { + const { calls, authWitnesses = [], packedArguments = [] } = executions; + const payload = EntrypointPayload.fromAppExecution(calls); const abi = this.getEntrypointAbi(); - const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [appPayload])); + const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [payload])); const txRequest = TxExecutionRequest.from({ argsHash: entrypointPackedArgs.hash, origin: this.address, functionData: FunctionData.fromAbi(abi), txContext: TxContext.empty(this.chainId, this.version), - packedArguments: [...appPackedArguments, entrypointPackedArgs], - authWitnesses: [], + packedArguments: [...payload.packedArguments, ...packedArguments, entrypointPackedArgs], + authWitnesses, }); return Promise.resolve(txRequest); diff --git a/yarn-project/aztec.js/src/entrypoint/entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/entrypoint.ts index ece23e67924..9cc6618c04f 100644 --- a/yarn-project/aztec.js/src/entrypoint/entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/entrypoint.ts @@ -1,25 +1,35 @@ -import { type FunctionCall, type TxExecutionRequest } from '@aztec/circuit-types'; -import { type Fr } from '@aztec/foundation/fields'; +import { + type AuthWitness, + type FunctionCall, + type PackedArguments, + type TxExecutionRequest, +} from '@aztec/circuit-types'; -import { type FeePaymentMethod } from '../fee/fee_payment_method.js'; +import { EntrypointPayload, type FeeOptions } from './payload.js'; -/** - * Fee payment options for a transaction. - */ -export type FeeOptions = { - /** The fee payment method to use */ - paymentMethod: FeePaymentMethod; - /** The fee limit to pay */ - maxFee: bigint | number | Fr; +export { EntrypointPayload, FeeOptions }; + +export { DefaultEntrypoint } from './default_entrypoint.js'; +export { DefaultMultiCallEntrypoint } from './default_multi_call_entrypoint.js'; + +/** Encodes the calls to be done in a transaction. */ +export type ExecutionRequestInit = { + /** The function calls to be executed. */ + calls: FunctionCall[]; + /** Any transient auth witnesses needed for this execution */ + authWitnesses?: AuthWitness[]; + /** Any transient packed arguments for this execution */ + packedArguments?: PackedArguments[]; + /** How the fee is going to be payed */ + fee?: FeeOptions; }; /** Creates transaction execution requests out of a set of function calls. */ export interface EntrypointInterface { /** * Generates an execution request out of set of function calls. - * @param executions - The execution intents to be run. - * @param feeOpts - The fee to be paid for the transaction. + * @param execution - The execution intents to be run. * @returns The authenticated transaction execution request. */ - createTxExecutionRequest(executions: FunctionCall[], feeOpts?: FeeOptions): Promise; + createTxExecutionRequest(execution: ExecutionRequestInit): Promise; } diff --git a/yarn-project/aztec.js/src/entrypoint/payload.ts b/yarn-project/aztec.js/src/entrypoint/payload.ts new file mode 100644 index 00000000000..83457a27b07 --- /dev/null +++ b/yarn-project/aztec.js/src/entrypoint/payload.ts @@ -0,0 +1,144 @@ +import { type FunctionCall, PackedArguments, emptyFunctionCall } from '@aztec/circuit-types'; +import { Fr, GeneratorIndex } from '@aztec/circuits.js'; +import { padArrayEnd } from '@aztec/foundation/collection'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { type Tuple } from '@aztec/foundation/serialize'; + +import { type FeePaymentMethod } from '../fee/fee_payment_method.js'; + +/** + * Fee payment options for a transaction. + */ +export type FeeOptions = { + /** The fee payment method to use */ + paymentMethod: FeePaymentMethod; + /** The fee limit to pay */ + maxFee: bigint | number | Fr; +}; + +// These must match the values defined in: +// - noir-projects/aztec-nr/aztec/src/entrypoint/app.nr +const APP_MAX_CALLS = 4; +// - and noir-projects/aztec-nr/aztec/src/entrypoint/fee.nr +const FEE_MAX_CALLS = 2; + +/* eslint-disable camelcase */ +/** Encoded function call for account contract entrypoint */ +type EncodedFunctionCall = { + /** Arguments hash for the call */ + args_hash: Fr; + /** Selector of the function to call */ + function_selector: Fr; + /** Address of the contract to call */ + target_address: Fr; + /** Whether the function is public or private */ + is_public: boolean; +}; +/* eslint-enable camelcase */ + +/** Assembles an entrypoint payload */ +export class EntrypointPayload { + #packedArguments: PackedArguments[] = []; + #functionCalls: EncodedFunctionCall[] = []; + #nonce = Fr.random(); + #generatorIndex: number; + + private constructor(functionCalls: FunctionCall[], generatorIndex: number) { + for (const call of functionCalls) { + this.#packedArguments.push(PackedArguments.fromArgs(call.args)); + } + + /* eslint-disable camelcase */ + this.#functionCalls = functionCalls.map((call, index) => ({ + args_hash: this.#packedArguments[index].hash, + function_selector: call.functionData.selector.toField(), + target_address: call.to.toField(), + is_public: !call.functionData.isPrivate, + })); + /* eslint-enable camelcase */ + + this.#generatorIndex = generatorIndex; + } + + /* eslint-disable camelcase */ + /** + * The function calls to execute. This uses snake_case naming so that it is compatible with Noir encoding + * @internal + */ + get function_calls() { + return this.#functionCalls; + } + /* eslint-enable camelcase */ + + /** + * The nonce + * @internal + */ + get nonce() { + return this.#nonce; + } + + /** + * The packed arguments for the function calls + */ + get packedArguments() { + return this.#packedArguments; + } + + /** + * Serializes the payload to an array of fields + * @returns The fields of the payload + */ + toFields(): Fr[] { + return [ + ...this.#functionCalls.flatMap(call => [ + call.args_hash, + call.function_selector, + call.target_address, + new Fr(call.is_public), + ]), + this.#nonce, + ]; + } + + /** + * Hashes the payload + * @returns The hash of the payload + */ + hash() { + return pedersenHash(this.toFields(), this.#generatorIndex); + } + + /** + * Creates an execution payload from a set of function calls + * @param functionCalls - The function calls to execute + * @returns The execution payload + */ + static fromFunctionCalls(functionCalls: FunctionCall[]) { + return new EntrypointPayload(functionCalls, 0); + } + + /** + * Creates an execution payload for the app-portion of a transaction from a set of function calls + * @param functionCalls - The function calls to execute + * @returns The execution payload + */ + static fromAppExecution(functionCalls: FunctionCall[] | Tuple) { + if (functionCalls.length > APP_MAX_CALLS) { + throw new Error(`Expected at most ${APP_MAX_CALLS} function calls, got ${functionCalls.length}`); + } + const paddedCalls = padArrayEnd(functionCalls, emptyFunctionCall(), APP_MAX_CALLS); + return new EntrypointPayload(paddedCalls, GeneratorIndex.SIGNATURE_PAYLOAD); + } + + /** + * Creates an execution payload to pay the fee for a transaction + * @param feeOpts - The fee payment options + * @returns The execution payload + */ + static async fromFeeOptions(feeOpts?: FeeOptions) { + const calls = feeOpts ? await feeOpts.paymentMethod.getFunctionCalls(new Fr(feeOpts.maxFee)) : []; + const paddedCalls = padArrayEnd(calls, emptyFunctionCall(), FEE_MAX_CALLS); + return new EntrypointPayload(paddedCalls, GeneratorIndex.FEE_PAYLOAD); + } +} diff --git a/yarn-project/aztec.js/src/wallet/account_wallet.ts b/yarn-project/aztec.js/src/wallet/account_wallet.ts index b75c81fca70..40ac9986477 100644 --- a/yarn-project/aztec.js/src/wallet/account_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/account_wallet.ts @@ -4,7 +4,7 @@ import { type ABIParameterVisibility, type FunctionAbi, FunctionType } from '@az import { type AccountInterface } from '../account/interface.js'; import { ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; -import { type FeeOptions } from '../entrypoint/entrypoint.js'; +import { type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; import { computeAuthWitMessageHash } from '../utils/authwit.js'; import { BaseWallet } from './base_wallet.js'; @@ -16,8 +16,8 @@ export class AccountWallet extends BaseWallet { super(pxe); } - createTxExecutionRequest(execs: FunctionCall[], fee?: FeeOptions): Promise { - return this.account.createTxExecutionRequest(execs, fee); + createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + return this.account.createTxExecutionRequest(exec); } getChainId(): Fr { diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index e5d5775eaa9..70106a6da67 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -28,7 +28,7 @@ import { type NodeInfo } from '@aztec/types/interfaces'; import { type Wallet } from '../account/wallet.js'; import { type ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; -import { type FeeOptions } from '../entrypoint/entrypoint.js'; +import { type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; /** * A base class for Wallet implementations @@ -42,7 +42,7 @@ export abstract class BaseWallet implements Wallet { abstract getVersion(): Fr; - abstract createTxExecutionRequest(execs: FunctionCall[], fee?: FeeOptions): Promise; + abstract createTxExecutionRequest(exec: ExecutionRequestInit): Promise; abstract createAuthWit( messageHashOrIntent: diff --git a/yarn-project/aztec.js/src/wallet/signerless_wallet.ts b/yarn-project/aztec.js/src/wallet/signerless_wallet.ts index 63ef84ae128..062c4491956 100644 --- a/yarn-project/aztec.js/src/wallet/signerless_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/signerless_wallet.ts @@ -1,8 +1,8 @@ -import { type AuthWitness, type FunctionCall, type PXE, type TxExecutionRequest } from '@aztec/circuit-types'; +import { type AuthWitness, type PXE, type TxExecutionRequest } from '@aztec/circuit-types'; import { type CompleteAddress, type Fr } from '@aztec/circuits.js'; import { DefaultEntrypoint } from '../entrypoint/default_entrypoint.js'; -import { type EntrypointInterface } from '../entrypoint/entrypoint.js'; +import { type EntrypointInterface, type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; import { BaseWallet } from './base_wallet.js'; /** @@ -13,14 +13,14 @@ export class SignerlessWallet extends BaseWallet { super(pxe); } - async createTxExecutionRequest(executions: FunctionCall[]): Promise { + async createTxExecutionRequest(execution: ExecutionRequestInit): Promise { let entrypoint = this.entrypoint; if (!entrypoint) { const { chainId, protocolVersion } = await this.pxe.getNodeInfo(); entrypoint = new DefaultEntrypoint(chainId, protocolVersion); } - return entrypoint.createTxExecutionRequest(executions); + return entrypoint.createTxExecutionRequest(execution); } getChainId(): Fr { diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index 7ee93045b6e..25493c9d5f6 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -2,8 +2,8 @@ import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; import { type AztecAddress, BatchCall, SignerlessWallet, type Wallet } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; +import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; import { type AztecNode } from '@aztec/circuit-types'; -import { DefaultMultiCallEntrypoint } from '@aztec/entrypoints/multi-call'; import { type DeployL1Contracts, type L1ContractAddresses, @@ -212,7 +212,7 @@ export async function createSandbox(config: Partial = {}) { if (config.enableGas) { await deployCanonicalL2GasToken( - new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint()), + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(aztecNodeConfig.chainId, aztecNodeConfig.version)), aztecNodeConfig.l1Contracts, ); } diff --git a/yarn-project/cli/src/cmds/create_account.ts b/yarn-project/cli/src/cmds/create_account.ts index 34a99f24e44..0b3ecbb3166 100644 --- a/yarn-project/cli/src/cmds/create_account.ts +++ b/yarn-project/cli/src/cmds/create_account.ts @@ -17,7 +17,7 @@ export async function createAccount( const account = getSchnorrAccount(client, actualPrivateKey, actualPrivateKey, Fr.ZERO); const { address, publicKey, partialAddress } = account.getCompleteAddress(); - const tx = await account.deploy(); + const tx = account.deploy(); const txHash = await tx.getTxHash(); debugLogger.verbose(`Account contract tx sent with hash ${txHash}`); if (wait) { diff --git a/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts b/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts new file mode 100644 index 00000000000..f5552190e01 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts @@ -0,0 +1,337 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountManager, + type DebugLogger, + ExtendedNote, + Fr, + NativeFeePaymentMethod, + Note, + PrivateFeePaymentMethod, + PublicFeePaymentMethod, + Schnorr, + type TxHash, + TxStatus, + type Wallet, + computeMessageSecretHash, + generatePublicKey, +} from '@aztec/aztec.js'; +import { type AztecAddress, CompleteAddress, Fq, getContractClassFromArtifact } from '@aztec/circuits.js'; +import { + TokenContract as BananaCoin, + FPCContract, + type GasTokenContract, + SchnorrAccountContract, +} from '@aztec/noir-contracts.js'; + +import { jest } from '@jest/globals'; + +import { + type BalancesFn, + type EndToEndContext, + expectMapping, + getBalancesFn, + publicDeployAccounts, + setup, +} from './fixtures/utils.js'; +import { GasPortalTestingHarnessFactory, type IGasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; + +const TOKEN_NAME = 'BananaCoin'; +const TOKEN_SYMBOL = 'BC'; +const TOKEN_DECIMALS = 18n; +const BRIDGED_FPC_GAS = 444n; + +jest.setTimeout(1000_000); + +describe('e2e_fees_account_init', () => { + let ctx: EndToEndContext; + let logger: DebugLogger; + let sequencer: Wallet; + let sequencersAddress: AztecAddress; + let alice: Wallet; + + let gas: GasTokenContract; + let bananaCoin: BananaCoin; + let bananaFPC: FPCContract; + + let gasBridgeTestHarness: IGasBridgingTestHarness; + + let gasBalances: BalancesFn; + let bananaPublicBalances: BalancesFn; + let bananaPrivateBalances: BalancesFn; + + let bobsPrivateEncryptionKey: Fq; + let bobsPrivateSigningKey: Fq; + let bobsAccountManager: AccountManager; + let bobsAddress: AztecAddress; + + let bobsInitialGas: bigint; + let alicesInitialGas: bigint; + let sequencersInitialGas: bigint; + let fpcsInitialGas: bigint; + let fpcsInitialPublicBananas: bigint; + + let maxFee: bigint; + let actualFee: bigint; + + // run this after each test's setup phase to get the initial balances + async function initBalances() { + [[bobsInitialGas, alicesInitialGas, sequencersInitialGas, fpcsInitialGas], [fpcsInitialPublicBananas]] = + await Promise.all([ + gasBalances(bobsAddress, alice.getAddress(), sequencersAddress, bananaFPC.address), + bananaPublicBalances(bananaFPC.address), + ]); + } + + beforeAll(async () => { + ctx = await setup(2); + logger = ctx.logger; + [sequencer, alice] = ctx.wallets; + sequencersAddress = sequencer.getAddress(); + + await ctx.aztecNode.setConfig({ + allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], + feeRecipient: sequencersAddress, + }); + + gasBridgeTestHarness = await GasPortalTestingHarnessFactory.create({ + pxeService: ctx.pxe, + publicClient: ctx.deployL1ContractsValues.publicClient, + walletClient: ctx.deployL1ContractsValues.walletClient, + wallet: ctx.wallet, + logger: ctx.logger, + mockL1: false, + }); + + gas = gasBridgeTestHarness.l2Token; + + bananaCoin = await BananaCoin.deploy(sequencer, sequencersAddress, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS) + .send() + .deployed(); + + logger.verbose(`BananaCoin deployed at ${bananaCoin.address}`); + + bananaFPC = await FPCContract.deploy(sequencer, bananaCoin.address, gas.address).send().deployed(); + logger.verbose(`bananaPay deployed at ${bananaFPC.address}`); + await publicDeployAccounts(sequencer, [sequencer]); + + await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, bananaFPC.address); + + bananaPublicBalances = getBalancesFn('🍌.public', bananaCoin.methods.balance_of_public, logger); + bananaPrivateBalances = getBalancesFn('🍌.private', bananaCoin.methods.balance_of_private, logger); + gasBalances = getBalancesFn('⛽', gas.methods.balance_of_public, logger); + }); + + afterAll(() => ctx.teardown()); + + beforeEach(() => { + maxFee = 3n; + actualFee = 1n; + bobsPrivateEncryptionKey = Fq.random(); + bobsPrivateSigningKey = Fq.random(); + bobsAccountManager = getSchnorrAccount(ctx.pxe, bobsPrivateEncryptionKey, bobsPrivateSigningKey, Fr.random()); + bobsAddress = bobsAccountManager.getCompleteAddress().address; + }); + + describe('account pays its own fee', () => { + describe('in the gas token', () => { + beforeEach(async () => { + await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, bobsAddress); + }); + + beforeEach(initBalances); + + it('account pays for its own fee', async () => { + await bobsAccountManager + .deploy({ + fee: { + maxFee, + paymentMethod: await NativeFeePaymentMethod.create(await bobsAccountManager.getWallet()), + }, + }) + .wait(); + + await expectMapping( + gasBalances, + [bobsAddress, sequencersAddress], + [bobsInitialGas - actualFee, sequencersInitialGas + actualFee], + ); + }); + }); + + describe('privately through an FPC', () => { + let mintedPrivateBananas: bigint; + beforeEach(async () => { + mintedPrivateBananas = 42n; + + // TODO the following sequence of events ends in a timeout + // 1. pxe.registerRecipient (aka just add the public key so pxe can encrypt notes) + // 2. mint note for mew account + // 3. accountManager.register (add pubkey + start a note processor) + // as a workaround, register (pubkey + note processors) the account first, before minting the note + await bobsAccountManager.register(); + + const secret = Fr.random(); + const secretHash = computeMessageSecretHash(secret); + const mintTx = await bananaCoin.methods.mint_private(mintedPrivateBananas, secretHash).send().wait(); + await addTransparentNoteToPxe(sequencersAddress, mintedPrivateBananas, secretHash, mintTx.txHash); + + // at this point, the new account owns a note + // but the account doesn't have a NoteProcessor registered + // so the note exists on the blockchain as an encrypted blob + // tell the pxe to start a note processor for the account ahead of its deployment + await bananaCoin.methods.redeem_shield(bobsAddress, mintedPrivateBananas, secret).send().wait(); + }); + + beforeEach(initBalances); + + it('account pays for its own fee', async () => { + const rebateSecret = Fr.random(); + const tx = await bobsAccountManager + .deploy({ + fee: { + maxFee, + paymentMethod: new PrivateFeePaymentMethod( + bananaCoin.address, + bananaFPC.address, + await bobsAccountManager.getWallet(), + rebateSecret, + ), + }, + }) + .wait(); + + expect(tx.status).toEqual(TxStatus.MINED); + + // the new account should have paid the full fee to the FPC + await expect(bananaPrivateBalances(bobsAddress)).resolves.toEqual([mintedPrivateBananas - maxFee]); + + // the FPC got paid through "unshield", so it's got a new public balance + await expect(bananaPublicBalances(bananaFPC.address)).resolves.toEqual([fpcsInitialPublicBananas + actualFee]); + + // the FPC should have paid the sequencer + await expect(gasBalances(bananaFPC.address, sequencersAddress)).resolves.toEqual([ + fpcsInitialGas - actualFee, + sequencersInitialGas + actualFee, + ]); + + // the new account should have received a refund + await expect( + // this rejects if note can't be added + addTransparentNoteToPxe(bobsAddress, maxFee - actualFee, computeMessageSecretHash(rebateSecret), tx.txHash), + ).resolves.toBeUndefined(); + + // and it can redeem the refund + await bananaCoin.methods + .redeem_shield(bobsAccountManager.getCompleteAddress().address, maxFee - actualFee, rebateSecret) + .send() + .wait(); + + await expect(bananaPrivateBalances(bobsAccountManager.getCompleteAddress().address)).resolves.toEqual([ + mintedPrivateBananas - actualFee, + ]); + }); + }); + + describe('public through an FPC', () => { + let mintedPublicBananas: bigint; + + beforeEach(async () => { + mintedPublicBananas = 37n; + await bananaCoin.methods.mint_public(bobsAddress, mintedPublicBananas).send().wait(); + }); + + beforeEach(initBalances); + + it('account pays for its own fee', async () => { + const tx = await bobsAccountManager + .deploy({ + skipPublicDeployment: false, + fee: { + maxFee, + paymentMethod: new PublicFeePaymentMethod( + bananaCoin.address, + bananaFPC.address, + await bobsAccountManager.getWallet(), + ), + }, + }) + .wait(); + + expect(tx.status).toEqual(TxStatus.MINED); + + // we should have paid the fee to the FPC + await expect( + bananaPublicBalances(bobsAccountManager.getCompleteAddress().address, bananaFPC.address), + ).resolves.toEqual([mintedPublicBananas - actualFee, fpcsInitialPublicBananas + actualFee]); + + // the FPC should have paid the sequencer + await expect(gasBalances(bananaFPC.address, sequencersAddress)).resolves.toEqual([ + fpcsInitialGas - actualFee, + sequencersInitialGas + actualFee, + ]); + }); + }); + }); + + describe('another account pays the fee', () => { + describe('in the gas token', () => { + beforeEach(async () => { + await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, alice.getAddress()); + }); + + beforeEach(initBalances); + + it("alice pays for bob's account", async () => { + // bob generates the private keys for his account on his own + const instance = bobsAccountManager.getInstance(); + + // and gives the public keys to alice + const encPubKey = generatePublicKey(bobsPrivateEncryptionKey); + const signingPubKey = new Schnorr().computePublicKey(bobsPrivateSigningKey); + const completeAddress = CompleteAddress.fromPublicKeyAndInstance(encPubKey, instance); + + // alice registers the keys in the PXE + await ctx.pxe.registerRecipient(completeAddress); + + // and deploys bob's account, paying the fee from her balance + const tx = await SchnorrAccountContract.deployWithPublicKey(encPubKey, alice, signingPubKey.x, signingPubKey.y) + .send({ + contractAddressSalt: instance.salt, + skipClassRegistration: true, + skipPublicDeployment: true, + skipInitialization: false, + universalDeploy: true, + fee: { + maxFee, + paymentMethod: await NativeFeePaymentMethod.create(alice), + }, + }) + .wait(); + + expect(tx.status).toBe(TxStatus.MINED); + + await expectMapping( + gasBalances, + [alice.getAddress(), bobsAddress, sequencersAddress], + [alicesInitialGas - actualFee, bobsInitialGas, sequencersInitialGas + actualFee], + ); + + // bob can now use his wallet + const bobsWallet = await bobsAccountManager.getWallet(); + await expect(gas.withWallet(bobsWallet).methods.balance_of_public(alice.getAddress()).simulate()).resolves.toBe( + alicesInitialGas - actualFee, + ); + }); + }); + }); + + async function addTransparentNoteToPxe(owner: AztecAddress, amount: bigint, secretHash: Fr, txHash: TxHash) { + const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. + const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote + + const note = new Note([new Fr(amount), secretHash]); + // this note isn't encrypted but we need to provide a registered public key + const extendedNote = new ExtendedNote(note, owner, bananaCoin.address, storageSlot, noteTypeId, txHash); + await ctx.pxe.addNote(extendedNote); + } +}); diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts index 90348fa8d28..3c6406ae255 100644 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts @@ -209,7 +209,7 @@ describe('e2e_dapp_subscription', () => { it('should call dapp subscription entrypoint', async () => { const dappPayload = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); const action = counterContract.methods.increment(bobAddress).request(); - const txExReq = await dappPayload.createTxExecutionRequest([action]); + const txExReq = await dappPayload.createTxExecutionRequest({ calls: [action] }); const tx = await pxe.proveTx(txExReq, true); const sentTx = new SentTx(pxe, pxe.sendTx(tx)); await sentTx.wait(); @@ -263,7 +263,7 @@ describe('e2e_dapp_subscription', () => { async function dappIncrement() { const dappEntrypoint = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); const action = counterContract.methods.increment(bobAddress).request(); - const txExReq = await dappEntrypoint.createTxExecutionRequest([action]); + const txExReq = await dappEntrypoint.createTxExecutionRequest({ calls: [action] }); const tx = await pxe.proveTx(txExReq, true); const sentTx = new SentTx(pxe, pxe.sendTx(tx)); return sentTx.wait(); diff --git a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts index 7f70195b940..812350b4015 100644 --- a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts @@ -121,7 +121,7 @@ describe('e2e_p2p_network', () => { const submitTxsTo = async (pxe: PXEService, account: AztecAddress, numTxs: number) => { const txs: SentTx[] = []; for (let i = 0; i < numTxs; i++) { - const tx = await getSchnorrAccount(pxe, GrumpkinScalar.random(), GrumpkinScalar.random(), Fr.random()).deploy(); + const tx = getSchnorrAccount(pxe, GrumpkinScalar.random(), GrumpkinScalar.random(), Fr.random()).deploy(); logger.info(`Tx sent with hash ${await tx.getTxHash()}`); const receipt = await tx.getReceipt(); expect(receipt).toEqual( diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 39b4fba0df9..c4938e4376e 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -27,7 +27,7 @@ import { waitForPXE, } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; -import { DefaultMultiCallEntrypoint } from '@aztec/entrypoints/multi-call'; +import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; import { randomBytes } from '@aztec/foundation/crypto'; import { AvailabilityOracleAbi, @@ -260,9 +260,12 @@ async function setupWithRemoteEnvironment( const teardown = () => Promise.resolve(); if (['1', 'true'].includes(ENABLE_GAS)) { + const { chainId, protocolVersion } = await pxeClient.getNodeInfo(); // this contract might already have been deployed // the following function is idempotent - await deployCanonicalGasToken(new SignerlessWallet(pxeClient, new DefaultMultiCallEntrypoint())); + await deployCanonicalGasToken( + new SignerlessWallet(pxeClient, new DefaultMultiCallEntrypoint(chainId, protocolVersion)), + ); } return { @@ -377,7 +380,9 @@ export async function setup( const { pxe, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); if (['1', 'true'].includes(ENABLE_GAS)) { - await deployCanonicalGasToken(new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint())); + await deployCanonicalGasToken( + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(config.chainId, config.version)), + ); } const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); diff --git a/yarn-project/entrypoints/package.json b/yarn-project/entrypoints/package.json index 7e62b41ff6d..3ee6b4bbe95 100644 --- a/yarn-project/entrypoints/package.json +++ b/yarn-project/entrypoints/package.json @@ -6,8 +6,7 @@ "type": "module", "exports": { "./dapp": "./dest/dapp_entrypoint.js", - "./account": "./dest/account_entrypoint.js", - "./multi-call": "./dest/multi_call_entrypoint.js" + "./account": "./dest/account_entrypoint.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/entrypoints/src/account_entrypoint.ts b/yarn-project/entrypoints/src/account_entrypoint.ts index acc3c011e4e..8d222f71b99 100644 --- a/yarn-project/entrypoints/src/account_entrypoint.ts +++ b/yarn-project/entrypoints/src/account_entrypoint.ts @@ -1,11 +1,10 @@ import { type AuthWitnessProvider } from '@aztec/aztec.js/account'; -import { type EntrypointInterface, type FeeOptions } from '@aztec/aztec.js/entrypoint'; -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; -import { type AztecAddress, FunctionData, GeneratorIndex, TxContext } from '@aztec/circuits.js'; +import { type EntrypointInterface, EntrypointPayload, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { type AztecAddress, FunctionData, TxContext } from '@aztec/circuits.js'; import { type FunctionAbi, encodeArguments } from '@aztec/foundation/abi'; import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; -import { buildAppPayload, buildFeePayload, hashPayload } from './entrypoint_payload.js'; /** * Implementation for an entrypoint interface that follows the default entrypoint signature @@ -19,22 +18,23 @@ export class DefaultAccountEntrypoint implements EntrypointInterface { private version: number = DEFAULT_VERSION, ) {} - async createTxExecutionRequest(executions: FunctionCall[], feeOpts?: FeeOptions): Promise { - const { payload: appPayload, packedArguments: appPackedArguments } = buildAppPayload(executions); - const { payload: feePayload, packedArguments: feePackedArguments } = await buildFeePayload(feeOpts); + async createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + const { calls, fee } = exec; + const appPayload = EntrypointPayload.fromAppExecution(calls); + const feePayload = await EntrypointPayload.fromFeeOptions(fee); const abi = this.getEntrypointAbi(); const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [appPayload, feePayload])); - const appAuthWitness = await this.auth.createAuthWit(hashPayload(appPayload, GeneratorIndex.SIGNATURE_PAYLOAD)); - const feeAuthWitness = await this.auth.createAuthWit(hashPayload(feePayload, GeneratorIndex.FEE_PAYLOAD)); + const appAuthWitness = await this.auth.createAuthWit(appPayload.hash()); + const feeAuthWitness = await this.auth.createAuthWit(feePayload.hash()); const txRequest = TxExecutionRequest.from({ argsHash: entrypointPackedArgs.hash, origin: this.address, functionData: FunctionData.fromAbi(abi), txContext: TxContext.empty(this.chainId, this.version), - packedArguments: [...appPackedArguments, ...feePackedArguments, entrypointPackedArgs], + packedArguments: [...appPayload.packedArguments, ...feePayload.packedArguments, entrypointPackedArgs], authWitnesses: [appAuthWitness, feeAuthWitness], }); diff --git a/yarn-project/entrypoints/src/dapp_entrypoint.ts b/yarn-project/entrypoints/src/dapp_entrypoint.ts index 67e41819e68..f52593ba721 100644 --- a/yarn-project/entrypoints/src/dapp_entrypoint.ts +++ b/yarn-project/entrypoints/src/dapp_entrypoint.ts @@ -1,12 +1,11 @@ import { computeInnerAuthWitHash, computeOuterAuthWitHash } from '@aztec/aztec.js'; import { type AuthWitnessProvider } from '@aztec/aztec.js/account'; -import { type EntrypointInterface } from '@aztec/aztec.js/entrypoint'; -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { type EntrypointInterface, EntrypointPayload, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecAddress, Fr, FunctionData, TxContext } from '@aztec/circuits.js'; import { type FunctionAbi, encodeArguments } from '@aztec/foundation/abi'; import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; -import { buildDappPayload } from './entrypoint_payload.js'; /** * Implementation for an entrypoint interface that follows the default entrypoint signature @@ -21,11 +20,13 @@ export class DefaultDappEntrypoint implements EntrypointInterface { private version: number = DEFAULT_VERSION, ) {} - async createTxExecutionRequest(executions: FunctionCall[]): Promise { - if (executions.length !== 1) { - throw new Error('ILLEGAL'); + async createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + const { calls } = exec; + if (calls.length !== 1) { + throw new Error(`Expected exactly 1 function call, got ${calls.length}`); } - const { payload, packedArguments } = buildDappPayload(executions[0]); + + const payload = EntrypointPayload.fromFunctionCalls(calls); const abi = this.getEntrypointAbi(); const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [payload, this.userAddress])); @@ -47,7 +48,7 @@ export class DefaultDappEntrypoint implements EntrypointInterface { origin: this.dappEntrypointAddress, functionData, txContext: TxContext.empty(this.chainId, this.version), - packedArguments: [...packedArguments, entrypointPackedArgs], + packedArguments: [...payload.packedArguments, entrypointPackedArgs], authWitnesses: [authWitness], }); diff --git a/yarn-project/entrypoints/src/entrypoint_payload.ts b/yarn-project/entrypoints/src/entrypoint_payload.ts deleted file mode 100644 index 894dcdc4f5e..00000000000 --- a/yarn-project/entrypoints/src/entrypoint_payload.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { type FeeOptions } from '@aztec/aztec.js/entrypoint'; -import { Fr } from '@aztec/aztec.js/fields'; -import { type FunctionCall, PackedArguments, emptyFunctionCall } from '@aztec/circuit-types'; -import { type AztecAddress } from '@aztec/circuits.js'; -import { padArrayEnd } from '@aztec/foundation/collection'; -import { pedersenHash } from '@aztec/foundation/crypto'; - -// These must match the values defined in: -// - noir-projects/aztec-nr/aztec/src/entrypoint/app.nr -const ACCOUNT_MAX_CALLS = 4; -// - and noir-projects/aztec-nr/aztec/src/entrypoint/fee.nr -const FEE_MAX_CALLS = 2; - -/** Encoded function call for account contract entrypoint */ -type EntrypointFunctionCall = { - // eslint-disable-next-line camelcase - /** Arguments hash for the call */ - args_hash: Fr; - // eslint-disable-next-line camelcase - /** Selector of the function to call */ - function_selector: Fr; - // eslint-disable-next-line camelcase - /** Address of the contract to call */ - target_address: Fr; - // eslint-disable-next-line camelcase - /** Whether the function is public or private */ - is_public: boolean; -}; - -/** Encoded payload for the account contract entrypoint */ -type EntrypointPayload = { - // eslint-disable-next-line camelcase - /** Encoded function calls to execute */ - function_calls: EntrypointFunctionCall[]; - /** A nonce for replay protection */ - nonce: Fr; -}; - -/** Represents a generic payload to be executed in the context of an account contract */ -export type PayloadWithArguments = { - /** The payload to be run */ - payload: EntrypointPayload; - /** The packed arguments for the function calls */ - packedArguments: PackedArguments[]; -}; - -/** - * Builds a payload to be sent to the account contract - * @param calls - The function calls to run - * @param maxCalls - The maximum number of call expected to be run. Used for padding - * @returns A payload object and packed arguments - */ -function buildPayload(calls: FunctionCall[], maxCalls: number): PayloadWithArguments { - const nonce = Fr.random(); - - const paddedCalls = padArrayEnd(calls, emptyFunctionCall(), maxCalls); - const packedArguments: PackedArguments[] = []; - for (const call of paddedCalls) { - packedArguments.push(PackedArguments.fromArgs(call.args)); - } - - const formattedCalls: EntrypointFunctionCall[] = paddedCalls.map((call, index) => ({ - // eslint-disable-next-line camelcase - args_hash: packedArguments[index].hash, - // eslint-disable-next-line camelcase - function_selector: call.functionData.selector.toField(), - // eslint-disable-next-line camelcase - target_address: call.to.toField(), - // eslint-disable-next-line camelcase - is_public: !call.functionData.isPrivate, - })); - - return { - payload: { - // eslint-disable-next-line camelcase - function_calls: formattedCalls, - nonce, - }, - packedArguments, - }; -} - -/** builds the payload for a Dapp entrypoint */ -export function buildDappPayload(call: FunctionCall): PayloadWithArguments { - return buildPayload([call], 1); -} - -/** Assembles an entrypoint app payload from a set of private and public function calls */ -export function buildAppPayload(calls: FunctionCall[]): PayloadWithArguments { - return buildPayload(calls, ACCOUNT_MAX_CALLS); -} - -/** Creates the payload for paying the fee for a transaction */ -export async function buildFeePayload(feeOpts?: FeeOptions): Promise { - const calls = feeOpts ? await feeOpts.paymentMethod.getFunctionCalls(new Fr(feeOpts.maxFee)) : []; - return buildPayload(calls, FEE_MAX_CALLS); -} - -// TODO (dogfooding) change all of these names app/dapp/fee/payload and generator indices for all of them -/** Hashes a payload to a 32-byte buffer */ -export function hashPayload(payload: EntrypointPayload, generatorIndex: number) { - return pedersenHash(flattenPayload(payload), generatorIndex); -} - -/** Hash the payload for a dapp */ -export function hashDappPayload(payload: EntrypointPayload, userAddress: AztecAddress, generatorIndex: number) { - return pedersenHash([...flattenPayload(payload), userAddress], generatorIndex); -} - -/** Flattens an payload */ -function flattenPayload(payload: EntrypointPayload) { - return [ - ...payload.function_calls.flatMap(call => [ - call.args_hash, - call.function_selector, - call.target_address, - new Fr(call.is_public), - ]), - payload.nonce, - ]; -} diff --git a/yarn-project/foundation/src/abi/abi.ts b/yarn-project/foundation/src/abi/abi.ts index aaa02291c71..1a9fb2b8901 100644 --- a/yarn-project/foundation/src/abi/abi.ts +++ b/yarn-project/foundation/src/abi/abi.ts @@ -383,27 +383,27 @@ export function getDefaultInitializer(contractArtifact: ContractArtifact): Funct /** * Returns an initializer from the contract. - * @param initalizerNameOrArtifact - The name of the constructor, or the artifact of the constructor, or undefined + * @param initializerNameOrArtifact - The name of the constructor, or the artifact of the constructor, or undefined * to pick the default initializer. */ export function getInitializer( contract: ContractArtifact, - initalizerNameOrArtifact: string | undefined | FunctionArtifact, + initializerNameOrArtifact: string | undefined | FunctionArtifact, ): FunctionArtifact | undefined { - if (typeof initalizerNameOrArtifact === 'string') { - const found = contract.functions.find(f => f.name === initalizerNameOrArtifact); + if (typeof initializerNameOrArtifact === 'string') { + const found = contract.functions.find(f => f.name === initializerNameOrArtifact); if (!found) { - throw new Error(`Constructor method ${initalizerNameOrArtifact} not found in contract artifact`); + throw new Error(`Constructor method ${initializerNameOrArtifact} not found in contract artifact`); } else if (!found.isInitializer) { - throw new Error(`Method ${initalizerNameOrArtifact} is not an initializer`); + throw new Error(`Method ${initializerNameOrArtifact} is not an initializer`); } return found; - } else if (initalizerNameOrArtifact === undefined) { + } else if (initializerNameOrArtifact === undefined) { return getDefaultInitializer(contract); } else { - if (!initalizerNameOrArtifact.isInitializer) { - throw new Error(`Method ${initalizerNameOrArtifact.name} is not an initializer`); + if (!initializerNameOrArtifact.isInitializer) { + throw new Error(`Method ${initializerNameOrArtifact.name} is not an initializer`); } - return initalizerNameOrArtifact; + return initializerNameOrArtifact; } }