aztec_wallet/
base_wallet.rs

1//! Production wallet implementation backed by PXE and Aztec node connections.
2//!
3//! [`BaseWallet`] is the standard [`Wallet`] implementation for interacting with
4//! a live Aztec network. It routes private operations through a [`Pxe`] backend,
5//! public queries through an [`AztecNode`] backend, and delegates authentication
6//! to an [`AccountProvider`].
7
8use async_trait::async_trait;
9use tokio::time::{sleep, Duration, Instant};
10
11use crate::abi::{AbiType, ContractArtifact, FunctionSelector};
12use crate::account_provider::AccountProvider;
13use crate::error::Error;
14use crate::node::{
15    create_aztec_node_client, wait_for_tx, AztecNode, HttpNodeClient, TxValidationResult, WaitOpts,
16};
17use crate::pxe::{self, Pxe, RegisterContractRequest};
18use crate::tx::{AuthWitness, ExecutionPayload, FunctionCall, TxHash, TxStatus};
19use crate::types::{AztecAddress, ContractInstanceWithAddress, Fr};
20use crate::wallet::{
21    Aliased, ChainInfo, ContractClassMetadata, ContractMetadata, EventMetadataDefinition,
22    ExecuteUtilityOptions, MessageHashOrIntent, PrivateEvent, PrivateEventFilter,
23    PrivateEventMetadata, ProfileOptions, SendOptions, SendResult, SimulateOptions,
24    TxProfileResult, TxSimulationResult, UtilityExecutionResult, Wallet,
25};
26use aztec_core::constants::protocol_contract_address;
27
28/// A production [`Wallet`] backed by PXE + Aztec node connections.
29///
30/// Routes private-state operations (simulate, prove, events, registration)
31/// through the PXE, public-state queries (chain info, contract metadata)
32/// through the Aztec node, and delegates auth witness creation to the
33/// account provider.
34pub struct BaseWallet<P, N, A> {
35    pxe: P,
36    node: N,
37    accounts: A,
38}
39
40impl<P: Pxe, N: AztecNode, A: AccountProvider> BaseWallet<P, N, A> {
41    fn decode_assertion_message_from_error_types(
42        error_types: &serde_json::Value,
43        selector_key: &str,
44    ) -> Option<String> {
45        let entry = error_types.get(selector_key)?;
46        match entry.get("error_kind").and_then(|v| v.as_str()) {
47            Some("string") | Some("fmtstring") => entry
48                .get("string")
49                .and_then(|v| v.as_str())
50                .map(str::to_owned),
51            _ => None,
52        }
53    }
54
55    fn known_protocol_assertion_message(
56        contract_address: &AztecAddress,
57        selector_key: &str,
58    ) -> Option<String> {
59        if *contract_address == protocol_contract_address::auth_registry()
60            && selector_key == "17089945683942782951"
61        {
62            return Some("unauthorized".to_owned());
63        }
64        None
65    }
66
67    fn known_assertion_message_by_selector(selector_key: &str) -> Option<String> {
68        match selector_key {
69            "17089945683942782951" => Some("unauthorized".to_owned()),
70            "7136484461999155778" => Some(
71                "Invalid authwit nonce. When 'from' and 'msg_sender' are the same, 'authwit_nonce' must be zero"
72                    .to_owned(),
73            ),
74            "1998584279744703196" => Some("attempt to subtract with overflow".to_owned()),
75            _ => None,
76        }
77    }
78
79    fn error_selector_key_from_hex(selector_hex: &str) -> Option<String> {
80        let raw = selector_hex.strip_prefix("0x").unwrap_or(selector_hex);
81        u128::from_str_radix(raw, 16)
82            .ok()
83            .map(|value| value.to_string())
84    }
85
86    async fn public_function_error_types(
87        &self,
88        contract_address: &AztecAddress,
89        function_selector: &FunctionSelector,
90    ) -> Result<Option<serde_json::Value>, Error> {
91        let Some(instance) = self.pxe.get_contract_instance(contract_address).await? else {
92            return Ok(None);
93        };
94        let Some(artifact) = self
95            .pxe
96            .get_contract_artifact(&instance.inner.current_contract_class_id)
97            .await?
98        else {
99            return Ok(None);
100        };
101
102        Ok(artifact
103            .find_function_by_selector(function_selector)
104            .and_then(|function| function.error_types.clone()))
105    }
106
107    async fn find_registered_error_message_by_selector(
108        &self,
109        selector_key: &str,
110    ) -> Result<Option<String>, Error> {
111        for address in self.pxe.get_contracts().await? {
112            let Some(instance) = self.pxe.get_contract_instance(&address).await? else {
113                continue;
114            };
115            let Some(artifact) = self
116                .pxe
117                .get_contract_artifact(&instance.inner.current_contract_class_id)
118                .await?
119            else {
120                continue;
121            };
122            for function in &artifact.functions {
123                let Some(error_types) = &function.error_types else {
124                    continue;
125                };
126                if let Some(message) =
127                    Self::decode_assertion_message_from_error_types(error_types, selector_key)
128                {
129                    return Ok(Some(message));
130                }
131            }
132        }
133        Ok(None)
134    }
135
136    async fn decode_public_assertion_message(
137        &self,
138        revert_reason: &serde_json::Value,
139    ) -> Option<String> {
140        let revert_data = revert_reason.get("revertData")?.as_array()?;
141        let selector_key = revert_data
142            .first()
143            .and_then(|value| value.as_str())
144            .and_then(Self::error_selector_key_from_hex)?;
145
146        let failing_function = revert_reason
147            .get("functionErrorStack")?
148            .as_array()?
149            .last()?
150            .as_object()?;
151        let contract_address = failing_function
152            .get("contractAddress")
153            .and_then(|value| value.as_str())
154            .and_then(|hex| Fr::from_hex(hex).ok())
155            .map(AztecAddress)?;
156        let function_selector = failing_function
157            .get("functionSelector")
158            .and_then(|value| value.as_str())
159            .and_then(|hex| FunctionSelector::from_hex(hex).ok())?;
160
161        if let Ok(Some(error_types)) = self
162            .public_function_error_types(&contract_address, &function_selector)
163            .await
164        {
165            if let Some(message) =
166                Self::decode_assertion_message_from_error_types(&error_types, &selector_key)
167            {
168                return Some(format!("Assertion failed: {message}"));
169            }
170        }
171
172        if let Ok(Some(message)) = self
173            .find_registered_error_message_by_selector(&selector_key)
174            .await
175        {
176            return Some(format!("Assertion failed: {message}"));
177        }
178
179        Self::known_protocol_assertion_message(&contract_address, &selector_key)
180            .or_else(|| Self::known_assertion_message_by_selector(&selector_key))
181            .map(|message| format!("Assertion failed: {message}"))
182    }
183
184    /// Create a new `BaseWallet` with the given PXE, node, and account provider.
185    pub fn new(pxe: P, node: N, accounts: A) -> Self {
186        Self {
187            pxe,
188            node,
189            accounts,
190        }
191    }
192
193    /// Get a reference to the underlying PXE client.
194    pub fn pxe(&self) -> &P {
195        &self.pxe
196    }
197
198    /// Get a reference to the underlying Aztec node client.
199    pub fn node(&self) -> &N {
200        &self.node
201    }
202
203    /// Get a reference to the account provider.
204    pub fn account_provider(&self) -> &A {
205        &self.accounts
206    }
207
208    /// Merge wallet-level auth witnesses and capsules into an execution payload.
209    fn merge_execution_payload(
210        mut exec: ExecutionPayload,
211        auth_witnesses: &[AuthWitness],
212        capsules: &[crate::tx::Capsule],
213    ) -> ExecutionPayload {
214        exec.auth_witnesses.extend_from_slice(auth_witnesses);
215        exec.capsules.extend_from_slice(capsules);
216        exec
217    }
218
219    /// Build scopes from a sender address and additional scopes.
220    fn build_scopes(from: &AztecAddress, additional: &[AztecAddress]) -> Vec<AztecAddress> {
221        let mut scopes = additional.to_vec();
222        if *from != AztecAddress(Fr::zero()) && !scopes.contains(from) {
223            scopes.push(*from);
224        }
225        scopes
226    }
227
228    fn has_no_private_return_values(result: &serde_json::Value) -> bool {
229        match result.get("returnValues") {
230            Some(serde_json::Value::Array(arr)) => arr.is_empty(),
231            Some(serde_json::Value::Object(obj)) => obj
232                .get("returnValues")
233                .and_then(|v| v.as_array())
234                .map(|arr| arr.is_empty())
235                .unwrap_or(false),
236            _ => false,
237        }
238    }
239
240    async fn wait_for_submission_checkpoint(&self, tx_hash: &TxHash) -> Result<(), Error> {
241        let start_block = self.node.get_block_number().await.unwrap_or(0);
242        let wait_opts = WaitOpts {
243            timeout: Duration::from_secs(15),
244            ..WaitOpts::default()
245        };
246
247        match wait_for_tx(&self.node, tx_hash, wait_opts).await {
248            Ok(_) => {}
249            Err(Error::Timeout(_)) | Err(Error::InvalidData(_)) => {
250                let deadline = Instant::now() + Duration::from_secs(30);
251                let mut next_log = Instant::now();
252                loop {
253                    match self.node.get_tx_receipt(tx_hash).await {
254                        Ok(receipt) if Instant::now() >= next_log => {
255                            tracing::debug!(
256                                tx_hash = %tx_hash,
257                                status = ?receipt.status,
258                                block = ?receipt.block_number,
259                                error = ?receipt.error,
260                                "wait_for_submission_checkpoint polling"
261                            );
262                            next_log = Instant::now() + Duration::from_secs(2);
263                        }
264                        Err(err) if Instant::now() >= next_log => {
265                            tracing::debug!(
266                                tx_hash = %tx_hash,
267                                error = %err,
268                                "wait_for_submission_checkpoint receipt error"
269                            );
270                            next_log = Instant::now() + Duration::from_secs(2);
271                        }
272                        _ => {}
273                    }
274                    let current_block = self.node.get_block_number().await?;
275                    if current_block > start_block {
276                        break;
277                    }
278                    if Instant::now() >= deadline {
279                        return Err(Error::Timeout(
280                            "transaction was submitted but did not become checkpoint-visible"
281                                .into(),
282                        ));
283                    }
284                    sleep(Duration::from_millis(500)).await;
285                }
286            }
287            Err(err) => return Err(err),
288        }
289
290        // Allow the node's world-state trees (nullifier, note-hash, public-data)
291        // to finish processing the block.  The tx is confirmed but the indexed
292        // Merkle trees may lag slightly behind the block tip.
293        self.wait_for_world_state(tx_hash).await
294    }
295
296    /// Poll the node until its world-state trees have been updated to include
297    /// the given transaction's effects.  We probe for one of the tx's own
298    /// nullifiers via `getNullifierMembershipWitness`; once the tree contains
299    /// it the world state is guaranteed current for that block.
300    async fn wait_for_world_state(&self, tx_hash: &TxHash) -> Result<(), Error> {
301        // Fetch the tx effect to obtain a known nullifier (the first nullifier
302        // is always the protocol/tx-hash nullifier).
303        let effect = match self.node.get_tx_effect(tx_hash).await {
304            Ok(Some(e)) => e,
305            _ => {
306                // If we can't fetch the effect, fall back to a short sleep.
307                sleep(Duration::from_secs(1)).await;
308                return Ok(());
309            }
310        };
311
312        let first_nullifier = effect
313            .pointer("/data/nullifiers/0")
314            .and_then(|v| v.as_str())
315            .and_then(|s| crate::types::Fr::from_hex(s).ok());
316
317        let Some(nullifier) = first_nullifier else {
318            sleep(Duration::from_secs(1)).await;
319            return Ok(());
320        };
321
322        let deadline = Instant::now() + Duration::from_secs(15);
323        loop {
324            if let Ok(Some(_)) = self
325                .node
326                .get_nullifier_membership_witness(0, &nullifier)
327                .await
328            {
329                return Ok(());
330            }
331            if Instant::now() >= deadline {
332                // Don't fail — the tx IS mined, the tree will catch up.
333                return Ok(());
334            }
335            sleep(Duration::from_millis(200)).await;
336        }
337    }
338
339    async fn simulate_public_calls(
340        &self,
341        tx_hash: &TxHash,
342        tx_json: &serde_json::Value,
343    ) -> Result<serde_json::Value, Error> {
344        let simulation = self.node.simulate_public_calls(tx_json, false).await?;
345        if let Some(revert_reason) = simulation.get("revertReason") {
346            if !revert_reason.is_null() {
347                let debug_logs = simulation
348                    .get("debugLogs")
349                    .cloned()
350                    .unwrap_or(serde_json::Value::Null);
351                let decoded_assertion = self.decode_public_assertion_message(revert_reason).await;
352                let decoded_prefix = decoded_assertion
353                    .map(|message| format!("{message} "))
354                    .unwrap_or_default();
355                return Err(Error::InvalidData(format!(
356                    "node public-call preflight for tx {} reverted: {}reason={} debugLogs={}",
357                    tx_hash, decoded_prefix, revert_reason, debug_logs
358                )));
359            }
360        }
361        Ok(simulation)
362    }
363
364    /// Errors from the simulation preflight that should be ignored.
365    ///
366    /// The node's `simulatePublicCalls` C++ AVM simulation can fail for reasons
367    /// that don't affect actual block execution, e.g. recently-deployed contracts
368    /// appearing "not deployed" because the simulation's world-state snapshot lags
369    /// behind the block-builder's.
370    fn should_ignore_public_preflight_error(err: &Error) -> bool {
371        match err {
372            Error::InvalidData(msg) => msg.contains("is not deployed"),
373            _ => false,
374        }
375    }
376
377    fn is_anchor_header_validation_lag(result: &TxValidationResult) -> bool {
378        match result {
379            TxValidationResult::Invalid { reason } => reason
380                .iter()
381                .any(|msg| msg.contains("Block header not found")),
382            _ => false,
383        }
384    }
385
386    async fn validate_tx_when_anchor_available(
387        &self,
388        tx_json: &serde_json::Value,
389    ) -> Result<TxValidationResult, Error> {
390        let deadline = Instant::now() + Duration::from_secs(300);
391        loop {
392            let result = self.node.is_valid_tx(tx_json).await?;
393            if !Self::is_anchor_header_validation_lag(&result) {
394                return Ok(result);
395            }
396            if Instant::now() >= deadline {
397                return Ok(result);
398            }
399            sleep(Duration::from_millis(500)).await;
400        }
401    }
402}
403
404#[async_trait]
405impl<P: Pxe, N: AztecNode, A: AccountProvider> Wallet for BaseWallet<P, N, A> {
406    async fn get_chain_info(&self) -> Result<ChainInfo, Error> {
407        let info = self.node.get_node_info().await?;
408        Ok(ChainInfo {
409            chain_id: Fr::from(info.l1_chain_id),
410            version: Fr::from(info.rollup_version),
411        })
412    }
413
414    async fn get_accounts(&self) -> Result<Vec<Aliased<AztecAddress>>, Error> {
415        self.accounts.get_accounts().await
416    }
417
418    async fn get_address_book(&self) -> Result<Vec<Aliased<AztecAddress>>, Error> {
419        let senders = self.pxe.get_senders().await?;
420        Ok(senders
421            .into_iter()
422            .map(|addr| Aliased {
423                alias: String::new(),
424                item: addr,
425            })
426            .collect())
427    }
428
429    async fn register_sender(
430        &self,
431        address: AztecAddress,
432        _alias: Option<String>,
433    ) -> Result<AztecAddress, Error> {
434        self.pxe.register_sender(&address).await
435    }
436
437    async fn get_contract_metadata(
438        &self,
439        address: AztecAddress,
440    ) -> Result<ContractMetadata, Error> {
441        let instance = self.pxe.get_contract_instance(&address).await?;
442        let on_chain = self.node.get_contract(&address).await?;
443
444        let is_published = on_chain.is_some();
445        let is_updated = on_chain
446            .as_ref()
447            .map(|contract| {
448                contract.current_contract_class_id != contract.original_contract_class_id
449            })
450            .unwrap_or(false);
451        let updated_contract_class_id = on_chain
452            .as_ref()
453            .and_then(|contract| is_updated.then_some(contract.current_contract_class_id));
454
455        // Simplified: full impl needs nullifier-based initialization check (Step 5)
456        Ok(ContractMetadata {
457            instance,
458            is_contract_initialized: is_published,
459            is_contract_published: is_published,
460            is_contract_updated: is_updated,
461            updated_contract_class_id,
462        })
463    }
464
465    async fn get_contract_class_metadata(
466        &self,
467        class_id: Fr,
468    ) -> Result<ContractClassMetadata, Error> {
469        let artifact = self.pxe.get_contract_artifact(&class_id).await?;
470        let on_chain = self.node.get_contract_class(&class_id).await?;
471
472        Ok(ContractClassMetadata {
473            is_artifact_registered: artifact.is_some(),
474            is_contract_class_publicly_registered: on_chain.is_some(),
475        })
476    }
477
478    async fn register_contract(
479        &self,
480        instance: ContractInstanceWithAddress,
481        artifact: Option<ContractArtifact>,
482        secret_key: Option<Fr>,
483    ) -> Result<ContractInstanceWithAddress, Error> {
484        let existing_instance = self.pxe.get_contract_instance(&instance.address).await?;
485
486        match (existing_instance, artifact) {
487            (Some(existing), Some(artifact))
488                if existing.current_contract_class_id != instance.current_contract_class_id =>
489            {
490                self.pxe
491                    .update_contract(&instance.address, &artifact)
492                    .await?;
493            }
494            (Some(_), Some(_)) | (Some(_), None) => {}
495            (None, artifact) => {
496                let artifact = match artifact {
497                    Some(artifact) => artifact,
498                    None => self
499                        .pxe
500                        .get_contract_artifact(&instance.current_contract_class_id)
501                        .await?
502                        .ok_or_else(|| {
503                            Error::InvalidData(format!(
504                                "cannot register contract at {} without an artifact; class {} is not registered in PXE",
505                                instance.address, instance.current_contract_class_id
506                            ))
507                        })?,
508                };
509
510                self.pxe
511                    .register_contract(RegisterContractRequest {
512                        instance: instance.clone(),
513                        artifact: Some(artifact),
514                    })
515                    .await?;
516            }
517        }
518
519        if let Some(sk) = secret_key {
520            let complete_address = self
521                .accounts
522                .get_complete_address(&instance.address)
523                .await?
524                .ok_or_else(|| {
525                    Error::InvalidData(format!(
526                        "cannot register account for {}: account provider does not expose its complete address",
527                        instance.address
528                    ))
529                })?;
530            self.pxe
531                .register_account(&sk, &complete_address.partial_address)
532                .await?;
533        }
534
535        Ok(instance)
536    }
537
538    async fn get_private_events(
539        &self,
540        event_metadata: &EventMetadataDefinition,
541        filter: PrivateEventFilter,
542    ) -> Result<Vec<PrivateEvent>, Error> {
543        let pxe_filter = pxe::PrivateEventFilter {
544            contract_address: filter.contract_address,
545            tx_hash: filter.tx_hash,
546            from_block: filter.from_block,
547            to_block: filter.to_block,
548            after_log: filter.after_log.map(|l| pxe::LogId {
549                block_number: l.block_number,
550                block_hash: pxe::BlockHash::default(),
551                tx_hash: TxHash::zero(),
552                tx_index: 0,
553                log_index: l.log_index,
554            }),
555            scopes: filter.scopes,
556        };
557
558        let packed = self
559            .pxe
560            .get_private_events(&event_metadata.event_selector, pxe_filter)
561            .await?;
562
563        Ok(decode_private_events(packed, event_metadata))
564    }
565
566    async fn simulate_tx(
567        &self,
568        exec: ExecutionPayload,
569        opts: SimulateOptions,
570    ) -> Result<TxSimulationResult, Error> {
571        let mut exec = Self::merge_execution_payload(exec, &opts.auth_witnesses, &opts.capsules);
572        if let Some(ref fee_payload) = opts.fee_execution_payload {
573            exec.calls.extend(fee_payload.calls.clone());
574            exec.auth_witnesses
575                .extend(fee_payload.auth_witnesses.clone());
576            exec.capsules.extend(fee_payload.capsules.clone());
577            exec.extra_hashed_args
578                .extend(fee_payload.extra_hashed_args.clone());
579            if let Some(payer) = fee_payload.fee_payer {
580                exec.fee_payer = Some(payer);
581            }
582        }
583        let chain_info = self.get_chain_info().await?;
584        let gas_settings = opts.gas_settings.clone().unwrap_or_default();
585
586        let tx_request = self
587            .accounts
588            .create_tx_execution_request(&opts.from, exec, gas_settings, &chain_info, None, None)
589            .await?;
590
591        let scopes = Self::build_scopes(&opts.from, &opts.additional_scopes);
592
593        let pxe_opts = pxe::SimulateTxOpts {
594            simulate_public: true,
595            skip_tx_validation: opts.skip_validation,
596            skip_fee_enforcement: opts.skip_fee_enforcement,
597            overrides: None,
598            scopes: scopes.clone(),
599        };
600
601        let result = self.pxe.simulate_tx(&tx_request, pxe_opts).await?;
602        let mut return_values = result.data.clone();
603
604        // Simulate public calls on the node (mirrors upstream PXE behaviour).
605        // The local PXE only executes the private part; public functions are
606        // executed by the sequencer, so we need the node's AVM simulator to
607        // catch public-side errors (e.g. duplicate nullifiers, assertions).
608        // Extract gas_used from the PXE simulation (private-phase only)
609        let mut gas_used = result.data.get("gasUsed").map(|g| crate::fee::Gas {
610            da_gas: g.get("daGas").and_then(|v| v.as_u64()).unwrap_or(0),
611            l2_gas: g.get("l2Gas").and_then(|v| v.as_u64()).unwrap_or(0),
612        });
613
614        let proven = self.pxe.prove_tx(&tx_request, scopes).await?;
615        let tx = proven.to_tx();
616        if !tx.public_function_calldata.is_empty() {
617            let tx_hash = proven.tx_hash.ok_or_else(|| {
618                Error::InvalidData("PXE prove_tx result did not include a tx hash".into())
619            })?;
620            let tx_json = tx.to_json_value()?;
621            let simulation = self.simulate_public_calls(&tx_hash, &tx_json).await?;
622            let no_private_returns = Self::has_no_private_return_values(&return_values);
623            if no_private_returns {
624                if let Some(public_return_values) = simulation.get("publicReturnValues") {
625                    return_values = public_return_values.clone();
626                }
627            }
628            // Update gas_used with total gas from public simulation
629            // (includes both private and public phase gas)
630            if let Some(total_gas) = simulation.get("gasUsed").and_then(|g| g.get("totalGas")) {
631                gas_used = Some(crate::fee::Gas {
632                    da_gas: total_gas.get("daGas").and_then(|v| v.as_u64()).unwrap_or(0),
633                    l2_gas: total_gas.get("l2Gas").and_then(|v| v.as_u64()).unwrap_or(0),
634                });
635            }
636        }
637
638        Ok(TxSimulationResult {
639            return_values,
640            gas_used,
641        })
642    }
643
644    async fn execute_utility(
645        &self,
646        call: FunctionCall,
647        opts: ExecuteUtilityOptions,
648    ) -> Result<UtilityExecutionResult, Error> {
649        let pxe_opts = pxe::ExecuteUtilityOpts {
650            authwits: opts.auth_witnesses,
651            scopes: vec![opts.scope],
652        };
653
654        let result = self.pxe.execute_utility(&call, pxe_opts).await?;
655
656        Ok(UtilityExecutionResult {
657            result: serde_json::to_value(&result.result).unwrap_or(serde_json::Value::Null),
658            stats: result.stats,
659        })
660    }
661
662    async fn profile_tx(
663        &self,
664        exec: ExecutionPayload,
665        opts: ProfileOptions,
666    ) -> Result<TxProfileResult, Error> {
667        let exec = Self::merge_execution_payload(exec, &opts.auth_witnesses, &opts.capsules);
668        let chain_info = self.get_chain_info().await?;
669        let gas_settings = opts.gas_settings.clone().unwrap_or_default();
670
671        let tx_request = self
672            .accounts
673            .create_tx_execution_request(&opts.from, exec, gas_settings, &chain_info, None, None)
674            .await?;
675
676        let scopes = Self::build_scopes(&opts.from, &opts.additional_scopes);
677
678        let profile_mode = match opts.profile_mode {
679            Some(super::wallet::ProfileMode::ExecutionSteps) => pxe::ProfileMode::ExecutionSteps,
680            Some(super::wallet::ProfileMode::Gates) => pxe::ProfileMode::Gates,
681            Some(super::wallet::ProfileMode::Full) | None => pxe::ProfileMode::Full,
682        };
683
684        let pxe_opts = pxe::ProfileTxOpts {
685            profile_mode,
686            skip_proof_generation: opts.skip_proof_generation,
687            scopes,
688        };
689
690        let result = self.pxe.profile_tx(&tx_request, pxe_opts).await?;
691
692        Ok(TxProfileResult {
693            return_values: result.data.clone(),
694            gas_used: None,
695            profile_data: result.data,
696        })
697    }
698
699    async fn send_tx(
700        &self,
701        exec: ExecutionPayload,
702        opts: SendOptions,
703    ) -> Result<SendResult, Error> {
704        let mut exec = Self::merge_execution_payload(exec, &opts.auth_witnesses, &opts.capsules);
705        // Merge fee execution payload (e.g., FeeJuicePaymentMethodWithClaim)
706        if let Some(ref fee_payload) = opts.fee_execution_payload {
707            exec.calls.extend(fee_payload.calls.clone());
708            exec.auth_witnesses
709                .extend(fee_payload.auth_witnesses.clone());
710            exec.capsules.extend(fee_payload.capsules.clone());
711            exec.extra_hashed_args
712                .extend(fee_payload.extra_hashed_args.clone());
713            if let Some(payer) = fee_payload.fee_payer {
714                exec.fee_payer = Some(payer);
715            }
716        }
717        let chain_info = self.get_chain_info().await?;
718        let gas_settings = opts.gas_settings.clone().unwrap_or_default();
719        let fee_payer = exec.fee_payer;
720
721        let tx_request = self
722            .accounts
723            .create_tx_execution_request(
724                &opts.from,
725                exec,
726                gas_settings,
727                &chain_info,
728                fee_payer,
729                opts.fee_execution_payload.as_ref().map(|_| 2u8), // FeeJuiceWithClaim when fee payload present
730            )
731            .await?;
732
733        let scopes = Self::build_scopes(&opts.from, &opts.additional_scopes);
734
735        let (tx_hash, tx_json) = {
736            let proven = self.pxe.prove_tx(&tx_request, scopes.clone()).await?;
737            let tx_hash = proven.tx_hash.ok_or_else(|| {
738                Error::InvalidData("PXE prove_tx result did not include a tx hash".into())
739            })?;
740
741            let tx = proven.to_tx();
742            let tx_json = tx.to_json_value()?;
743            if !tx.public_function_calldata.is_empty() {
744                match self.simulate_public_calls(&tx_hash, &tx_json).await {
745                    Ok(_) => {}
746                    Err(err) if Self::should_ignore_public_preflight_error(&err) => {
747                        tracing::debug!(
748                            tx_hash = %tx_hash,
749                            error = %err,
750                            "ignoring public-call preflight error"
751                        );
752                    }
753                    Err(err) => return Err(err),
754                }
755            }
756            (tx_hash, tx_json)
757        };
758
759        match self.validate_tx_when_anchor_available(&tx_json).await? {
760            TxValidationResult::Valid => {}
761            TxValidationResult::Invalid { reason } => {
762                return Err(Error::InvalidData(format!(
763                    "node rejected tx {} during preflight validation: {}",
764                    tx_hash,
765                    reason.join(", ")
766                )));
767            }
768            TxValidationResult::Skipped { reason } => {
769                tracing::debug!(
770                    tx_hash = %tx_hash,
771                    reasons = %reason.join(", "),
772                    "node skipped tx preflight validation"
773                );
774            }
775        }
776        self.node.send_tx(&tx_json).await?;
777        self.wait_for_submission_checkpoint(&tx_hash).await?;
778
779        Ok(SendResult { tx_hash })
780    }
781
782    async fn wait_for_contract(&self, address: AztecAddress) -> Result<(), Error> {
783        let timeout = Duration::from_secs(30);
784        let interval = Duration::from_millis(250);
785        let stabilization = Duration::from_secs(2);
786        let start = Instant::now();
787
788        loop {
789            if let Some(contract) = self.node.get_contract(&address).await? {
790                let class_id = contract.current_contract_class_id;
791                if self.node.get_contract_class(&class_id).await?.is_some() {
792                    sleep(stabilization).await;
793                    if let Some(contract) = self.node.get_contract(&address).await? {
794                        if self
795                            .node
796                            .get_contract_class(&contract.current_contract_class_id)
797                            .await?
798                            .is_some()
799                        {
800                            return Ok(());
801                        }
802                    }
803                }
804            }
805
806            if start.elapsed() >= timeout {
807                return Err(Error::Timeout(format!(
808                    "contract {address} did not become node-visible within {:?}",
809                    timeout
810                )));
811            }
812
813            sleep(interval).await;
814        }
815    }
816
817    async fn wait_for_tx_proven(&self, tx_hash: TxHash) -> Result<(), Error> {
818        wait_for_tx(
819            &self.node,
820            &tx_hash,
821            WaitOpts {
822                wait_for_status: TxStatus::Proven,
823                timeout: Duration::from_secs(60),
824                interval: Duration::from_millis(500),
825                ..WaitOpts::default()
826            },
827        )
828        .await
829        .map(|_| ())
830    }
831
832    async fn create_auth_wit(
833        &self,
834        from: AztecAddress,
835        message_hash_or_intent: MessageHashOrIntent,
836    ) -> Result<AuthWitness, Error> {
837        let chain_info = self.get_chain_info().await?;
838        self.accounts
839            .create_auth_wit(&from, message_hash_or_intent, &chain_info)
840            .await
841    }
842
843    async fn get_public_storage_at(&self, contract: &AztecAddress, slot: &Fr) -> Result<Fr, Error> {
844        // Use 0 = "latest" to get the most current state.
845        self.node.get_public_storage_at(0, contract, slot).await
846    }
847}
848
849/// Decode packed private events from the PXE into wallet-level [`PrivateEvent`] objects.
850fn decode_private_events(
851    packed: Vec<pxe::PackedPrivateEvent>,
852    event_metadata: &EventMetadataDefinition,
853) -> Vec<PrivateEvent> {
854    let field_names = resolve_event_field_names(event_metadata);
855    packed
856        .into_iter()
857        .map(|pe| {
858            let mut event_map = serde_json::Map::new();
859            for (i, name) in field_names.iter().enumerate() {
860                if let Some(field) = pe.packed_event.get(i) {
861                    event_map.insert(
862                        name.clone(),
863                        serde_json::to_value(field).unwrap_or_default(),
864                    );
865                }
866            }
867
868            PrivateEvent {
869                event: serde_json::Value::Object(event_map),
870                metadata: PrivateEventMetadata {
871                    tx_hash: pe.tx_hash,
872                    block_number: Some(pe.l2_block_number),
873                    log_index: None,
874                },
875            }
876        })
877        .collect()
878}
879
880fn resolve_event_field_names(event_metadata: &EventMetadataDefinition) -> Vec<String> {
881    if !event_metadata.field_names.is_empty() {
882        return event_metadata.field_names.clone();
883    }
884
885    match &event_metadata.abi_type {
886        AbiType::Struct { fields, .. } => fields.iter().map(|field| field.name.clone()).collect(),
887        _ => vec![],
888    }
889}
890
891/// Create a [`BaseWallet`] connected to a PXE and node.
892pub fn create_wallet<P: Pxe, N: AztecNode, A: AccountProvider>(
893    pxe: P,
894    node: N,
895    accounts: A,
896) -> BaseWallet<P, N, A> {
897    BaseWallet::new(pxe, node, accounts)
898}
899
900/// Create a [`BaseWallet`] backed by an embedded PXE (in-process) and HTTP node.
901///
902/// This is the recommended way to create a wallet for Aztec v4.x, where PXE
903/// runs client-side. Only requires a single `node_url` — no separate PXE server.
904#[cfg(feature = "embedded-pxe")]
905pub async fn create_embedded_wallet<A: AccountProvider>(
906    node_url: impl Into<String>,
907    accounts: A,
908) -> Result<
909    BaseWallet<aztec_pxe::EmbeddedPxe<HttpNodeClient>, HttpNodeClient, A>,
910    crate::error::Error,
911> {
912    let node = create_aztec_node_client(node_url);
913    let pxe = aztec_pxe::EmbeddedPxe::create_ephemeral(node.clone()).await?;
914    Ok(BaseWallet::new(pxe, node, accounts))
915}
916
917// ---------------------------------------------------------------------------
918// Tests
919// ---------------------------------------------------------------------------
920
921#[cfg(test)]
922#[allow(clippy::unwrap_used, clippy::expect_used)]
923mod tests {
924    use super::*;
925    use crate::abi::{AbiParameter, AbiType, EventSelector};
926    use crate::fee::GasSettings;
927    use crate::node::{NodeInfo, PublicLogFilter, PublicLogsResponse};
928    use crate::pxe::{
929        BlockHeader, ExecuteUtilityOpts, PackedPrivateEvent, ProfileTxOpts, SimulateTxOpts,
930        TxExecutionRequest, TxProfileResult as PxeTxProfileResult, TxProvingResult,
931        TxSimulationResult as PxeTxSimulationResult, UtilityExecutionResult as PxeUtilityResult,
932    };
933    use crate::tx::{TxExecutionResult, TxReceipt, TxStatus};
934    use crate::types::{CompleteAddress, ContractInstance, PublicKeys};
935    use std::sync::Mutex;
936
937    // -----------------------------------------------------------------------
938    // Mock AztecNode
939    // -----------------------------------------------------------------------
940
941    struct MockNode {
942        info: NodeInfo,
943        contract: Mutex<Option<ContractInstanceWithAddress>>,
944        contract_class: Mutex<Option<serde_json::Value>>,
945        sent_txs: Mutex<Vec<serde_json::Value>>,
946    }
947
948    impl MockNode {
949        fn new() -> Self {
950            Self {
951                info: NodeInfo {
952                    node_version: "test-0.1.0".into(),
953                    l1_chain_id: 31337,
954                    rollup_version: 1,
955                    enr: None,
956                    l1_contract_addresses: serde_json::json!({}),
957                    protocol_contract_addresses: serde_json::json!({}),
958                    real_proofs: false,
959                    l2_circuits_vk_tree_root: None,
960                    l2_protocol_contracts_hash: None,
961                },
962                contract: Mutex::new(None),
963                contract_class: Mutex::new(None),
964                sent_txs: Mutex::new(vec![]),
965            }
966        }
967
968        fn with_contract(self, c: ContractInstanceWithAddress) -> Self {
969            *self.contract.lock().unwrap() = Some(c);
970            self
971        }
972
973        fn with_contract_class(self, c: serde_json::Value) -> Self {
974            *self.contract_class.lock().unwrap() = Some(c);
975            self
976        }
977    }
978
979    #[async_trait]
980    impl AztecNode for MockNode {
981        async fn get_node_info(&self) -> Result<NodeInfo, Error> {
982            Ok(self.info.clone())
983        }
984
985        async fn get_block_number(&self) -> Result<u64, Error> {
986            Ok(0)
987        }
988
989        async fn get_proven_block_number(&self) -> Result<u64, Error> {
990            Ok(0)
991        }
992
993        async fn get_tx_receipt(&self, _tx_hash: &TxHash) -> Result<TxReceipt, Error> {
994            Ok(TxReceipt {
995                tx_hash: TxHash::zero(),
996                status: TxStatus::Checkpointed,
997                execution_result: Some(TxExecutionResult::Success),
998                error: None,
999                transaction_fee: None,
1000                block_hash: None,
1001                block_number: None,
1002                epoch_number: None,
1003            })
1004        }
1005
1006        async fn get_tx_effect(
1007            &self,
1008            _tx_hash: &TxHash,
1009        ) -> Result<Option<serde_json::Value>, Error> {
1010            Ok(None)
1011        }
1012        async fn get_tx_by_hash(
1013            &self,
1014            _tx_hash: &TxHash,
1015        ) -> Result<Option<serde_json::Value>, Error> {
1016            Ok(None)
1017        }
1018
1019        async fn get_public_logs(
1020            &self,
1021            _filter: PublicLogFilter,
1022        ) -> Result<PublicLogsResponse, Error> {
1023            Ok(PublicLogsResponse {
1024                logs: vec![],
1025                max_logs_hit: false,
1026            })
1027        }
1028
1029        async fn send_tx(&self, tx: &serde_json::Value) -> Result<(), Error> {
1030            self.sent_txs.lock().unwrap().push(tx.clone());
1031            Ok(())
1032        }
1033
1034        async fn get_contract(
1035            &self,
1036            _address: &AztecAddress,
1037        ) -> Result<Option<ContractInstanceWithAddress>, Error> {
1038            Ok(self.contract.lock().unwrap().clone())
1039        }
1040
1041        async fn get_contract_class(&self, _id: &Fr) -> Result<Option<serde_json::Value>, Error> {
1042            Ok(self.contract_class.lock().unwrap().clone())
1043        }
1044
1045        async fn get_block_header(&self, _block_number: u64) -> Result<serde_json::Value, Error> {
1046            Ok(serde_json::json!({"blockNumber": 1}))
1047        }
1048        async fn get_block(&self, _block_number: u64) -> Result<Option<serde_json::Value>, Error> {
1049            Ok(None)
1050        }
1051        async fn get_note_hash_membership_witness(
1052            &self,
1053            _block_number: u64,
1054            _note_hash: &Fr,
1055        ) -> Result<Option<serde_json::Value>, Error> {
1056            Ok(None)
1057        }
1058        async fn get_nullifier_membership_witness(
1059            &self,
1060            _block_number: u64,
1061            _nullifier: &Fr,
1062        ) -> Result<Option<serde_json::Value>, Error> {
1063            Ok(None)
1064        }
1065        async fn get_low_nullifier_membership_witness(
1066            &self,
1067            _block_number: u64,
1068            _nullifier: &Fr,
1069        ) -> Result<Option<serde_json::Value>, Error> {
1070            Ok(None)
1071        }
1072        async fn get_public_storage_at(
1073            &self,
1074            _block_number: u64,
1075            _contract: &AztecAddress,
1076            _slot: &Fr,
1077        ) -> Result<Fr, Error> {
1078            Ok(Fr::zero())
1079        }
1080        async fn get_public_data_witness(
1081            &self,
1082            _block_number: u64,
1083            _leaf_slot: &Fr,
1084        ) -> Result<Option<serde_json::Value>, Error> {
1085            Ok(None)
1086        }
1087        async fn get_l1_to_l2_message_membership_witness(
1088            &self,
1089            _block_number: u64,
1090            _entry_key: &Fr,
1091        ) -> Result<Option<serde_json::Value>, Error> {
1092            Ok(None)
1093        }
1094        async fn simulate_public_calls(
1095            &self,
1096            _tx: &serde_json::Value,
1097            _skip_fee_enforcement: bool,
1098        ) -> Result<serde_json::Value, Error> {
1099            Ok(serde_json::Value::Null)
1100        }
1101        async fn is_valid_tx(
1102            &self,
1103            _tx: &serde_json::Value,
1104        ) -> Result<crate::node::TxValidationResult, Error> {
1105            Ok(crate::node::TxValidationResult::Valid)
1106        }
1107        async fn get_private_logs_by_tags(&self, _tags: &[Fr]) -> Result<serde_json::Value, Error> {
1108            Ok(serde_json::json!([]))
1109        }
1110        async fn get_public_logs_by_tags_from_contract(
1111            &self,
1112            _contract: &AztecAddress,
1113            _tags: &[Fr],
1114        ) -> Result<serde_json::Value, Error> {
1115            Ok(serde_json::json!([]))
1116        }
1117        async fn register_contract_function_signatures(
1118            &self,
1119            _signatures: &[String],
1120        ) -> Result<(), Error> {
1121            Ok(())
1122        }
1123        async fn get_block_hash_membership_witness(
1124            &self,
1125            _block_number: u64,
1126            _block_hash: &Fr,
1127        ) -> Result<Option<serde_json::Value>, Error> {
1128            Ok(None)
1129        }
1130        async fn find_leaves_indexes(
1131            &self,
1132            _block_number: u64,
1133            _tree_id: &str,
1134            _leaves: &[Fr],
1135        ) -> Result<Vec<Option<u64>>, Error> {
1136            Ok(vec![])
1137        }
1138    }
1139
1140    // -----------------------------------------------------------------------
1141    // Mock PXE
1142    // -----------------------------------------------------------------------
1143
1144    struct MockPxe {
1145        senders: Mutex<Vec<AztecAddress>>,
1146        contract_instance: Mutex<Option<ContractInstanceWithAddress>>,
1147        contract_artifact: Mutex<Option<ContractArtifact>>,
1148        registered_contracts: Mutex<Vec<RegisterContractRequest>>,
1149        updated_contracts: Mutex<Vec<(AztecAddress, ContractArtifact)>>,
1150        registered_accounts: Mutex<Vec<(Fr, Fr)>>,
1151        simulate_opts: Mutex<Vec<SimulateTxOpts>>,
1152        prove_scopes: Mutex<Vec<Vec<AztecAddress>>>,
1153        profile_opts: Mutex<Vec<ProfileTxOpts>>,
1154        utility_opts: Mutex<Vec<ExecuteUtilityOpts>>,
1155        simulate_result: PxeTxSimulationResult,
1156        profile_result: PxeTxProfileResult,
1157        proving_result: TxProvingResult,
1158        utility_result: PxeUtilityResult,
1159        packed_events: Vec<PackedPrivateEvent>,
1160    }
1161
1162    impl MockPxe {
1163        fn new() -> Self {
1164            Self {
1165                senders: Mutex::new(vec![]),
1166                contract_instance: Mutex::new(None),
1167                contract_artifact: Mutex::new(None),
1168                registered_contracts: Mutex::new(vec![]),
1169                updated_contracts: Mutex::new(vec![]),
1170                registered_accounts: Mutex::new(vec![]),
1171                simulate_opts: Mutex::new(vec![]),
1172                prove_scopes: Mutex::new(vec![]),
1173                profile_opts: Mutex::new(vec![]),
1174                utility_opts: Mutex::new(vec![]),
1175                simulate_result: PxeTxSimulationResult {
1176                    data: serde_json::json!({"returnValues": [42]}),
1177                },
1178                profile_result: PxeTxProfileResult {
1179                    data: serde_json::json!({"profileData": "test"}),
1180                },
1181                proving_result: TxProvingResult {
1182                    tx_hash: Some(
1183                        TxHash::from_hex(
1184                            "0x00000000000000000000000000000000000000000000000000000000deadbeef",
1185                        )
1186                        .expect("test hash"),
1187                    ),
1188                    private_execution_result: serde_json::json!({}),
1189                    public_inputs: aztec_core::tx::PrivateKernelTailCircuitPublicInputs::from_bytes(
1190                        vec![0],
1191                    ),
1192                    chonk_proof: aztec_core::tx::ChonkProof::from_bytes(vec![0]),
1193                    contract_class_log_fields: vec![],
1194                    public_function_calldata: vec![],
1195                    stats: None,
1196                },
1197                utility_result: PxeUtilityResult {
1198                    result: vec![Fr::from(99u64)],
1199                    stats: None,
1200                },
1201                packed_events: vec![],
1202            }
1203        }
1204
1205        fn with_senders(self, senders: Vec<AztecAddress>) -> Self {
1206            *self.senders.lock().unwrap() = senders;
1207            self
1208        }
1209
1210        fn with_contract_instance(self, inst: ContractInstanceWithAddress) -> Self {
1211            *self.contract_instance.lock().unwrap() = Some(inst);
1212            self
1213        }
1214
1215        fn with_contract_artifact(self, art: ContractArtifact) -> Self {
1216            *self.contract_artifact.lock().unwrap() = Some(art);
1217            self
1218        }
1219
1220        fn with_packed_events(mut self, events: Vec<PackedPrivateEvent>) -> Self {
1221            self.packed_events = events;
1222            self
1223        }
1224    }
1225
1226    #[async_trait]
1227    impl Pxe for MockPxe {
1228        async fn get_synced_block_header(&self) -> Result<BlockHeader, Error> {
1229            Ok(BlockHeader {
1230                data: serde_json::json!({}),
1231            })
1232        }
1233
1234        async fn get_contract_instance(
1235            &self,
1236            _address: &AztecAddress,
1237        ) -> Result<Option<ContractInstanceWithAddress>, Error> {
1238            Ok(self.contract_instance.lock().unwrap().clone())
1239        }
1240
1241        async fn get_contract_artifact(&self, _id: &Fr) -> Result<Option<ContractArtifact>, Error> {
1242            Ok(self.contract_artifact.lock().unwrap().clone())
1243        }
1244
1245        async fn get_contracts(&self) -> Result<Vec<AztecAddress>, Error> {
1246            Ok(vec![])
1247        }
1248
1249        async fn register_account(
1250            &self,
1251            secret_key: &Fr,
1252            partial_address: &Fr,
1253        ) -> Result<CompleteAddress, Error> {
1254            self.registered_accounts
1255                .lock()
1256                .unwrap()
1257                .push((*secret_key, *partial_address));
1258            Ok(CompleteAddress {
1259                address: AztecAddress(Fr::from(1u64)),
1260                public_keys: PublicKeys::default(),
1261                partial_address: *partial_address,
1262            })
1263        }
1264
1265        async fn get_registered_accounts(&self) -> Result<Vec<CompleteAddress>, Error> {
1266            Ok(vec![])
1267        }
1268
1269        async fn register_sender(&self, sender: &AztecAddress) -> Result<AztecAddress, Error> {
1270            self.senders.lock().unwrap().push(*sender);
1271            Ok(*sender)
1272        }
1273
1274        async fn get_senders(&self) -> Result<Vec<AztecAddress>, Error> {
1275            Ok(self.senders.lock().unwrap().clone())
1276        }
1277
1278        async fn remove_sender(&self, _sender: &AztecAddress) -> Result<(), Error> {
1279            Ok(())
1280        }
1281
1282        async fn register_contract_class(&self, _artifact: &ContractArtifact) -> Result<(), Error> {
1283            Ok(())
1284        }
1285
1286        async fn register_contract(&self, request: RegisterContractRequest) -> Result<(), Error> {
1287            self.registered_contracts.lock().unwrap().push(request);
1288            Ok(())
1289        }
1290
1291        async fn update_contract(
1292            &self,
1293            address: &AztecAddress,
1294            artifact: &ContractArtifact,
1295        ) -> Result<(), Error> {
1296            self.updated_contracts
1297                .lock()
1298                .unwrap()
1299                .push((*address, artifact.clone()));
1300            Ok(())
1301        }
1302
1303        async fn simulate_tx(
1304            &self,
1305            _tx_request: &TxExecutionRequest,
1306            opts: SimulateTxOpts,
1307        ) -> Result<PxeTxSimulationResult, Error> {
1308            self.simulate_opts.lock().unwrap().push(opts);
1309            Ok(self.simulate_result.clone())
1310        }
1311
1312        async fn prove_tx(
1313            &self,
1314            _tx_request: &TxExecutionRequest,
1315            scopes: Vec<AztecAddress>,
1316        ) -> Result<TxProvingResult, Error> {
1317            self.prove_scopes.lock().unwrap().push(scopes);
1318            Ok(self.proving_result.clone())
1319        }
1320
1321        async fn profile_tx(
1322            &self,
1323            _tx_request: &TxExecutionRequest,
1324            opts: ProfileTxOpts,
1325        ) -> Result<PxeTxProfileResult, Error> {
1326            self.profile_opts.lock().unwrap().push(opts);
1327            Ok(self.profile_result.clone())
1328        }
1329
1330        async fn execute_utility(
1331            &self,
1332            _call: &FunctionCall,
1333            opts: ExecuteUtilityOpts,
1334        ) -> Result<PxeUtilityResult, Error> {
1335            self.utility_opts.lock().unwrap().push(opts);
1336            Ok(self.utility_result.clone())
1337        }
1338
1339        async fn get_private_events(
1340            &self,
1341            _event_selector: &EventSelector,
1342            _filter: pxe::PrivateEventFilter,
1343        ) -> Result<Vec<PackedPrivateEvent>, Error> {
1344            Ok(self.packed_events.clone())
1345        }
1346
1347        async fn stop(&self) -> Result<(), Error> {
1348            Ok(())
1349        }
1350    }
1351
1352    // -----------------------------------------------------------------------
1353    // Mock AccountProvider
1354    // -----------------------------------------------------------------------
1355
1356    struct MockAccountProvider {
1357        accounts: Vec<Aliased<AztecAddress>>,
1358        complete_addresses: Vec<CompleteAddress>,
1359        created_execs: Mutex<Vec<ExecutionPayload>>,
1360    }
1361
1362    impl MockAccountProvider {
1363        fn new(accounts: Vec<Aliased<AztecAddress>>) -> Self {
1364            Self {
1365                accounts,
1366                complete_addresses: vec![],
1367                created_execs: Mutex::new(vec![]),
1368            }
1369        }
1370
1371        fn single(address: AztecAddress) -> Self {
1372            Self {
1373                accounts: vec![Aliased {
1374                    alias: "test".to_owned(),
1375                    item: address,
1376                }],
1377                complete_addresses: vec![],
1378                created_execs: Mutex::new(vec![]),
1379            }
1380        }
1381
1382        fn single_with_complete(address: AztecAddress, partial_address: Fr) -> Self {
1383            Self {
1384                accounts: vec![Aliased {
1385                    alias: "test".to_owned(),
1386                    item: address,
1387                }],
1388                complete_addresses: vec![CompleteAddress {
1389                    address,
1390                    public_keys: PublicKeys::default(),
1391                    partial_address,
1392                }],
1393                created_execs: Mutex::new(vec![]),
1394            }
1395        }
1396    }
1397
1398    #[async_trait]
1399    impl AccountProvider for MockAccountProvider {
1400        async fn create_tx_execution_request(
1401            &self,
1402            from: &AztecAddress,
1403            exec: ExecutionPayload,
1404            _gas_settings: GasSettings,
1405            _chain_info: &ChainInfo,
1406            _fee_payer: Option<AztecAddress>,
1407            _fee_payment_method: Option<u8>,
1408        ) -> Result<TxExecutionRequest, Error> {
1409            if !self.accounts.iter().any(|a| a.item == *from) {
1410                return Err(Error::InvalidData(format!("account not found: {from}")));
1411            }
1412            self.created_execs.lock().unwrap().push(exec);
1413            Ok(TxExecutionRequest {
1414                data: serde_json::json!({
1415                    "origin": from.to_string(),
1416                    "calls": [],
1417                }),
1418            })
1419        }
1420
1421        async fn create_auth_wit(
1422            &self,
1423            from: &AztecAddress,
1424            _intent: MessageHashOrIntent,
1425            _chain_info: &ChainInfo,
1426        ) -> Result<AuthWitness, Error> {
1427            if !self.accounts.iter().any(|a| a.item == *from) {
1428                return Err(Error::InvalidData(format!("account not found: {from}")));
1429            }
1430            Ok(AuthWitness {
1431                fields: vec![Fr::from(1u64), Fr::from(2u64)],
1432                ..Default::default()
1433            })
1434        }
1435
1436        async fn get_complete_address(
1437            &self,
1438            address: &AztecAddress,
1439        ) -> Result<Option<CompleteAddress>, Error> {
1440            Ok(self
1441                .complete_addresses
1442                .iter()
1443                .find(|complete| complete.address == *address)
1444                .cloned())
1445        }
1446
1447        async fn get_accounts(&self) -> Result<Vec<Aliased<AztecAddress>>, Error> {
1448            Ok(self.accounts.clone())
1449        }
1450    }
1451
1452    // -----------------------------------------------------------------------
1453    // Helpers
1454    // -----------------------------------------------------------------------
1455
1456    fn sample_instance() -> ContractInstanceWithAddress {
1457        ContractInstanceWithAddress {
1458            address: AztecAddress(Fr::from(1u64)),
1459            inner: ContractInstance {
1460                version: 1,
1461                salt: Fr::from(42u64),
1462                deployer: AztecAddress(Fr::from(2u64)),
1463                current_contract_class_id: Fr::from(100u64),
1464                original_contract_class_id: Fr::from(100u64),
1465                initialization_hash: Fr::from(0u64),
1466                public_keys: PublicKeys::default(),
1467            },
1468        }
1469    }
1470
1471    fn sample_artifact() -> ContractArtifact {
1472        ContractArtifact {
1473            name: "TestContract".to_owned(),
1474            functions: vec![],
1475            outputs: None,
1476            file_map: None,
1477            context_inputs_sizes: None,
1478        }
1479    }
1480
1481    fn test_address() -> AztecAddress {
1482        AztecAddress(Fr::from(1u64))
1483    }
1484
1485    fn make_wallet(
1486        pxe: MockPxe,
1487        node: MockNode,
1488        accounts: MockAccountProvider,
1489    ) -> BaseWallet<MockPxe, MockNode, MockAccountProvider> {
1490        BaseWallet::new(pxe, node, accounts)
1491    }
1492
1493    // -----------------------------------------------------------------------
1494    // Tests
1495    // -----------------------------------------------------------------------
1496
1497    #[test]
1498    fn base_wallet_is_send_sync() {
1499        fn assert_send_sync<T: Send + Sync>() {}
1500        assert_send_sync::<BaseWallet<MockPxe, MockNode, MockAccountProvider>>();
1501    }
1502
1503    #[tokio::test]
1504    async fn test_get_chain_info() {
1505        let wallet = make_wallet(
1506            MockPxe::new(),
1507            MockNode::new(),
1508            MockAccountProvider::new(vec![]),
1509        );
1510        let info = wallet.get_chain_info().await.expect("get chain info");
1511        assert_eq!(info.chain_id, Fr::from(31337u64));
1512        assert_eq!(info.version, Fr::from(1u64));
1513    }
1514
1515    #[tokio::test]
1516    async fn test_get_accounts() {
1517        let addr = test_address();
1518        let wallet = make_wallet(
1519            MockPxe::new(),
1520            MockNode::new(),
1521            MockAccountProvider::single(addr),
1522        );
1523        let accounts = wallet.get_accounts().await.expect("get accounts");
1524        assert_eq!(accounts.len(), 1);
1525        assert_eq!(accounts[0].item, addr);
1526        assert_eq!(accounts[0].alias, "test");
1527    }
1528
1529    #[tokio::test]
1530    async fn test_get_address_book() {
1531        let senders = vec![AztecAddress(Fr::from(10u64)), AztecAddress(Fr::from(20u64))];
1532        let wallet = make_wallet(
1533            MockPxe::new().with_senders(senders.clone()),
1534            MockNode::new(),
1535            MockAccountProvider::new(vec![]),
1536        );
1537        let book = wallet.get_address_book().await.expect("get address book");
1538        assert_eq!(book.len(), 2);
1539        assert_eq!(book[0].item, senders[0]);
1540        assert!(book[0].alias.is_empty());
1541    }
1542
1543    #[tokio::test]
1544    async fn test_register_sender() {
1545        let wallet = make_wallet(
1546            MockPxe::new(),
1547            MockNode::new(),
1548            MockAccountProvider::new(vec![]),
1549        );
1550        let addr = AztecAddress(Fr::from(99u64));
1551        let result = wallet
1552            .register_sender(addr, Some("bob".into()))
1553            .await
1554            .expect("register sender");
1555        assert_eq!(result, addr);
1556    }
1557
1558    #[tokio::test]
1559    async fn test_register_contract() {
1560        let instance = sample_instance();
1561        let artifact = sample_artifact();
1562        let wallet = make_wallet(
1563            MockPxe::new(),
1564            MockNode::new(),
1565            MockAccountProvider::new(vec![]),
1566        );
1567        let result = wallet
1568            .register_contract(instance.clone(), Some(artifact.clone()), None)
1569            .await
1570            .expect("register contract");
1571        assert_eq!(result.address, instance.address);
1572
1573        let registered = wallet.pxe.registered_contracts.lock().unwrap();
1574        assert_eq!(registered.len(), 1);
1575        assert_eq!(registered[0].instance.address, instance.address);
1576        assert_eq!(
1577            registered[0]
1578                .artifact
1579                .as_ref()
1580                .expect("artifact attached")
1581                .name,
1582            artifact.name,
1583        );
1584    }
1585
1586    #[tokio::test]
1587    async fn test_register_contract_with_secret_key() {
1588        let partial_address = Fr::from(777u64);
1589        let wallet = make_wallet(
1590            MockPxe::new(),
1591            MockNode::new(),
1592            MockAccountProvider::single_with_complete(test_address(), partial_address),
1593        );
1594        let instance = sample_instance();
1595        let sk = Fr::from(12345u64);
1596        wallet
1597            .register_contract(instance.clone(), Some(sample_artifact()), Some(sk))
1598            .await
1599            .expect("register contract with sk");
1600
1601        // Account registration must use the managed partial address from the
1602        // account provider, not a zero or caller-supplied value.
1603        let accounts = wallet.pxe.registered_accounts.lock().unwrap();
1604        assert_eq!(accounts.len(), 1);
1605        assert_eq!(accounts[0].0, sk);
1606        assert_eq!(accounts[0].1, partial_address);
1607    }
1608
1609    #[tokio::test]
1610    async fn test_register_contract_falls_back_to_pxe_artifact() {
1611        let wallet = make_wallet(
1612            MockPxe::new().with_contract_artifact(sample_artifact()),
1613            MockNode::new(),
1614            MockAccountProvider::new(vec![]),
1615        );
1616        let instance = sample_instance();
1617        wallet
1618            .register_contract(instance.clone(), None, None)
1619            .await
1620            .expect("register with PXE-stored artifact");
1621
1622        let registered = wallet.pxe.registered_contracts.lock().unwrap();
1623        assert_eq!(registered.len(), 1);
1624        assert_eq!(
1625            registered[0]
1626                .artifact
1627                .as_ref()
1628                .expect("artifact resolved from PXE")
1629                .name,
1630            "TestContract",
1631        );
1632    }
1633
1634    #[tokio::test]
1635    async fn test_register_contract_reuses_existing_registration() {
1636        let wallet = make_wallet(
1637            MockPxe::new().with_contract_instance(sample_instance()),
1638            MockNode::new(),
1639            MockAccountProvider::new(vec![]),
1640        );
1641
1642        wallet
1643            .register_contract(sample_instance(), None, None)
1644            .await
1645            .expect("reuse existing registration");
1646
1647        assert!(wallet.pxe.registered_contracts.lock().unwrap().is_empty());
1648    }
1649
1650    #[tokio::test]
1651    async fn test_register_contract_updates_existing_registration() {
1652        let existing = sample_instance();
1653        let mut updated = sample_instance();
1654        updated.inner.current_contract_class_id = Fr::from(200u64);
1655        let artifact = sample_artifact();
1656        let wallet = make_wallet(
1657            MockPxe::new().with_contract_instance(existing),
1658            MockNode::new(),
1659            MockAccountProvider::new(vec![]),
1660        );
1661
1662        wallet
1663            .register_contract(updated.clone(), Some(artifact.clone()), None)
1664            .await
1665            .expect("update existing registration");
1666
1667        let updated_contracts = wallet.pxe.updated_contracts.lock().unwrap();
1668        assert_eq!(updated_contracts.len(), 1);
1669        assert_eq!(updated_contracts[0].0, updated.address);
1670        assert_eq!(updated_contracts[0].1.name, artifact.name);
1671    }
1672
1673    #[tokio::test]
1674    async fn test_get_contract_metadata_not_published() {
1675        let instance = sample_instance();
1676        let wallet = make_wallet(
1677            MockPxe::new().with_contract_instance(instance.clone()),
1678            MockNode::new(),
1679            MockAccountProvider::new(vec![]),
1680        );
1681        let meta = wallet
1682            .get_contract_metadata(instance.address)
1683            .await
1684            .expect("get contract metadata");
1685        assert!(meta.instance.is_some());
1686        assert!(!meta.is_contract_published);
1687        assert!(!meta.is_contract_initialized);
1688    }
1689
1690    #[tokio::test]
1691    async fn test_get_contract_metadata_published() {
1692        let instance = sample_instance();
1693        let wallet = make_wallet(
1694            MockPxe::new().with_contract_instance(instance.clone()),
1695            MockNode::new().with_contract(instance.clone()),
1696            MockAccountProvider::new(vec![]),
1697        );
1698        let meta = wallet
1699            .get_contract_metadata(instance.address)
1700            .await
1701            .expect("get contract metadata");
1702        assert!(meta.instance.is_some());
1703        assert!(meta.is_contract_published);
1704        assert!(meta.is_contract_initialized);
1705    }
1706
1707    #[tokio::test]
1708    async fn test_get_contract_metadata_updated() {
1709        let instance = sample_instance();
1710        let mut on_chain = sample_instance();
1711        on_chain.inner.current_contract_class_id = Fr::from(200u64);
1712        let wallet = make_wallet(
1713            MockPxe::new().with_contract_instance(instance),
1714            MockNode::new().with_contract(on_chain),
1715            MockAccountProvider::new(vec![]),
1716        );
1717
1718        let meta = wallet
1719            .get_contract_metadata(test_address())
1720            .await
1721            .expect("get updated contract metadata");
1722        assert!(meta.is_contract_updated);
1723        assert_eq!(meta.updated_contract_class_id, Some(Fr::from(200u64)));
1724    }
1725
1726    #[tokio::test]
1727    async fn test_get_contract_class_metadata() {
1728        let art = sample_artifact();
1729        let wallet = make_wallet(
1730            MockPxe::new().with_contract_artifact(art),
1731            MockNode::new().with_contract_class(serde_json::json!({"id": "0x64"})),
1732            MockAccountProvider::new(vec![]),
1733        );
1734        let meta = wallet
1735            .get_contract_class_metadata(Fr::from(100u64))
1736            .await
1737            .expect("get contract class metadata");
1738        assert!(meta.is_artifact_registered);
1739        assert!(meta.is_contract_class_publicly_registered);
1740    }
1741
1742    #[tokio::test]
1743    async fn test_get_contract_class_metadata_not_registered() {
1744        let wallet = make_wallet(
1745            MockPxe::new(),
1746            MockNode::new(),
1747            MockAccountProvider::new(vec![]),
1748        );
1749        let meta = wallet
1750            .get_contract_class_metadata(Fr::from(100u64))
1751            .await
1752            .expect("get contract class metadata");
1753        assert!(!meta.is_artifact_registered);
1754        assert!(!meta.is_contract_class_publicly_registered);
1755    }
1756
1757    #[tokio::test]
1758    async fn test_simulate_tx() {
1759        let addr = test_address();
1760        let wallet = make_wallet(
1761            MockPxe::new(),
1762            MockNode::new(),
1763            MockAccountProvider::single(addr),
1764        );
1765        let result = wallet
1766            .simulate_tx(
1767                ExecutionPayload::default(),
1768                SimulateOptions {
1769                    from: addr,
1770                    ..Default::default()
1771                },
1772            )
1773            .await
1774            .expect("simulate tx");
1775        assert_eq!(
1776            result.return_values,
1777            serde_json::json!({"returnValues": [42]})
1778        );
1779
1780        let simulate_opts = wallet.pxe.simulate_opts.lock().unwrap();
1781        assert_eq!(simulate_opts.len(), 1);
1782        assert!(simulate_opts[0].skip_fee_enforcement);
1783    }
1784
1785    #[tokio::test]
1786    async fn test_send_tx() {
1787        let addr = test_address();
1788        let wallet = make_wallet(
1789            MockPxe::new(),
1790            MockNode::new(),
1791            MockAccountProvider::single(addr),
1792        );
1793        let result = wallet
1794            .send_tx(
1795                ExecutionPayload::default(),
1796                SendOptions {
1797                    from: addr,
1798                    ..Default::default()
1799                },
1800            )
1801            .await
1802            .expect("send tx");
1803        assert_eq!(
1804            result.tx_hash,
1805            TxHash::from_hex("0x00000000000000000000000000000000000000000000000000000000deadbeef")
1806                .unwrap()
1807        );
1808
1809        // Verify node received the proven tx
1810        let sent = wallet.node.sent_txs.lock().unwrap();
1811        assert_eq!(sent.len(), 1);
1812
1813        let scopes = wallet.pxe.prove_scopes.lock().unwrap();
1814        assert_eq!(scopes.as_slice(), &[vec![addr]]);
1815    }
1816
1817    #[tokio::test]
1818    async fn test_create_auth_wit() {
1819        let addr = test_address();
1820        let wallet = make_wallet(
1821            MockPxe::new(),
1822            MockNode::new(),
1823            MockAccountProvider::single(addr),
1824        );
1825        let wit = wallet
1826            .create_auth_wit(
1827                addr,
1828                MessageHashOrIntent::Hash {
1829                    hash: Fr::from(42u64),
1830                },
1831            )
1832            .await
1833            .expect("create auth wit");
1834        assert_eq!(wit.fields.len(), 2);
1835        assert_eq!(wit.fields[0], Fr::from(1u64));
1836    }
1837
1838    #[tokio::test]
1839    async fn test_create_auth_wit_unknown_account() {
1840        let wallet = make_wallet(
1841            MockPxe::new(),
1842            MockNode::new(),
1843            MockAccountProvider::new(vec![]),
1844        );
1845        let result = wallet
1846            .create_auth_wit(
1847                AztecAddress(Fr::from(999u64)),
1848                MessageHashOrIntent::Hash {
1849                    hash: Fr::from(1u64),
1850                },
1851            )
1852            .await;
1853        assert!(result.is_err());
1854    }
1855
1856    #[tokio::test]
1857    async fn test_execute_utility() {
1858        let wallet = make_wallet(
1859            MockPxe::new(),
1860            MockNode::new(),
1861            MockAccountProvider::new(vec![]),
1862        );
1863        let call = FunctionCall {
1864            to: AztecAddress(Fr::from(1u64)),
1865            selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd").expect("valid selector"),
1866            args: vec![],
1867            function_type: crate::abi::FunctionType::Utility,
1868            is_static: true,
1869            hide_msg_sender: false,
1870        };
1871        let result = wallet
1872            .execute_utility(call, ExecuteUtilityOptions::default())
1873            .await
1874            .expect("execute utility");
1875        assert_ne!(result.result, serde_json::Value::Null);
1876
1877        let utility_opts = wallet.pxe.utility_opts.lock().unwrap();
1878        assert_eq!(
1879            utility_opts.as_slice(),
1880            &[ExecuteUtilityOpts {
1881                authwits: vec![],
1882                scopes: vec![AztecAddress(Fr::zero())],
1883            }]
1884        );
1885    }
1886
1887    #[tokio::test]
1888    async fn test_profile_tx() {
1889        let addr = test_address();
1890        let wallet = make_wallet(
1891            MockPxe::new(),
1892            MockNode::new(),
1893            MockAccountProvider::single(addr),
1894        );
1895        let result = wallet
1896            .profile_tx(
1897                ExecutionPayload::default(),
1898                ProfileOptions {
1899                    from: addr,
1900                    ..Default::default()
1901                },
1902            )
1903            .await
1904            .expect("profile tx");
1905        assert_ne!(result.profile_data, serde_json::Value::Null);
1906
1907        let profile_opts = wallet.pxe.profile_opts.lock().unwrap();
1908        assert_eq!(profile_opts.len(), 1);
1909        assert!(profile_opts[0].skip_proof_generation);
1910    }
1911
1912    #[tokio::test]
1913    async fn test_wallet_options_are_merged_into_execution_payload() {
1914        let addr = test_address();
1915        let wallet = make_wallet(
1916            MockPxe::new(),
1917            MockNode::new(),
1918            MockAccountProvider::single(addr),
1919        );
1920
1921        wallet
1922            .simulate_tx(
1923                ExecutionPayload::default(),
1924                SimulateOptions {
1925                    from: addr,
1926                    auth_witnesses: vec![AuthWitness {
1927                        fields: vec![Fr::from(9u64)],
1928                        ..Default::default()
1929                    }],
1930                    capsules: vec![crate::tx::Capsule {
1931                        contract_address: AztecAddress(Fr::zero()),
1932                        storage_slot: Fr::zero(),
1933                        data: vec![Fr::from(1u64), Fr::from(2u64), Fr::from(3u64)],
1934                    }],
1935                    ..Default::default()
1936                },
1937            )
1938            .await
1939            .expect("simulate tx with wallet options");
1940
1941        let created_execs = wallet.accounts.created_execs.lock().unwrap();
1942        assert_eq!(created_execs.len(), 1);
1943        assert_eq!(created_execs[0].auth_witnesses.len(), 1);
1944        assert_eq!(created_execs[0].capsules.len(), 1);
1945    }
1946
1947    #[tokio::test]
1948    async fn test_get_private_events() {
1949        let packed = vec![PackedPrivateEvent {
1950            packed_event: vec![Fr::from(100u64), Fr::from(200u64)],
1951            tx_hash: TxHash::zero(),
1952            l2_block_number: 5,
1953            l2_block_hash: pxe::BlockHash::default(),
1954            event_selector: EventSelector(Fr::from(1u64)),
1955        }];
1956        let wallet = make_wallet(
1957            MockPxe::new().with_packed_events(packed),
1958            MockNode::new(),
1959            MockAccountProvider::new(vec![]),
1960        );
1961
1962        let event_metadata = EventMetadataDefinition {
1963            event_selector: EventSelector(Fr::from(1u64)),
1964            abi_type: AbiType::Struct {
1965                name: "Transfer".to_owned(),
1966                fields: vec![
1967                    AbiParameter {
1968                        name: "amount".to_owned(),
1969                        typ: AbiType::Field,
1970                        visibility: None,
1971                    },
1972                    AbiParameter {
1973                        name: "sender".to_owned(),
1974                        typ: AbiType::Field,
1975                        visibility: None,
1976                    },
1977                ],
1978            },
1979            field_names: vec!["amount".to_owned(), "sender".to_owned()],
1980        };
1981
1982        let events = wallet
1983            .get_private_events(
1984                &event_metadata,
1985                PrivateEventFilter {
1986                    contract_address: AztecAddress(Fr::from(1u64)),
1987                    ..Default::default()
1988                },
1989            )
1990            .await
1991            .expect("get private events");
1992
1993        assert_eq!(events.len(), 1);
1994        assert_eq!(events[0].metadata.block_number, Some(5));
1995        assert_eq!(events[0].metadata.tx_hash, TxHash::zero());
1996
1997        // Verify decoded fields
1998        let event = &events[0].event;
1999        assert!(event.get("amount").is_some());
2000        assert!(event.get("sender").is_some());
2001    }
2002
2003    #[tokio::test]
2004    async fn test_create_wallet_factory() {
2005        let wallet = create_wallet(
2006            MockPxe::new(),
2007            MockNode::new(),
2008            MockAccountProvider::new(vec![]),
2009        );
2010        let info = wallet.get_chain_info().await.expect("get chain info");
2011        assert_eq!(info.chain_id, Fr::from(31337u64));
2012    }
2013}