diff --git a/yarn-project/aztec.js/src/authorization/call_authorization_request.ts b/yarn-project/aztec.js/src/authorization/call_authorization_request.ts index 89e6f06b7bf8..d90881a580a7 100644 --- a/yarn-project/aztec.js/src/authorization/call_authorization_request.ts +++ b/yarn-project/aztec.js/src/authorization/call_authorization_request.ts @@ -1,14 +1,16 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import { FieldReader } from '@aztec/foundation/serialize'; import { AuthorizationSelector, FunctionSelector } from '@aztec/stdlib/abi'; +import { computeInnerAuthWitHash } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { computeVarArgsHash } from '@aztec/stdlib/hash'; /** * An authwit request for a function call. Includes the preimage of the data * to be signed, as opposed of just the inner hash. */ export class CallAuthorizationRequest { - constructor( + private constructor( /** * The selector of the authwit type, used to identify it * when emitted from `emit_offchain_effect`oracle. @@ -38,6 +40,26 @@ export class CallAuthorizationRequest { public args: Fr[], ) {} + /** Validates that innerHash and argsHash are consistent with the provided preimage fields. */ + private async validate(): Promise { + const expectedArgsHash = await computeVarArgsHash(this.args); + if (!expectedArgsHash.equals(this.argsHash)) { + throw new Error( + `CallAuthorizationRequest argsHash mismatch: expected ${expectedArgsHash.toString()}, got ${this.argsHash.toString()}`, + ); + } + const expectedInnerHash = await computeInnerAuthWitHash([ + this.msgSender.toField(), + this.functionSelector.toField(), + this.argsHash, + ]); + if (!expectedInnerHash.equals(this.innerHash)) { + throw new Error( + `CallAuthorizationRequest innerHash mismatch: expected ${expectedInnerHash.toString()}, got ${this.innerHash.toString()}`, + ); + } + } + static getSelector(): Promise { return AuthorizationSelector.fromSignature('CallAuthorization((Field),(u32),Field)'); } @@ -51,7 +73,7 @@ export class CallAuthorizationRequest { `Invalid authorization selector for CallAuthwit: expected ${expectedSelector.toString()}, got ${selector.toString()}`, ); } - return new CallAuthorizationRequest( + const request = new CallAuthorizationRequest( selector, reader.readField(), AztecAddress.fromField(reader.readField()), @@ -59,5 +81,7 @@ export class CallAuthorizationRequest { reader.readField(), reader.readFieldArray(reader.remainingFields()), ); + await request.validate(); + return request; } } diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index a1493d0cc13c..3cdb6f9fead1 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -13,16 +13,16 @@ import { AccountManager, type Aliased, type SimulateOptions } from '@aztec/aztec import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { LogFn } from '@aztec/foundation/log'; -import type { AccessScopes, NotesFilter } from '@aztec/pxe/client/lazy'; +import type { NotesFilter } from '@aztec/pxe/client/lazy'; import type { PXEConfig } from '@aztec/pxe/config'; import type { PXE } from '@aztec/pxe/server'; import { createPXE, getPXEConfig } from '@aztec/pxe/server'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { NoteDao } from '@aztec/stdlib/note'; -import type { TxProvingResult, TxSimulationResult } from '@aztec/stdlib/tx'; +import type { SimulationOverrides, TxProvingResult, TxSimulationResult } from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; -import { BaseWallet, type FeeOptions } from '@aztec/wallet-sdk/base-wallet'; +import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet'; import type { WalletDB } from '../storage/wallet_db.js'; import type { AccountType } from './constants.js'; @@ -224,21 +224,19 @@ export class CLIWallet extends BaseWallet { */ protected override async simulateViaEntrypoint( executionPayload: ExecutionPayload, - from: AztecAddress, - feeOptions: FeeOptions, - scopes: AccessScopes, - skipTxValidation?: boolean, - skipFeeEnforcement?: boolean, + opts: SimulateViaEntrypointOptions, ): Promise { - if (from.equals(AztecAddress.ZERO)) { - return super.simulateViaEntrypoint( - executionPayload, - from, - feeOptions, - scopes, - skipTxValidation, - skipFeeEnforcement, - ); + const { from, feeOptions, scopes } = opts; + let overrides: SimulationOverrides | undefined; + let fromAccount: Account; + if (!from.equals(AztecAddress.ZERO)) { + const { account, instance, artifact } = await this.getFakeAccountDataFor(from); + fromAccount = account; + overrides = { + contracts: { [from.toString()]: { instance, artifact } }, + }; + } else { + fromAccount = await this.getAccountFromAddress(from); } const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload(); @@ -251,7 +249,6 @@ export class CLIWallet extends BaseWallet { ? mergeExecutionPayloads([feeExecutionPayload, executionPayload]) : executionPayload; - const { account: fromAccount, instance, artifact } = await this.getFakeAccountDataFor(from); const chainInfo = await this.getChainInfo(); const txRequest = await fromAccount.createTxExecutionRequest( finalExecutionPayload, @@ -263,9 +260,7 @@ export class CLIWallet extends BaseWallet { simulatePublic: true, skipFeeEnforcement: true, skipTxValidation: true, - overrides: { - contracts: { [from.toString()]: { instance, artifact } }, - }, + overrides, scopes, }); } diff --git a/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts b/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts index 4cdd919dacad..683bcbb3fb32 100644 --- a/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts +++ b/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts @@ -9,6 +9,8 @@ import { PendingNoteHashesContract } from '@aztec/noir-test-contracts.js/Pending import { type AbiDecoded, decodeFromAbi, getFunctionArtifact } from '@aztec/stdlib/abi'; import { computeOuterAuthWitHash } from '@aztec/stdlib/auth-witness'; +import { jest } from '@jest/globals'; + import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js'; import { setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; @@ -108,7 +110,7 @@ describe('Kernelless simulation', () => { nonceForAuthwits, ); - wallet.enableSimulatedSimulations(); + wallet.setSimulationMode('kernelless-override'); const { offchainEffects } = await addLiquidityInteraction.simulate({ from: liquidityProviderAddress, @@ -216,7 +218,7 @@ describe('Kernelless simulation', () => { ).resolves.toBeDefined(); }); - it('produces matching gas estimates between kernelless and with-kernels simulation', async () => { + it('produces matching gas estimates and fee payer between kernelless and with-kernels simulation', async () => { const swapperBalancesBefore = await getWalletBalances(swapperAddress); const ammBalancesBefore = await getAmmBalances(); @@ -236,27 +238,27 @@ describe('Kernelless simulation', () => { nonceForAuthwits, ); - wallet.enableSimulatedSimulations(); - const swapKernellessGas = ( - await swapExactTokensInteraction.simulate({ - from: swapperAddress, - includeMetadata: true, - }) - ).estimatedGas!; + const simulateTxSpy = jest.spyOn(wallet, 'simulateTx'); + + wallet.setSimulationMode('kernelless-override'); + const kernellessResult = await swapExactTokensInteraction.simulate({ + from: swapperAddress, + includeMetadata: true, + }); + const swapKernellessGas = kernellessResult.estimatedGas!; const swapAuthwit = await wallet.createAuthWit(swapperAddress, { caller: amm.address, action: token0.methods.transfer_to_public(swapperAddress, amm.address, amountIn, nonceForAuthwits), }); - wallet.disableSimulatedSimulations(); - const swapWithKernelsGas = ( - await swapExactTokensInteraction.simulate({ - from: swapperAddress, - includeMetadata: true, - authWitnesses: [swapAuthwit], - }) - ).estimatedGas!; + wallet.setSimulationMode('full'); + const withKernelsResult = await swapExactTokensInteraction.simulate({ + from: swapperAddress, + includeMetadata: true, + authWitnesses: [swapAuthwit], + }); + const swapWithKernelsGas = withKernelsResult.estimatedGas!; logger.info(`Kernelless gas: L2=${swapKernellessGas.gasLimits.l2Gas} DA=${swapKernellessGas.gasLimits.daGas}`); logger.info( @@ -265,6 +267,16 @@ describe('Kernelless simulation', () => { expect(swapKernellessGas.gasLimits.daGas).toEqual(swapWithKernelsGas.gasLimits.daGas); expect(swapKernellessGas.gasLimits.l2Gas).toEqual(swapWithKernelsGas.gasLimits.l2Gas); + + expect(simulateTxSpy).toHaveBeenCalledTimes(2); + const kernellessTxResult = await (simulateTxSpy.mock.results[0].value as ReturnType); + const withKernelsTxResult = await (simulateTxSpy.mock.results[1].value as ReturnType); + const kernellessFeePayer = kernellessTxResult.publicInputs.feePayer; + const withKernelsFeePayer = withKernelsTxResult.publicInputs.feePayer; + expect(kernellessFeePayer).toEqual(withKernelsFeePayer); + expect(kernellessFeePayer).toEqual(swapperAddress); + + simulateTxSpy.mockRestore(); }); }); @@ -288,7 +300,7 @@ describe('Kernelless simulation', () => { await pendingNoteHashesContract.methods.get_then_nullify_note.selector(), ); - wallet.enableSimulatedSimulations(); + wallet.setSimulationMode('kernelless-override'); const kernellessGas = ( await interaction.simulate({ from: adminAddress, @@ -296,7 +308,7 @@ describe('Kernelless simulation', () => { }) ).estimatedGas!; - wallet.disableSimulatedSimulations(); + wallet.setSimulationMode('full'); const withKernelsGas = ( await interaction.simulate({ from: adminAddress, @@ -325,14 +337,14 @@ describe('Kernelless simulation', () => { const mintAmount = 100n; // Insert a note with real kernels so it lands on-chain - wallet.disableSimulatedSimulations(); + wallet.setSimulationMode('full'); await pendingNoteHashesContract.methods.insert_note(mintAmount, adminAddress, adminAddress).send({ from: adminAddress, }); // Kernelless simulation of reading + nullifying that settled note produces a settled // read request that gets verified against the note hash tree at the anchor block - wallet.enableSimulatedSimulations(); + wallet.setSimulationMode('kernelless-override'); await expect( pendingNoteHashesContract.methods.get_then_nullify_note(mintAmount, adminAddress).simulate({ from: adminAddress, diff --git a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts index 24dc0adb6c85..e1082cdc1a14 100644 --- a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts @@ -16,7 +16,7 @@ import { AccountManager, type SendOptions } from '@aztec/aztec.js/wallet'; import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account'; import { Fq, Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; -import type { AccessScopes, NotesFilter } from '@aztec/pxe/client/lazy'; +import type { NotesFilter } from '@aztec/pxe/client/lazy'; import { type PXEConfig, getPXEConfig } from '@aztec/pxe/config'; import { PXE, type PXECreationOptions, createPXE } from '@aztec/pxe/server'; import { AuthWitness } from '@aztec/stdlib/auth-witness'; @@ -24,9 +24,9 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import type { NoteDao } from '@aztec/stdlib/note'; -import type { BlockHeader, TxHash, TxReceipt, TxSimulationResult } from '@aztec/stdlib/tx'; +import type { BlockHeader, SimulationOverrides, TxHash, TxReceipt, TxSimulationResult } from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; -import { BaseWallet, type FeeOptions } from '@aztec/wallet-sdk/base-wallet'; +import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet'; import { AztecNodeProxy, ProvenTx } from './utils.js'; @@ -125,21 +125,15 @@ export class TestWallet extends BaseWallet { protected accounts: Map = new Map(); /** - * Toggle for running "simulated simulations" when calling simulateTx. - * - * When this flag is true, simulateViaEntrypoint constructs a request using a fake account - * (and accepts contract overrides on the input) and the PXE emulates kernel effects without - * generating kernel witnesses. When false, simulateViaEntrypoint defers to the standard - * simulation path via the real account entrypoint. + * Controls how the test wallet simulates transactions: + * - `kernelless`: Skips kernel circuits but uses the real account contract. Default. + * - `kernelless-override`: Skips kernels and replaces the account with a stub that doesn't do authwit validation. + * - `full`: Uses real kernel circuits and real account contracts. Slow! */ - private simulatedSimulations = false; + private simulationMode: 'kernelless' | 'kernelless-override' | 'full' = 'kernelless'; - enableSimulatedSimulations() { - this.simulatedSimulations = true; - } - - disableSimulatedSimulations() { - this.simulatedSimulations = false; + setSimulationMode(mode: 'kernelless' | 'kernelless-override' | 'full') { + this.simulationMode = mode; } setMinFeePadding(value?: number) { @@ -220,27 +214,24 @@ export class TestWallet extends BaseWallet { return account.createAuthWit(intentInnerHash, chainInfo); } - /** - * Override simulateViaEntrypoint to use fake accounts for kernelless simulation - * when simulatedSimulations is enabled. Otherwise falls through to the real entrypoint path. - */ protected override async simulateViaEntrypoint( executionPayload: ExecutionPayload, - from: AztecAddress, - feeOptions: FeeOptions, - scopes: AccessScopes, - skipTxValidation?: boolean, - skipFeeEnforcement?: boolean, + opts: SimulateViaEntrypointOptions, ): Promise { - if (!this.simulatedSimulations) { - return super.simulateViaEntrypoint( - executionPayload, - from, - feeOptions, - scopes, - skipTxValidation, - skipFeeEnforcement, - ); + const { from, feeOptions, scopes, skipTxValidation, skipFeeEnforcement } = opts; + const skipKernels = this.simulationMode !== 'full'; + const useOverride = this.simulationMode === 'kernelless-override' && !from.equals(AztecAddress.ZERO); + + let overrides: SimulationOverrides | undefined; + let fromAccount: Account; + if (useOverride) { + const { account, instance, artifact } = await this.getFakeAccountDataFor(from); + fromAccount = account; + overrides = { + contracts: { [from.toString()]: { instance, artifact } }, + }; + } else { + fromAccount = await this.getAccountFromAddress(from); } const feeExecutionPayload = await feeOptions.walletFeePaymentMethod?.getExecutionPayload(); @@ -252,7 +243,6 @@ export class TestWallet extends BaseWallet { const finalExecutionPayload = feeExecutionPayload ? mergeExecutionPayloads([feeExecutionPayload, executionPayload]) : executionPayload; - const { account: fromAccount, instance, artifact } = await this.getFakeAccountDataFor(from); const chainInfo = await this.getChainInfo(); const txRequest = await fromAccount.createTxExecutionRequest( finalExecutionPayload, @@ -260,14 +250,12 @@ export class TestWallet extends BaseWallet { chainInfo, executionOptions, ); - const contractOverrides = { - [from.toString()]: { instance, artifact }, - }; return this.pxe.simulateTx(txRequest, { simulatePublic: true, - skipFeeEnforcement: true, - skipTxValidation: true, - overrides: { contracts: contractOverrides }, + skipKernels, + skipFeeEnforcement, + skipTxValidation, + overrides, scopes, }); } diff --git a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts index 6b1bd80e6baf..046a2ccb9f30 100644 --- a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts +++ b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts @@ -448,6 +448,8 @@ export async function generateSimulatedProvingResult( privateExecutionResult.entrypoint.publicInputs.anchorBlockHeader.globalVariables.timestamp + BigInt(MAX_TX_LIFETIME); + let feePayer = AztecAddress.zero(); + const executions = [privateExecutionResult.entrypoint]; while (executions.length !== 0) { @@ -462,6 +464,13 @@ export async function generateSimulatedProvingResult( const { contractAddress } = execution.publicInputs.callContext; + if (execution.publicInputs.isFeePayer) { + if (!feePayer.isZero()) { + throw new Error('Multiple fee payers found in private execution result'); + } + feePayer = contractAddress; + } + scopedNoteHashes.push( ...execution.publicInputs.noteHashes .getActiveItems() @@ -682,7 +691,7 @@ export async function generateSimulatedProvingResult( daGas: TX_DA_GAS_OVERHEAD, }), ), - /*feePayer=*/ AztecAddress.zero(), + /*feePayer=*/ feePayer, /*expirationTimestamp=*/ expirationTimestamp, hasPublicCalls ? inputsForPublic : undefined, !hasPublicCalls ? inputsForRollup : undefined, diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 8e065f0db345..37ac0238cf25 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -80,6 +80,16 @@ export type FeeOptions = { gasSettings: GasSettings; }; +/** Options for `simulateViaEntrypoint`. */ +export type SimulateViaEntrypointOptions = Pick< + SimulateOptions, + 'from' | 'additionalScopes' | 'skipTxValidation' | 'skipFeeEnforcement' +> & { + /** Fee options for the entrypoint */ + feeOptions: FeeOptions; + /** Scopes to use for the simulation */ + scopes: AccessScopes; +}; /** * A base class for Wallet implementations */ @@ -300,22 +310,20 @@ export abstract class BaseWallet implements Wallet { /** * Simulates calls through the standard PXE path (account entrypoint). * @param executionPayload - The execution payload to simulate. - * @param from - The sender address. - * @param feeOptions - Fee options for the transaction. - * @param skipTxValidation - Whether to skip tx validation. - * @param skipFeeEnforcement - Whether to skip fee enforcement. - * @param scopes - The scopes to use for the simulation. + * @param opts - Simulation options. */ - protected async simulateViaEntrypoint( - executionPayload: ExecutionPayload, - from: AztecAddress, - feeOptions: FeeOptions, - scopes: AccessScopes, - skipTxValidation?: boolean, - skipFeeEnforcement?: boolean, - ) { - const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, from, feeOptions); - return this.pxe.simulateTx(txRequest, { simulatePublic: true, skipTxValidation, skipFeeEnforcement, scopes }); + protected async simulateViaEntrypoint(executionPayload: ExecutionPayload, opts: SimulateViaEntrypointOptions) { + const txRequest = await this.createTxExecutionRequestFromPayloadAndFee( + executionPayload, + opts.from, + opts.feeOptions, + ); + return this.pxe.simulateTx(txRequest, { + simulatePublic: true, + skipTxValidation: opts.skipTxValidation, + skipFeeEnforcement: opts.skipFeeEnforcement, + scopes: opts.scopes, + }); } /** @@ -357,14 +365,13 @@ export abstract class BaseWallet implements Wallet { ) : Promise.resolve([]), remainingCalls.length > 0 - ? this.simulateViaEntrypoint( - remainingPayload, - opts.from, + ? this.simulateViaEntrypoint(remainingPayload, { + from: opts.from, feeOptions, - this.scopesFrom(opts.from, opts.additionalScopes), - opts.skipTxValidation, - opts.skipFeeEnforcement ?? true, - ) + scopes: this.scopesFrom(opts.from, opts.additionalScopes), + skipTxValidation: opts.skipTxValidation, + skipFeeEnforcement: opts.skipFeeEnforcement ?? true, + }) : Promise.resolve(null), ]); diff --git a/yarn-project/wallet-sdk/src/base-wallet/index.ts b/yarn-project/wallet-sdk/src/base-wallet/index.ts index 4b6718cae7ae..224088457a7c 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/index.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/index.ts @@ -1,2 +1,2 @@ -export { BaseWallet, type FeeOptions } from './base_wallet.js'; +export { BaseWallet, type FeeOptions, type SimulateViaEntrypointOptions } from './base_wallet.js'; export { simulateViaNode, buildMergedSimulationResult, extractOptimizablePublicStaticCalls } from './utils.js'; diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index df2a73b9ecf1..dbc63b3deef8 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -1,22 +1,26 @@ import { type Account, SignerlessAccount } from '@aztec/aztec.js/account'; -import type { Aliased } from '@aztec/aztec.js/wallet'; +import { CallAuthorizationRequest } from '@aztec/aztec.js/authorization'; +import { type InteractionWaitOptions, type SendReturn, getGasLimits } from '@aztec/aztec.js/contracts'; +import type { Aliased, SendOptions } from '@aztec/aztec.js/wallet'; import { AccountManager } from '@aztec/aztec.js/wallet'; import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account'; import { Fq, Fr } from '@aztec/foundation/curves/bn254'; import type { Logger } from '@aztec/foundation/log'; -import type { AccessScopes, PXEConfig, PXECreationOptions } from '@aztec/pxe/client/lazy'; +import type { PXEConfig, PXECreationOptions } from '@aztec/pxe/client/lazy'; import type { PXE } from '@aztec/pxe/server'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract'; +import { GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { ExecutionPayload, SimulationOverrides, type TxSimulationResult, + collectOffchainEffects, mergeExecutionPayloads, } from '@aztec/stdlib/tx'; -import { BaseWallet, type FeeOptions } from '@aztec/wallet-sdk/base-wallet'; +import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet'; import type { AccountContractsProvider } from './account-contract-providers/types.js'; import { type AccountType, WalletDB } from './wallet_db.js'; @@ -32,7 +36,11 @@ export type EmbeddedWalletOptions = { pxeOptions?: PXECreationOptions; }; +const DEFAULT_ESTIMATED_GAS_PADDING = 0.1; + export class EmbeddedWallet extends BaseWallet { + protected estimatedGasPadding = DEFAULT_ESTIMATED_GAS_PADDING; + constructor( pxe: PXE, aztecNode: AztecNode, @@ -79,6 +87,66 @@ export class EmbeddedWallet extends BaseWallet { return storedSenders; } + /** + * Overrides the base sendTx to add a pre-simulation step before the actual send. The simulation + * estimates actual gas usage and captures call authorization requests to generate + * the necessary authwitnesses. + */ + public override async sendTx( + executionPayload: ExecutionPayload, + opts: SendOptions, + ): Promise> { + const feeOptions = await this.completeFeeOptionsForEstimation( + opts.from, + executionPayload.feePayer, + opts.fee?.gasSettings, + ); + + // Simulate the transaction first to estimate gas and capture required + // private authwitnesses based on offchain effects. + const simulationResult = await this.simulateViaEntrypoint(executionPayload, { + from: opts.from, + feeOptions, + scopes: this.scopesFrom(opts.from, opts.additionalScopes), + skipTxValidation: true, + }); + + const offchainEffects = collectOffchainEffects(simulationResult.privateExecutionResult); + const authWitnesses = await Promise.all( + offchainEffects.map(async effect => { + try { + const authRequest = await CallAuthorizationRequest.fromFields(effect.data); + return this.createAuthWit(opts.from, { + consumer: effect.contractAddress, + innerHash: authRequest.innerHash, + }); + } catch { + return undefined; + } + }), + ); + for (const authwit of authWitnesses) { + if (authwit) { + executionPayload.authWitnesses.push(authwit); + } + } + const estimated = getGasLimits(simulationResult, this.estimatedGasPadding); + this.log.verbose( + `Estimated gas limits for tx: DA=${estimated.gasLimits.daGas} L2=${estimated.gasLimits.l2Gas} teardownDA=${estimated.teardownGasLimits.daGas} teardownL2=${estimated.teardownGasLimits.l2Gas}`, + ); + const gasSettings = GasSettings.from({ + ...opts.fee?.gasSettings, + maxFeesPerGas: feeOptions.gasSettings.maxFeesPerGas, + maxPriorityFeesPerGas: feeOptions.gasSettings.maxPriorityFeesPerGas, + gasLimits: opts.fee?.gasSettings?.gasLimits ?? estimated.gasLimits, + teardownGasLimits: opts.fee?.gasSettings?.teardownGasLimits ?? estimated.teardownGasLimits, + }); + return super.sendTx(executionPayload, { + ...opts, + fee: { ...opts.fee, gasSettings }, + }); + } + /** * Simulates calls via a stub account entrypoint, bypassing real account authorization. * This allows kernelless simulation with contract overrides, skipping expensive @@ -86,12 +154,10 @@ export class EmbeddedWallet extends BaseWallet { */ protected override async simulateViaEntrypoint( executionPayload: ExecutionPayload, - from: AztecAddress, - feeOptions: FeeOptions, - scopes: AccessScopes, - skipTxValidation?: boolean, - skipFeeEnforcement?: boolean, + opts: SimulateViaEntrypointOptions, ): Promise { + const { from, feeOptions, scopes, skipTxValidation, skipFeeEnforcement } = opts; + let overrides: SimulationOverrides | undefined; let fromAccount: Account; if (!from.equals(AztecAddress.ZERO)) { @@ -220,6 +286,10 @@ export class EmbeddedWallet extends BaseWallet { this.minFeePadding = value ?? 0.5; } + setEstimatedGasPadding(value?: number) { + this.estimatedGasPadding = value ?? DEFAULT_ESTIMATED_GAS_PADDING; + } + stop() { return this.pxe.stop(); }