aztec_account/
account.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4use crate::abi::{AbiValue, ContractArtifact};
5use crate::error::Error;
6use crate::fee::GasSettings;
7use crate::tx::{AuthWitness, Capsule, ExecutionPayload, HashedValues, TxContext};
8use crate::types::{AztecAddress, CompleteAddress, ContractInstanceWithAddress, Fr, Salt};
9use crate::wallet::{
10    ChainInfo, MessageHashOrIntent, SendOptions, SendResult, SimulateOptions, TxSimulationResult,
11    Wallet,
12};
13
14use aztec_contract::deployment::{
15    get_contract_instance_from_instantiation_params, ContractInstantiationParams,
16};
17use aztec_fee::FeePaymentMethod;
18
19// ---------------------------------------------------------------------------
20// Supporting types
21// ---------------------------------------------------------------------------
22
23/// Options for account entrypoint execution.
24#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct EntrypointOptions {
27    /// Override the fee payer for this transaction.
28    pub fee_payer: Option<AztecAddress>,
29    /// Gas settings for the entrypoint call.
30    pub gas_settings: Option<GasSettings>,
31    /// Fee payment method for the account entrypoint (0=External, 1=PreexistingFeeJuice, 2=FeeJuiceWithClaim).
32    #[serde(default)]
33    pub fee_payment_method: Option<u8>,
34}
35
36/// A transaction execution request produced by an account's entrypoint.
37///
38/// This mirrors the upstream Aztec `TxExecutionRequest` shape used by PXE.
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct TxExecutionRequest {
42    /// The origin account address.
43    pub origin: AztecAddress,
44    /// Selector of the entrypoint function to execute.
45    pub function_selector: crate::abi::FunctionSelector,
46    /// Hash of the first call's encoded arguments.
47    pub first_call_args_hash: Fr,
48    /// Transaction context (chain info + gas settings).
49    pub tx_context: TxContext,
50    /// Hashed arguments for all calls in the transaction.
51    pub args_of_calls: Vec<HashedValues>,
52    /// Authorization witnesses.
53    pub auth_witnesses: Vec<AuthWitness>,
54    /// Capsules (private data) for the transaction.
55    pub capsules: Vec<Capsule>,
56    /// Salt used to randomize the tx request hash.
57    pub salt: Fr,
58    /// Optional fee payer override (defaults to origin if absent).
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub fee_payer: Option<AztecAddress>,
61}
62
63/// Specification for the initialization function of an account contract.
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct InitializationSpec {
67    /// Name of the initializer function in the contract artifact.
68    pub constructor_name: String,
69    /// Arguments to pass to the initializer function.
70    pub constructor_args: Vec<AbiValue>,
71}
72
73// ---------------------------------------------------------------------------
74// Traits
75// ---------------------------------------------------------------------------
76
77/// Provides authorization witnesses for transaction authentication.
78///
79/// This trait is the foundation of the Aztec account model. Implementations
80/// produce [`AuthWitness`] values that prove the caller is authorized to
81/// execute a given intent.
82#[async_trait]
83pub trait AuthorizationProvider: Send + Sync {
84    /// Create an authorization witness for the given intent.
85    async fn create_auth_wit(
86        &self,
87        intent: MessageHashOrIntent,
88        chain_info: &ChainInfo,
89    ) -> Result<AuthWitness, Error>;
90}
91
92/// An Aztec account -- combines an entrypoint, auth-witness provider, and
93/// complete address.
94///
95/// The `Account` trait is the main abstraction for account-based transaction
96/// creation. It wraps execution payloads through the account's entrypoint
97/// contract, adding authentication and fee payment.
98#[async_trait]
99pub trait Account: Send + Sync + AuthorizationProvider {
100    /// Get the complete address of this account.
101    fn complete_address(&self) -> &CompleteAddress;
102
103    /// Get the account address.
104    fn address(&self) -> AztecAddress;
105
106    /// Create a full transaction execution request from a payload.
107    ///
108    /// This processes the payload through the account's entrypoint,
109    /// adding authentication and gas handling.
110    async fn create_tx_execution_request(
111        &self,
112        exec: ExecutionPayload,
113        gas_settings: GasSettings,
114        chain_info: &ChainInfo,
115        options: EntrypointOptions,
116    ) -> Result<TxExecutionRequest, Error>;
117
118    /// Wrap an execution payload through the account's entrypoint.
119    ///
120    /// Similar to [`Account::create_tx_execution_request`] but returns an
121    /// [`ExecutionPayload`] rather than a full request.
122    async fn wrap_execution_payload(
123        &self,
124        exec: ExecutionPayload,
125        options: EntrypointOptions,
126    ) -> Result<ExecutionPayload, Error>;
127}
128
129/// Defines an account contract type (e.g., Schnorr, ECDSA).
130///
131/// Implementations provide the contract artifact, initialization parameters,
132/// and the ability to produce [`Account`] and [`AuthorizationProvider`]
133/// instances for a given address.
134#[async_trait]
135pub trait AccountContract: Send + Sync {
136    /// Get the contract artifact for this account type.
137    async fn contract_artifact(&self) -> Result<ContractArtifact, Error>;
138
139    /// Get the initialization function name and arguments, if any.
140    async fn initialization_function_and_args(&self) -> Result<Option<InitializationSpec>, Error>;
141
142    /// Create an [`Account`] instance for the given address.
143    fn account(&self, address: CompleteAddress) -> Box<dyn Account>;
144
145    /// Create an [`AuthorizationProvider`] for the given address.
146    fn auth_witness_provider(&self, address: CompleteAddress) -> Box<dyn AuthorizationProvider>;
147}
148
149// ---------------------------------------------------------------------------
150// get_account_contract_address
151// ---------------------------------------------------------------------------
152
153/// Computes the address of an account contract before deployment.
154///
155/// This derives keys from `secret`, gets the contract artifact and initialization spec,
156/// then computes the deterministic address using salt and derived public keys.
157///
158/// Uses the same shared instance-construction path as deployment to avoid
159/// duplicating class-id / init-hash / address derivation logic.
160pub async fn get_account_contract_address(
161    account_contract: &dyn AccountContract,
162    secret: Fr,
163    salt: impl Into<Salt>,
164) -> Result<AztecAddress, Error> {
165    let salt: Fr = salt.into();
166    let derived = aztec_crypto::derive_keys(&secret);
167    let public_keys = derived.public_keys;
168
169    let init_spec = account_contract.initialization_function_and_args().await?;
170    let artifact = account_contract.contract_artifact().await?;
171
172    let constructor_name = init_spec
173        .as_ref()
174        .map(|spec| spec.constructor_name.as_str());
175    let constructor_args = init_spec
176        .as_ref()
177        .map(|spec| spec.constructor_args.clone())
178        .unwrap_or_default();
179
180    let instance = get_contract_instance_from_instantiation_params(
181        &artifact,
182        ContractInstantiationParams {
183            constructor_name,
184            constructor_args,
185            salt,
186            public_keys,
187            deployer: AztecAddress::zero(),
188        },
189    )?;
190
191    Ok(instance.address)
192}
193
194// ---------------------------------------------------------------------------
195// AccountWithSecretKey
196// ---------------------------------------------------------------------------
197
198/// An account paired with its secret key.
199///
200/// Returned by [`AccountManager::account`] and provides both the account
201/// interface and the secret key needed for signing operations.
202pub struct AccountWithSecretKey {
203    /// The underlying account implementation.
204    pub account: Box<dyn Account>,
205    /// The secret key associated with this account.
206    pub secret_key: Fr,
207    /// Deployment salt for this account contract.
208    pub salt: Salt,
209}
210
211impl std::fmt::Debug for AccountWithSecretKey {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("AccountWithSecretKey")
214            .field("secret_key", &self.secret_key)
215            .field("salt", &self.salt)
216            .finish_non_exhaustive()
217    }
218}
219
220#[async_trait]
221impl AuthorizationProvider for AccountWithSecretKey {
222    async fn create_auth_wit(
223        &self,
224        intent: MessageHashOrIntent,
225        chain_info: &ChainInfo,
226    ) -> Result<AuthWitness, Error> {
227        self.account.create_auth_wit(intent, chain_info).await
228    }
229}
230
231#[async_trait]
232impl Account for AccountWithSecretKey {
233    fn complete_address(&self) -> &CompleteAddress {
234        self.account.complete_address()
235    }
236
237    fn address(&self) -> AztecAddress {
238        self.account.address()
239    }
240
241    async fn create_tx_execution_request(
242        &self,
243        exec: ExecutionPayload,
244        gas_settings: GasSettings,
245        chain_info: &ChainInfo,
246        options: EntrypointOptions,
247    ) -> Result<TxExecutionRequest, Error> {
248        self.account
249            .create_tx_execution_request(exec, gas_settings, chain_info, options)
250            .await
251    }
252
253    async fn wrap_execution_payload(
254        &self,
255        exec: ExecutionPayload,
256        options: EntrypointOptions,
257    ) -> Result<ExecutionPayload, Error> {
258        self.account.wrap_execution_payload(exec, options).await
259    }
260}
261
262// ---------------------------------------------------------------------------
263// DeployAccountMethod
264// ---------------------------------------------------------------------------
265
266/// Options specific to account deployment.
267pub struct DeployAccountOptions {
268    /// Skip publishing the contract class (if already published).
269    pub skip_class_publication: bool,
270    /// Skip publishing the contract instance.
271    pub skip_instance_publication: bool,
272    /// Skip calling the constructor.
273    pub skip_initialization: bool,
274    /// Skip registering the contract with the wallet.
275    pub skip_registration: bool,
276    /// Explicit deployer override for third-party deployment.
277    pub from: Option<AztecAddress>,
278    /// Fee payment method.
279    pub fee: Option<std::sync::Arc<dyn aztec_fee::FeePaymentMethod>>,
280    /// Fee entrypoint options override.
281    pub fee_entrypoint_options: Option<crate::entrypoint::DefaultAccountEntrypointOptions>,
282}
283
284impl Default for DeployAccountOptions {
285    fn default() -> Self {
286        Self {
287            skip_class_publication: true,
288            skip_instance_publication: true,
289            skip_initialization: false,
290            skip_registration: false,
291            from: None,
292            fee: None,
293            fee_entrypoint_options: None,
294        }
295    }
296}
297
298impl std::fmt::Debug for DeployAccountOptions {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        f.debug_struct("DeployAccountOptions")
301            .field("skip_class_publication", &self.skip_class_publication)
302            .field("skip_instance_publication", &self.skip_instance_publication)
303            .field("skip_initialization", &self.skip_initialization)
304            .field("from", &self.from)
305            .field("has_fee", &self.fee.is_some())
306            .finish_non_exhaustive()
307    }
308}
309
310/// A deployment method for account contracts.
311///
312/// Wraps the generic [`aztec_contract::deployment::DeployMethod`] with
313/// account-specific fee-payment wrapping via
314/// [`crate::meta_payment::AccountEntrypointMetaPaymentMethod`].
315pub struct DeployAccountMethod<'a, W> {
316    wallet: &'a W,
317    account: std::sync::Arc<dyn Account>,
318    inner: aztec_contract::deployment::DeployMethod<'a, W>,
319    inner_instance: ContractInstanceWithAddress,
320}
321
322impl<W> std::fmt::Debug for DeployAccountMethod<'_, W> {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        f.debug_struct("DeployAccountMethod")
325            .field("inner", &self.inner)
326            .finish_non_exhaustive()
327    }
328}
329
330impl<W: Wallet> DeployAccountMethod<'_, W> {
331    /// Build the full deployment execution payload.
332    ///
333    /// For self-deployed accounts (deployer == zero), fee payment goes after
334    /// the deployment payload because the account contract must exist before
335    /// it can execute its own entrypoint. For third-party deploys, fee goes first.
336    pub async fn request(&self, opts: &DeployAccountOptions) -> Result<ExecutionPayload, Error> {
337        let deploy_opts = self.to_deploy_options(opts);
338        let deploy_payload = self.inner.request(&deploy_opts).await?;
339        let is_self_deploy = opts.from == Some(AztecAddress::zero());
340
341        let fee_payload = match (&opts.fee, is_self_deploy) {
342            (Some(method), true) => {
343                let wrapped = crate::meta_payment::AccountEntrypointMetaPaymentMethod::new(
344                    self.account.clone(),
345                    Some(method.clone()),
346                    opts.fee_entrypoint_options.clone(),
347                );
348                Some(wrapped.get_fee_execution_payload().await?)
349            }
350            (Some(method), false) => Some(method.get_fee_execution_payload().await?),
351            (None, true) => {
352                let wrapped = crate::meta_payment::AccountEntrypointMetaPaymentMethod::new(
353                    self.account.clone(),
354                    None,
355                    Some(crate::entrypoint::DefaultAccountEntrypointOptions {
356                        cancellable: false,
357                        tx_nonce: None,
358                        fee_payment_method_options:
359                            crate::entrypoint::AccountFeePaymentMethodOptions::PreexistingFeeJuice,
360                    }),
361                );
362                Some(wrapped.get_fee_execution_payload().await?)
363            }
364            (None, false) => None,
365        };
366
367        match fee_payload {
368            Some(fee) if is_self_deploy => ExecutionPayload::merge(vec![deploy_payload, fee]),
369            Some(fee) => ExecutionPayload::merge(vec![fee, deploy_payload]),
370            None => Ok(deploy_payload),
371        }
372    }
373
374    /// Simulate the account deployment without sending.
375    pub async fn simulate(
376        &self,
377        opts: &DeployAccountOptions,
378        sim_opts: SimulateOptions,
379    ) -> Result<TxSimulationResult, Error> {
380        let payload = self.request(opts).await?;
381        self.wallet.simulate_tx(payload, sim_opts).await
382    }
383
384    /// Send the account deployment transaction.
385    pub async fn send(
386        &self,
387        opts: &DeployAccountOptions,
388        send_opts: SendOptions,
389    ) -> Result<DeployResult, Error> {
390        let deploy_opts = self.to_deploy_options(opts);
391        let instance = self.inner.get_instance(&deploy_opts)?;
392        let payload = self.request(opts).await?;
393        let send_result = self.wallet.send_tx(payload, send_opts).await?;
394        Ok(DeployResult {
395            send_result,
396            instance,
397        })
398    }
399
400    /// Get the contract instance that will be deployed.
401    pub fn instance(&self) -> &ContractInstanceWithAddress {
402        &self.inner_instance
403    }
404
405    fn to_deploy_options(
406        &self,
407        opts: &DeployAccountOptions,
408    ) -> aztec_contract::deployment::DeployOptions {
409        aztec_contract::deployment::DeployOptions {
410            contract_address_salt: Some(self.inner_instance.inner.salt),
411            skip_class_publication: opts.skip_class_publication,
412            skip_instance_publication: opts.skip_instance_publication,
413            skip_initialization: opts.skip_initialization,
414            skip_registration: opts.skip_registration,
415            universal_deploy: true,
416            from: None,
417        }
418    }
419}
420
421/// The result of an account deployment transaction.
422#[derive(Clone, Debug)]
423pub struct DeployResult {
424    /// The underlying send result (tx hash).
425    pub send_result: SendResult,
426    /// The deployed contract instance with its derived address.
427    pub instance: ContractInstanceWithAddress,
428}
429
430// ---------------------------------------------------------------------------
431// AccountManager
432// ---------------------------------------------------------------------------
433
434/// Manages account creation, address computation, and deployment.
435///
436/// `AccountManager` is the main entry point for working with Aztec accounts.
437/// It combines a wallet backend, a secret key, and an account contract type
438/// to provide deterministic address computation, account instance creation,
439/// and deployment method generation.
440pub struct AccountManager<W> {
441    wallet: W,
442    secret_key: Fr,
443    account_contract: Box<dyn AccountContract>,
444    has_initializer: bool,
445    instance: ContractInstanceWithAddress,
446    salt: Salt,
447}
448
449impl<W: Wallet> AccountManager<W> {
450    /// Create a new account manager.
451    ///
452    /// Validates the account contract by fetching its artifact and
453    /// initializer metadata. Contract instance hashing and key derivation
454    /// are not implemented yet, so the stored instance remains an explicit
455    /// placeholder until the deployment/key path lands.
456    pub async fn create(
457        wallet: W,
458        secret_key: Fr,
459        account_contract: Box<dyn AccountContract>,
460        salt: Option<impl Into<Salt>>,
461    ) -> Result<Self, Error> {
462        let salt: Salt = salt.map(Into::into).unwrap_or_else(Salt::random);
463
464        let artifact = account_contract.contract_artifact().await?;
465        let init_spec = account_contract.initialization_function_and_args().await?;
466
467        if let Some(spec) = &init_spec {
468            let initializer = artifact.find_function(&spec.constructor_name)?;
469            if !initializer.is_initializer {
470                return Err(Error::Abi(format!(
471                    "function '{}' in account artifact '{}' is not marked as an initializer",
472                    spec.constructor_name, artifact.name
473                )));
474            }
475
476            let expected = initializer.parameters.len();
477            let actual = spec.constructor_args.len();
478            if actual != expected {
479                return Err(Error::Abi(format!(
480                    "initializer '{}' in account artifact '{}' expects {expected} argument(s), got {actual}",
481                    spec.constructor_name, artifact.name
482                )));
483            }
484        }
485
486        // Derive keys and compute full contract instance with real address.
487        let derived = aztec_crypto::derive_keys(&secret_key);
488        let public_keys = derived.public_keys;
489
490        let constructor_name = init_spec
491            .as_ref()
492            .map(|spec| spec.constructor_name.as_str());
493        let constructor_args = init_spec
494            .as_ref()
495            .map(|spec| spec.constructor_args.clone())
496            .unwrap_or_default();
497
498        let instance = get_contract_instance_from_instantiation_params(
499            &artifact,
500            ContractInstantiationParams {
501                constructor_name,
502                constructor_args,
503                salt,
504                public_keys,
505                deployer: AztecAddress::zero(),
506            },
507        )?;
508
509        Ok(Self {
510            wallet,
511            secret_key,
512            account_contract,
513            has_initializer: init_spec.is_some(),
514            instance,
515            salt,
516        })
517    }
518
519    /// Get the complete address of this account.
520    ///
521    /// Derives the full key set from the secret key, computes the partial
522    /// address from the contract instance, and returns the complete address.
523    #[allow(clippy::unused_async)]
524    pub async fn complete_address(&self) -> Result<CompleteAddress, Error> {
525        use aztec_core::hash::{
526            compute_address, compute_partial_address, compute_salted_initialization_hash,
527        };
528        use aztec_crypto::derive_keys;
529
530        let derived = derive_keys(&self.secret_key);
531
532        let salted_init_hash = compute_salted_initialization_hash(
533            self.instance.inner.salt,
534            self.instance.inner.initialization_hash,
535            self.instance.inner.deployer,
536        );
537        let partial_address = compute_partial_address(
538            self.instance.inner.original_contract_class_id,
539            salted_init_hash,
540        );
541
542        let address = compute_address(&derived.public_keys, &partial_address)?;
543
544        Ok(CompleteAddress {
545            address,
546            public_keys: derived.public_keys,
547            partial_address,
548        })
549    }
550
551    /// Get the address of this account instance.
552    pub const fn address(&self) -> AztecAddress {
553        self.instance.address
554    }
555
556    /// Get the contract instance for this account.
557    pub const fn instance(&self) -> &ContractInstanceWithAddress {
558        &self.instance
559    }
560
561    /// Get the salt used for this account.
562    pub const fn salt(&self) -> Salt {
563        self.salt
564    }
565
566    /// Get the secret key for this account.
567    pub const fn secret_key(&self) -> Fr {
568        self.secret_key
569    }
570
571    /// Return whether this account contract has an initializer function.
572    pub const fn has_initializer(&self) -> bool {
573        self.has_initializer
574    }
575
576    /// Get an [`Account`] implementation backed by this manager's account
577    /// contract, paired with the secret key.
578    pub async fn account(&self) -> Result<AccountWithSecretKey, Error> {
579        let complete_addr = self.complete_address().await?;
580        Ok(AccountWithSecretKey {
581            account: self.account_contract.account(complete_addr),
582            secret_key: self.secret_key,
583            salt: self.salt,
584        })
585    }
586
587    /// Get a deployment method for this account's contract.
588    ///
589    /// Fetches the artifact and initialization spec from the account contract,
590    /// creates a generic `ContractDeployer` + `DeployMethod`, and wraps it
591    /// with account-specific fee-payment logic in a [`DeployAccountMethod`].
592    pub async fn deploy_method(&self) -> Result<DeployAccountMethod<'_, W>, Error> {
593        let artifact = self.account_contract.contract_artifact().await?;
594        let init_spec = self
595            .account_contract
596            .initialization_function_and_args()
597            .await?;
598
599        let complete_addr = self.complete_address().await?;
600        let account: std::sync::Arc<dyn Account> =
601            std::sync::Arc::from(self.account_contract.account(complete_addr));
602
603        let constructor_args = init_spec
604            .as_ref()
605            .map(|s| s.constructor_args.clone())
606            .unwrap_or_default();
607
608        let mut deployer =
609            aztec_contract::deployment::ContractDeployer::new(artifact, &self.wallet)
610                .with_public_keys(self.instance.inner.public_keys.clone());
611        if let Some(spec) = &init_spec {
612            deployer = deployer.with_constructor_name(spec.constructor_name.clone());
613        }
614
615        let inner = deployer.deploy(constructor_args)?;
616
617        // Get the instance using the account's salt.
618        let deploy_opts = aztec_contract::deployment::DeployOptions {
619            contract_address_salt: Some(self.salt),
620            ..Default::default()
621        };
622        let inner_instance = inner.get_instance(&deploy_opts)?;
623
624        Ok(DeployAccountMethod {
625            wallet: &self.wallet,
626            account,
627            inner,
628            inner_instance,
629        })
630    }
631}
632
633// ---------------------------------------------------------------------------
634// Tests
635// ---------------------------------------------------------------------------
636
637#[cfg(test)]
638#[allow(clippy::unwrap_used, clippy::expect_used)]
639mod tests {
640    use super::*;
641    use crate::abi::{AbiParameter, AbiType, FunctionArtifact, FunctionSelector, FunctionType};
642    use crate::fee::Gas;
643    use crate::types::PublicKeys;
644    use crate::wallet::MockWallet;
645
646    // -- Test helpers: mock account types -----------------------------------
647
648    struct MockAuthProvider {
649        addr: CompleteAddress,
650    }
651
652    #[async_trait]
653    impl AuthorizationProvider for MockAuthProvider {
654        async fn create_auth_wit(
655            &self,
656            _intent: MessageHashOrIntent,
657            _chain_info: &ChainInfo,
658        ) -> Result<AuthWitness, Error> {
659            Ok(AuthWitness {
660                fields: vec![self.addr.address.0],
661                ..Default::default()
662            })
663        }
664    }
665
666    struct MockAccount {
667        addr: CompleteAddress,
668    }
669
670    #[async_trait]
671    impl AuthorizationProvider for MockAccount {
672        async fn create_auth_wit(
673            &self,
674            _intent: MessageHashOrIntent,
675            _chain_info: &ChainInfo,
676        ) -> Result<AuthWitness, Error> {
677            Ok(AuthWitness {
678                fields: vec![self.addr.address.0],
679                ..Default::default()
680            })
681        }
682    }
683
684    #[async_trait]
685    impl Account for MockAccount {
686        fn complete_address(&self) -> &CompleteAddress {
687            &self.addr
688        }
689
690        fn address(&self) -> AztecAddress {
691            self.addr.address
692        }
693
694        async fn create_tx_execution_request(
695            &self,
696            exec: ExecutionPayload,
697            gas_settings: GasSettings,
698            chain_info: &ChainInfo,
699            _options: EntrypointOptions,
700        ) -> Result<TxExecutionRequest, Error> {
701            Ok(TxExecutionRequest {
702                origin: self.addr.address,
703                function_selector: FunctionSelector::from_hex("0x12345678")
704                    .expect("valid selector"),
705                first_call_args_hash: Fr::from(11u64),
706                tx_context: TxContext {
707                    chain_id: chain_info.chain_id,
708                    version: chain_info.version,
709                    gas_settings,
710                },
711                args_of_calls: exec.extra_hashed_args,
712                auth_witnesses: exec.auth_witnesses,
713                capsules: exec.capsules,
714                salt: Fr::from(7u64),
715                fee_payer: exec.fee_payer,
716            })
717        }
718
719        async fn wrap_execution_payload(
720            &self,
721            exec: ExecutionPayload,
722            options: EntrypointOptions,
723        ) -> Result<ExecutionPayload, Error> {
724            Ok(ExecutionPayload {
725                fee_payer: options.fee_payer.or(exec.fee_payer),
726                ..exec
727            })
728        }
729    }
730
731    struct MockAccountContract;
732
733    #[async_trait]
734    impl AccountContract for MockAccountContract {
735        async fn contract_artifact(&self) -> Result<ContractArtifact, Error> {
736            Ok(ContractArtifact {
737                name: "MockAccount".to_owned(),
738                functions: vec![FunctionArtifact {
739                    name: "constructor".to_owned(),
740                    function_type: FunctionType::Private,
741                    is_initializer: true,
742                    is_static: false,
743                    parameters: vec![AbiParameter {
744                        name: "owner".to_owned(),
745                        typ: AbiType::Field,
746                        visibility: None,
747                    }],
748                    return_types: vec![],
749                    selector: Some(
750                        FunctionSelector::from_hex("0x12345678").expect("valid selector"),
751                    ),
752                    bytecode: None,
753                    verification_key_hash: None,
754                    verification_key: None,
755                    custom_attributes: None,
756                    is_unconstrained: None,
757                    debug_symbols: None,
758                    error_types: None,
759                    is_only_self: None,
760                }],
761                outputs: None,
762                file_map: None,
763                context_inputs_sizes: None,
764            })
765        }
766
767        async fn initialization_function_and_args(
768            &self,
769        ) -> Result<Option<InitializationSpec>, Error> {
770            Ok(Some(InitializationSpec {
771                constructor_name: "constructor".to_owned(),
772                constructor_args: vec![AbiValue::Field(Fr::from(42u64))],
773            }))
774        }
775
776        fn account(&self, address: CompleteAddress) -> Box<dyn Account> {
777            Box::new(MockAccount { addr: address })
778        }
779
780        fn auth_witness_provider(
781            &self,
782            address: CompleteAddress,
783        ) -> Box<dyn AuthorizationProvider> {
784            Box::new(MockAuthProvider { addr: address })
785        }
786    }
787
788    struct NoInitializerAccountContract;
789
790    #[async_trait]
791    impl AccountContract for NoInitializerAccountContract {
792        async fn contract_artifact(&self) -> Result<ContractArtifact, Error> {
793            Ok(ContractArtifact {
794                name: "NoInitializerAccount".to_owned(),
795                functions: vec![],
796                outputs: None,
797                file_map: None,
798                context_inputs_sizes: None,
799            })
800        }
801
802        async fn initialization_function_and_args(
803            &self,
804        ) -> Result<Option<InitializationSpec>, Error> {
805            Ok(None)
806        }
807
808        fn account(&self, address: CompleteAddress) -> Box<dyn Account> {
809            Box::new(MockAccount { addr: address })
810        }
811
812        fn auth_witness_provider(
813            &self,
814            address: CompleteAddress,
815        ) -> Box<dyn AuthorizationProvider> {
816            Box::new(MockAuthProvider { addr: address })
817        }
818    }
819
820    struct BadInitializerNameAccountContract;
821
822    #[async_trait]
823    impl AccountContract for BadInitializerNameAccountContract {
824        async fn contract_artifact(&self) -> Result<ContractArtifact, Error> {
825            MockAccountContract.contract_artifact().await
826        }
827
828        async fn initialization_function_and_args(
829            &self,
830        ) -> Result<Option<InitializationSpec>, Error> {
831            Ok(Some(InitializationSpec {
832                constructor_name: "missing".to_owned(),
833                constructor_args: vec![AbiValue::Field(Fr::from(42u64))],
834            }))
835        }
836
837        fn account(&self, address: CompleteAddress) -> Box<dyn Account> {
838            Box::new(MockAccount { addr: address })
839        }
840
841        fn auth_witness_provider(
842            &self,
843            address: CompleteAddress,
844        ) -> Box<dyn AuthorizationProvider> {
845            Box::new(MockAuthProvider { addr: address })
846        }
847    }
848
849    struct BadInitializerArgsAccountContract;
850
851    #[async_trait]
852    impl AccountContract for BadInitializerArgsAccountContract {
853        async fn contract_artifact(&self) -> Result<ContractArtifact, Error> {
854            MockAccountContract.contract_artifact().await
855        }
856
857        async fn initialization_function_and_args(
858            &self,
859        ) -> Result<Option<InitializationSpec>, Error> {
860            Ok(Some(InitializationSpec {
861                constructor_name: "constructor".to_owned(),
862                constructor_args: vec![],
863            }))
864        }
865
866        fn account(&self, address: CompleteAddress) -> Box<dyn Account> {
867            Box::new(MockAccount { addr: address })
868        }
869
870        fn auth_witness_provider(
871            &self,
872            address: CompleteAddress,
873        ) -> Box<dyn AuthorizationProvider> {
874            Box::new(MockAuthProvider { addr: address })
875        }
876    }
877
878    fn sample_chain_info() -> ChainInfo {
879        ChainInfo {
880            chain_id: Fr::from(31337u64),
881            version: Fr::from(1u64),
882        }
883    }
884
885    fn sample_complete_address() -> CompleteAddress {
886        CompleteAddress {
887            address: AztecAddress(Fr::from(99u64)),
888            public_keys: PublicKeys::default(),
889            partial_address: Fr::from(1u64),
890        }
891    }
892
893    // -- Trait object safety -----------------------------------------------
894
895    #[test]
896    fn authorization_provider_is_object_safe() {
897        fn _assert(_: &dyn AuthorizationProvider) {}
898    }
899
900    #[test]
901    fn account_is_object_safe() {
902        fn _assert(_: &dyn Account) {}
903    }
904
905    #[test]
906    fn account_contract_is_object_safe() {
907        fn _assert(_: &dyn AccountContract) {}
908    }
909
910    // -- Send + Sync -------------------------------------------------------
911
912    #[test]
913    fn mock_account_is_send_sync() {
914        fn assert_send_sync<T: Send + Sync>() {}
915        assert_send_sync::<MockAccount>();
916        assert_send_sync::<MockAuthProvider>();
917        assert_send_sync::<MockAccountContract>();
918    }
919
920    #[test]
921    fn account_with_secret_key_is_send() {
922        fn assert_send<T: Send>() {}
923        assert_send::<AccountWithSecretKey>();
924    }
925
926    // -- Supporting type serde ---------------------------------------------
927
928    #[test]
929    fn entrypoint_options_default() {
930        let opts = EntrypointOptions::default();
931        assert!(opts.fee_payer.is_none());
932        assert!(opts.gas_settings.is_none());
933    }
934
935    #[test]
936    fn entrypoint_options_roundtrip() {
937        let opts = EntrypointOptions {
938            fee_payer: Some(AztecAddress(Fr::from(1u64))),
939            gas_settings: Some(GasSettings {
940                gas_limits: Some(Gas {
941                    da_gas: 100,
942                    l2_gas: 200,
943                }),
944                ..GasSettings::default()
945            }),
946            fee_payment_method: None,
947        };
948        let json = serde_json::to_string(&opts).expect("serialize EntrypointOptions");
949        let decoded: EntrypointOptions =
950            serde_json::from_str(&json).expect("deserialize EntrypointOptions");
951        assert_eq!(decoded, opts);
952    }
953
954    #[test]
955    fn tx_execution_request_roundtrip() {
956        let req = TxExecutionRequest {
957            origin: AztecAddress(Fr::from(1u64)),
958            function_selector: FunctionSelector::from_hex("0xaabbccdd").expect("selector"),
959            first_call_args_hash: Fr::from(3u64),
960            tx_context: TxContext {
961                chain_id: Fr::from(31337u64),
962                version: Fr::from(1u64),
963                gas_settings: GasSettings::default(),
964            },
965            args_of_calls: vec![HashedValues::from_args(vec![Fr::from(7u64)])],
966            auth_witnesses: vec![AuthWitness {
967                fields: vec![Fr::from(42u64)],
968                ..Default::default()
969            }],
970            capsules: vec![],
971            salt: Fr::from(9u64),
972            fee_payer: None,
973        };
974        let json = serde_json::to_string(&req).expect("serialize TxExecutionRequest");
975        let decoded: TxExecutionRequest =
976            serde_json::from_str(&json).expect("deserialize TxExecutionRequest");
977        assert_eq!(decoded, req);
978    }
979
980    #[test]
981    fn initialization_spec_fields() {
982        let spec = InitializationSpec {
983            constructor_name: "constructor".to_owned(),
984            constructor_args: vec![AbiValue::Field(Fr::from(1u64)), AbiValue::Boolean(true)],
985        };
986        assert_eq!(spec.constructor_name, "constructor");
987        assert_eq!(spec.constructor_args.len(), 2);
988    }
989
990    // -- AuthorizationProvider tests ---------------------------------------
991
992    #[tokio::test]
993    async fn mock_auth_provider_creates_witness() {
994        let provider = MockAuthProvider {
995            addr: sample_complete_address(),
996        };
997        let chain_info = sample_chain_info();
998        let wit = provider
999            .create_auth_wit(
1000                MessageHashOrIntent::Hash {
1001                    hash: Fr::from(1u64),
1002                },
1003                &chain_info,
1004            )
1005            .await
1006            .expect("create auth wit");
1007        assert_eq!(wit.fields.len(), 1);
1008        assert_eq!(wit.fields[0], Fr::from(99u64));
1009    }
1010
1011    // -- Account tests -----------------------------------------------------
1012
1013    #[tokio::test]
1014    async fn mock_account_address() {
1015        let account = MockAccount {
1016            addr: sample_complete_address(),
1017        };
1018        assert_eq!(account.address(), AztecAddress(Fr::from(99u64)));
1019        assert_eq!(
1020            account.complete_address().address,
1021            AztecAddress(Fr::from(99u64))
1022        );
1023    }
1024
1025    #[tokio::test]
1026    async fn mock_account_creates_execution_request() {
1027        let account = MockAccount {
1028            addr: sample_complete_address(),
1029        };
1030        let chain_info = sample_chain_info();
1031
1032        let payload = ExecutionPayload {
1033            calls: vec![],
1034            auth_witnesses: vec![AuthWitness {
1035                fields: vec![Fr::from(1u64)],
1036                ..Default::default()
1037            }],
1038            capsules: vec![],
1039            extra_hashed_args: vec![],
1040            fee_payer: None,
1041        };
1042
1043        let gas_settings = GasSettings {
1044            gas_limits: Some(Gas {
1045                da_gas: 100,
1046                l2_gas: 200,
1047            }),
1048            ..GasSettings::default()
1049        };
1050
1051        let options = EntrypointOptions {
1052            fee_payer: Some(AztecAddress(Fr::from(5u64))),
1053            gas_settings: None,
1054            fee_payment_method: None,
1055        };
1056
1057        let req = account
1058            .create_tx_execution_request(payload, gas_settings.clone(), &chain_info, options)
1059            .await
1060            .expect("create tx execution request");
1061
1062        assert_eq!(req.origin, AztecAddress(Fr::from(99u64)));
1063        assert_eq!(req.auth_witnesses.len(), 1);
1064        assert_eq!(req.tx_context.gas_settings, gas_settings);
1065        assert_eq!(req.tx_context.chain_id, chain_info.chain_id);
1066    }
1067
1068    #[tokio::test]
1069    async fn mock_account_wraps_payload() {
1070        let account = MockAccount {
1071            addr: sample_complete_address(),
1072        };
1073
1074        let payload = ExecutionPayload {
1075            calls: vec![],
1076            auth_witnesses: vec![],
1077            capsules: vec![],
1078            extra_hashed_args: vec![],
1079            fee_payer: Some(AztecAddress(Fr::from(3u64))),
1080        };
1081
1082        let options = EntrypointOptions::default();
1083
1084        let wrapped = account
1085            .wrap_execution_payload(payload, options)
1086            .await
1087            .expect("wrap execution payload");
1088
1089        assert_eq!(wrapped.fee_payer, Some(AztecAddress(Fr::from(3u64))));
1090    }
1091
1092    #[tokio::test]
1093    async fn mock_account_wrap_overrides_fee_payer() {
1094        let account = MockAccount {
1095            addr: sample_complete_address(),
1096        };
1097
1098        let payload = ExecutionPayload {
1099            calls: vec![],
1100            auth_witnesses: vec![],
1101            capsules: vec![],
1102            extra_hashed_args: vec![],
1103            fee_payer: Some(AztecAddress(Fr::from(3u64))),
1104        };
1105
1106        let options = EntrypointOptions {
1107            fee_payer: Some(AztecAddress(Fr::from(7u64))),
1108            gas_settings: None,
1109            fee_payment_method: None,
1110        };
1111
1112        let wrapped = account
1113            .wrap_execution_payload(payload, options)
1114            .await
1115            .expect("wrap execution payload");
1116
1117        assert_eq!(wrapped.fee_payer, Some(AztecAddress(Fr::from(7u64))));
1118    }
1119
1120    #[tokio::test]
1121    async fn account_with_secret_key_delegates_to_inner_account() {
1122        let account = AccountWithSecretKey {
1123            account: Box::new(MockAccount {
1124                addr: sample_complete_address(),
1125            }),
1126            secret_key: Fr::from(42u64),
1127            salt: Fr::from(7u64),
1128        };
1129
1130        let chain_info = sample_chain_info();
1131        let wit = account
1132            .create_auth_wit(
1133                MessageHashOrIntent::Hash {
1134                    hash: Fr::from(1u64),
1135                },
1136                &chain_info,
1137            )
1138            .await
1139            .expect("create auth wit");
1140
1141        assert_eq!(account.address(), AztecAddress(Fr::from(99u64)));
1142        assert_eq!(account.complete_address().partial_address, Fr::from(1u64));
1143        assert_eq!(account.secret_key, Fr::from(42u64));
1144        assert_eq!(account.salt, Fr::from(7u64));
1145        assert_eq!(wit.fields, vec![Fr::from(99u64)]);
1146    }
1147
1148    // -- AccountContract tests ---------------------------------------------
1149
1150    #[tokio::test]
1151    async fn mock_account_contract_artifact() {
1152        let contract = MockAccountContract;
1153        let artifact = contract
1154            .contract_artifact()
1155            .await
1156            .expect("get contract artifact");
1157        assert_eq!(artifact.name, "MockAccount");
1158        assert_eq!(artifact.functions.len(), 1);
1159        assert!(artifact.functions[0].is_initializer);
1160    }
1161
1162    #[tokio::test]
1163    async fn mock_account_contract_init_spec() {
1164        let contract = MockAccountContract;
1165        let spec = contract
1166            .initialization_function_and_args()
1167            .await
1168            .expect("get init spec")
1169            .expect("should have init spec");
1170        assert_eq!(spec.constructor_name, "constructor");
1171        assert_eq!(spec.constructor_args.len(), 1);
1172    }
1173
1174    #[tokio::test]
1175    async fn mock_account_contract_produces_account() {
1176        let contract = MockAccountContract;
1177        let addr = sample_complete_address();
1178        let account = contract.account(addr);
1179        assert_eq!(account.address(), AztecAddress(Fr::from(99u64)));
1180    }
1181
1182    #[tokio::test]
1183    async fn mock_account_contract_produces_auth_provider() {
1184        let contract = MockAccountContract;
1185        let addr = sample_complete_address();
1186        let provider = contract.auth_witness_provider(addr);
1187        let chain_info = sample_chain_info();
1188
1189        let wit = provider
1190            .create_auth_wit(
1191                MessageHashOrIntent::Hash {
1192                    hash: Fr::from(1u64),
1193                },
1194                &chain_info,
1195            )
1196            .await
1197            .expect("create auth wit");
1198        assert_eq!(wit.fields.len(), 1);
1199    }
1200
1201    // -- AccountManager tests ----------------------------------------------
1202
1203    #[tokio::test]
1204    async fn account_manager_create() {
1205        let wallet = MockWallet::new(sample_chain_info());
1206        let manager = AccountManager::create(
1207            wallet,
1208            Fr::from(42u64),
1209            Box::new(MockAccountContract),
1210            Some(Fr::from(7u64)),
1211        )
1212        .await
1213        .expect("create account manager");
1214
1215        assert_eq!(manager.salt(), Fr::from(7u64));
1216        assert_eq!(manager.secret_key(), Fr::from(42u64));
1217    }
1218
1219    #[tokio::test]
1220    async fn account_manager_default_salt() {
1221        let wallet = MockWallet::new(sample_chain_info());
1222        let manager = AccountManager::create(
1223            wallet,
1224            Fr::from(1u64),
1225            Box::new(MockAccountContract),
1226            None::<Fr>,
1227        )
1228        .await
1229        .expect("create account manager");
1230
1231        assert_ne!(manager.salt(), Fr::zero());
1232    }
1233
1234    #[tokio::test]
1235    async fn account_manager_rejects_missing_initializer_function() {
1236        let wallet = MockWallet::new(sample_chain_info());
1237        let result = AccountManager::create(
1238            wallet,
1239            Fr::from(1u64),
1240            Box::new(BadInitializerNameAccountContract),
1241            None::<Fr>,
1242        )
1243        .await;
1244        assert!(result.is_err());
1245        let err = result.err().expect("initializer lookup should fail");
1246
1247        assert!(err.to_string().contains("not found"));
1248    }
1249
1250    #[tokio::test]
1251    async fn account_manager_rejects_initializer_argument_mismatch() {
1252        let wallet = MockWallet::new(sample_chain_info());
1253        let result = AccountManager::create(
1254            wallet,
1255            Fr::from(1u64),
1256            Box::new(BadInitializerArgsAccountContract),
1257            None::<Fr>,
1258        )
1259        .await;
1260        assert!(result.is_err());
1261        let err = result
1262            .err()
1263            .expect("initializer arg validation should fail");
1264
1265        assert!(err.to_string().contains("expects 1 argument(s), got 0"));
1266    }
1267
1268    #[tokio::test]
1269    async fn account_manager_address_accessors() {
1270        let wallet = MockWallet::new(sample_chain_info());
1271        let manager = AccountManager::create(
1272            wallet,
1273            Fr::from(1u64),
1274            Box::new(MockAccountContract),
1275            None::<Fr>,
1276        )
1277        .await
1278        .expect("create account manager");
1279
1280        // Address is now computed from real key derivation and artifact hashing
1281        assert_ne!(manager.address(), AztecAddress(Fr::zero()));
1282
1283        // complete_address() now works (derives keys from secret key)
1284        let complete = manager
1285            .complete_address()
1286            .await
1287            .expect("complete address derivation");
1288        assert!(!complete.public_keys.is_empty());
1289        assert!(!complete.address.0.is_zero());
1290
1291        let instance = manager.instance();
1292        assert_eq!(instance.inner.version, 1);
1293    }
1294
1295    #[tokio::test]
1296    async fn account_manager_account() {
1297        let wallet = MockWallet::new(sample_chain_info());
1298        let manager = AccountManager::create(
1299            wallet,
1300            Fr::from(42u64),
1301            Box::new(MockAccountContract),
1302            None::<Fr>,
1303        )
1304        .await
1305        .expect("create account manager");
1306
1307        // account() now works since complete_address() is implemented
1308        let account = manager.account().await.expect("account construction");
1309        assert_eq!(account.secret_key, Fr::from(42u64));
1310    }
1311
1312    #[tokio::test]
1313    async fn account_manager_account_creates_auth_wit() {
1314        let wallet = MockWallet::new(sample_chain_info());
1315        let manager = AccountManager::create(
1316            wallet,
1317            Fr::from(42u64),
1318            Box::new(MockAccountContract),
1319            None::<Fr>,
1320        )
1321        .await
1322        .expect("create account manager");
1323
1324        let account = manager.account().await.expect("account construction");
1325        // Verify the account can create auth witnesses
1326        let chain_info = sample_chain_info();
1327        let wit = account
1328            .create_auth_wit(
1329                MessageHashOrIntent::Hash {
1330                    hash: Fr::from(999u64),
1331                },
1332                &chain_info,
1333            )
1334            .await
1335            .expect("create auth wit");
1336        assert!(!wit.fields.is_empty());
1337    }
1338
1339    // -- DeployAccountMethod tests -----------------------------------------
1340
1341    #[tokio::test]
1342    async fn deploy_method_request_builds_payload() {
1343        let wallet = MockWallet::new(sample_chain_info());
1344        let manager = AccountManager::create(
1345            wallet,
1346            Fr::from(1u64),
1347            Box::new(MockAccountContract),
1348            None::<Fr>,
1349        )
1350        .await
1351        .expect("create account manager");
1352
1353        let deploy = manager.deploy_method().await.expect("build deploy method");
1354        let opts = DeployAccountOptions {
1355            skip_registration: true,
1356            ..Default::default()
1357        };
1358        let payload = deploy.request(&opts).await.expect("request should succeed");
1359        // Should have at least some calls (class publication, instance publication, constructor)
1360        assert!(!payload.calls.is_empty());
1361    }
1362
1363    #[tokio::test]
1364    async fn deploy_method_requires_initializer() {
1365        let wallet = MockWallet::new(sample_chain_info());
1366        let manager = AccountManager::create(
1367            wallet,
1368            Fr::from(1u64),
1369            Box::new(NoInitializerAccountContract),
1370            None::<Fr>,
1371        )
1372        .await
1373        .expect("create account manager");
1374
1375        assert!(!manager.has_initializer());
1376        let deploy = manager
1377            .deploy_method()
1378            .await
1379            .expect("deploy method should still build");
1380        let payload = deploy
1381            .request(&DeployAccountOptions {
1382                skip_registration: true,
1383                ..Default::default()
1384            })
1385            .await
1386            .expect("request should succeed without initializer");
1387        assert!(payload.calls.is_empty());
1388    }
1389
1390    #[tokio::test]
1391    async fn deploy_method_instance() {
1392        let wallet = MockWallet::new(sample_chain_info());
1393        let manager = AccountManager::create(
1394            wallet,
1395            Fr::from(1u64),
1396            Box::new(MockAccountContract),
1397            Some(Fr::from(99u64)),
1398        )
1399        .await
1400        .expect("create account manager");
1401
1402        let deploy = manager.deploy_method().await.expect("build deploy method");
1403        assert_eq!(deploy.instance().inner.salt, Fr::from(99u64));
1404        // Address should be non-zero (real derivation)
1405        assert_ne!(deploy.instance().address, AztecAddress(Fr::zero()));
1406    }
1407
1408    #[tokio::test]
1409    async fn deploy_method_instance_matches_manager_address() {
1410        let wallet = MockWallet::new(sample_chain_info());
1411        let manager = AccountManager::create(
1412            wallet,
1413            Fr::from(1u64),
1414            Box::new(MockAccountContract),
1415            Some(Fr::from(99u64)),
1416        )
1417        .await
1418        .expect("create account manager");
1419
1420        let deploy = manager.deploy_method().await.expect("build deploy method");
1421        // The deployed instance address should match manager's stored instance address
1422        assert_eq!(deploy.instance().address, manager.instance().address);
1423    }
1424
1425    #[tokio::test]
1426    async fn deploy_method_skip_flags_pass_through() {
1427        let wallet = MockWallet::new(sample_chain_info());
1428        let manager = AccountManager::create(
1429            wallet,
1430            Fr::from(1u64),
1431            Box::new(MockAccountContract),
1432            None::<Fr>,
1433        )
1434        .await
1435        .expect("create account manager");
1436
1437        let deploy = manager.deploy_method().await.expect("build deploy method");
1438        let opts = DeployAccountOptions {
1439            skip_registration: true,
1440            skip_class_publication: true,
1441            skip_instance_publication: true,
1442            skip_initialization: true,
1443            ..Default::default()
1444        };
1445        let payload = deploy.request(&opts).await.expect("request");
1446        // With everything skipped, should produce empty or fee-only payload
1447        // The fee wrapper still produces calls via meta payment
1448        // This mainly tests that skip flags don't crash
1449        let _ = payload;
1450    }
1451}