aztec_pxe_client/
pxe.rs

1use std::{fmt, vec};
2
3use async_trait::async_trait;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use aztec_core::abi::{ContractArtifact, EventSelector};
7use aztec_core::error::Error;
8use aztec_core::tx::{
9    AuthWitness, ChonkProof, ContractClassLogFields, FunctionCall, HashedValues,
10    PrivateKernelTailCircuitPublicInputs, Tx, TxHash,
11};
12use aztec_core::types::{AztecAddress, CompleteAddress, ContractInstanceWithAddress, Fr};
13
14// ---------------------------------------------------------------------------
15// Supporting types — opaque wrappers
16// ---------------------------------------------------------------------------
17
18fn strip_0x(s: &str) -> &str {
19    s.strip_prefix("0x").unwrap_or(s)
20}
21
22fn decode_hex_32(s: &str) -> Result<[u8; 32], Error> {
23    let raw = strip_0x(s);
24    if raw.len() > 64 {
25        return Err(Error::InvalidData(
26            "hex value too large: expected at most 32 bytes".to_owned(),
27        ));
28    }
29
30    let padded = if raw.len() % 2 == 1 {
31        format!("0{raw}")
32    } else {
33        raw.to_owned()
34    };
35
36    let decoded = hex::decode(padded).map_err(|e| Error::InvalidData(e.to_string()))?;
37    if decoded.len() > 32 {
38        return Err(Error::InvalidData(
39            "hex value too large: expected at most 32 bytes".to_owned(),
40        ));
41    }
42
43    let mut out = [0u8; 32];
44    out[32 - decoded.len()..].copy_from_slice(&decoded);
45    Ok(out)
46}
47
48fn encode_hex(bytes: &[u8]) -> String {
49    format!("0x{}", hex::encode(bytes))
50}
51
52/// A synced block header from the PXE.
53///
54/// The full block header has many fields; kept as opaque JSON until
55/// the complete schema is ported to `aztec-core`.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct BlockHeader {
58    /// The full block header data.
59    #[serde(flatten)]
60    pub data: serde_json::Value,
61}
62
63/// A transaction execution request submitted to the PXE.
64///
65/// Contains encoded function calls, auth witnesses, gas settings, etc.
66/// Kept as opaque JSON until the full schema is ported.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TxExecutionRequest {
69    /// The request data.
70    #[serde(flatten)]
71    pub data: serde_json::Value,
72}
73
74/// Result of proving a transaction via the PXE.
75///
76/// Mirrors the upstream stdlib proving result shape closely enough for the
77/// wallet to materialize the node-facing `Tx` envelope.
78#[derive(Debug, Clone, Serialize, Deserialize, Default)]
79#[serde(rename_all = "camelCase")]
80pub struct TxProvingResult {
81    /// Canonical tx hash computed from the tail public inputs.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub tx_hash: Option<TxHash>,
84    /// Structured private execution result.
85    #[serde(default)]
86    pub private_execution_result: serde_json::Value,
87    /// Serialized private-kernel tail public inputs.
88    pub public_inputs: PrivateKernelTailCircuitPublicInputs,
89    /// Serialized chonk proof.
90    pub chonk_proof: ChonkProof,
91    /// Contract-class log preimages.
92    #[serde(default)]
93    pub contract_class_log_fields: Vec<ContractClassLogFields>,
94    /// Calldata preimages for enqueued public calls.
95    #[serde(default)]
96    pub public_function_calldata: Vec<HashedValues>,
97    /// Optional proving or profiling stats.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub stats: Option<serde_json::Value>,
100}
101
102impl TxProvingResult {
103    /// Convert the proving result into the node-facing transaction payload.
104    ///
105    /// This matches upstream `TxProvingResult.toTx()` semantics:
106    /// - Contract class log fields are included as-is (already collected/sorted by the prover)
107    /// - Public function calldata preimages are included for hash verification
108    /// - The tx hash would be computed from the public inputs (done by the node)
109    pub fn to_tx(&self) -> Tx {
110        Tx {
111            data: self.public_inputs.clone(),
112            chonk_proof: self.chonk_proof.clone(),
113            contract_class_log_fields: self.contract_class_log_fields.clone(),
114            public_function_calldata: self.public_function_calldata.clone(),
115        }
116    }
117
118    /// Validate the internal consistency of the proving result.
119    ///
120    /// Checks the same invariants that the node's `DataTxValidator` would check:
121    /// - Calldata count matches number of public calls
122    /// - Contract class log count matches log hashes in public inputs
123    pub fn validate(&self) -> Result<(), Error> {
124        // Calldata count check
125        // (Full validation requires parsing public_inputs, which is a buffer;
126        // for now we just ensure arrays are non-empty only when expected)
127        // Validation will be expanded once typed public inputs parsing is available
128        let _ = self.public_function_calldata.len();
129        let _ = self.contract_class_log_fields.len();
130        Ok(())
131    }
132}
133
134/// Result of simulating a transaction via the PXE.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct TxSimulationResult {
137    /// The simulation result data.
138    #[serde(flatten)]
139    pub data: serde_json::Value,
140}
141
142/// Result of profiling a transaction via the PXE.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct TxProfileResult {
145    /// The profiling result data.
146    #[serde(flatten)]
147    pub data: serde_json::Value,
148}
149
150/// A 32-byte L2 block hash.
151#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
152pub struct BlockHash(pub [u8; 32]);
153
154impl BlockHash {
155    /// Parse a block hash from a hex string.
156    pub fn from_hex(value: &str) -> Result<Self, Error> {
157        Ok(Self(decode_hex_32(value)?))
158    }
159}
160
161impl fmt::Display for BlockHash {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.write_str(&encode_hex(&self.0))
164    }
165}
166
167impl fmt::Debug for BlockHash {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "BlockHash({self})")
170    }
171}
172
173impl Serialize for BlockHash {
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: Serializer,
177    {
178        serializer.serialize_str(&self.to_string())
179    }
180}
181
182impl<'de> Deserialize<'de> for BlockHash {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: Deserializer<'de>,
186    {
187        let s = String::deserialize(deserializer)?;
188        Self::from_hex(&s).map_err(serde::de::Error::custom)
189    }
190}
191
192/// A globally unique log identifier used for event pagination.
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct LogId {
196    /// The L2 block number containing the log.
197    pub block_number: u64,
198    /// The L2 block hash containing the log.
199    pub block_hash: BlockHash,
200    /// The transaction hash that emitted the log.
201    pub tx_hash: TxHash,
202    /// The transaction index within the block.
203    pub tx_index: u64,
204    /// The log index within the transaction.
205    pub log_index: u64,
206}
207
208/// Profiling modes supported by the PXE.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210pub enum ProfileMode {
211    /// Full profiling.
212    #[serde(rename = "full")]
213    Full,
214    /// Execution-steps profiling.
215    #[serde(rename = "execution-steps")]
216    ExecutionSteps,
217    /// Gates profiling.
218    #[serde(rename = "gates")]
219    Gates,
220}
221
222// ---------------------------------------------------------------------------
223// Supporting types — concrete structs
224// ---------------------------------------------------------------------------
225
226/// Result of executing a utility (view) function via the PXE.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct UtilityExecutionResult {
230    /// Raw field return values from the utility function.
231    pub result: Vec<Fr>,
232    /// Optional simulation stats payload.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub stats: Option<serde_json::Value>,
235}
236
237/// Options for PXE transaction simulation.
238#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct SimulateTxOpts {
241    /// Whether to simulate the public phase as well.
242    #[serde(default)]
243    pub simulate_public: bool,
244    /// Whether to skip transaction validation.
245    #[serde(default)]
246    pub skip_tx_validation: bool,
247    /// Whether to skip fee enforcement during simulation.
248    #[serde(default)]
249    pub skip_fee_enforcement: bool,
250    /// Simulation-time state overrides.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub overrides: Option<serde_json::Value>,
253    /// Note-discovery scopes.
254    #[serde(default)]
255    pub scopes: Vec<AztecAddress>,
256}
257
258/// Options for PXE transaction profiling.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct ProfileTxOpts {
262    /// The profiling mode to use.
263    pub profile_mode: ProfileMode,
264    /// Whether to skip proof generation during profiling.
265    #[serde(default = "default_skip_proof_generation")]
266    pub skip_proof_generation: bool,
267    /// Note-discovery scopes.
268    #[serde(default)]
269    pub scopes: Vec<AztecAddress>,
270}
271
272impl Default for ProfileTxOpts {
273    fn default() -> Self {
274        Self {
275            profile_mode: ProfileMode::Full,
276            skip_proof_generation: default_skip_proof_generation(),
277            scopes: vec![],
278        }
279    }
280}
281
282/// Options for executing a utility function via the PXE.
283#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct ExecuteUtilityOpts {
286    /// Authentication witnesses required for the call.
287    #[serde(default)]
288    pub authwits: Vec<AuthWitness>,
289    /// Note-discovery scopes.
290    #[serde(default)]
291    pub scopes: Vec<AztecAddress>,
292}
293
294/// A packed private event retrieved from the PXE.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct PackedPrivateEvent {
298    /// The packed event data as field elements.
299    pub packed_event: Vec<Fr>,
300    /// Hash of the transaction that emitted the event.
301    pub tx_hash: TxHash,
302    /// L2 block number containing the event.
303    pub l2_block_number: u64,
304    /// L2 block hash containing the event.
305    pub l2_block_hash: BlockHash,
306    /// Selector identifying the event type.
307    pub event_selector: EventSelector,
308}
309
310/// Filter for querying private events from the PXE.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct PrivateEventFilter {
314    /// Contract to filter events from.
315    pub contract_address: AztecAddress,
316    /// Filter by transaction hash.
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub tx_hash: Option<TxHash>,
319    /// Start block (inclusive).
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    pub from_block: Option<u64>,
322    /// End block (inclusive).
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub to_block: Option<u64>,
325    /// Cursor for pagination.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub after_log: Option<LogId>,
328    /// Note-discovery scopes.
329    #[serde(default)]
330    pub scopes: Vec<AztecAddress>,
331}
332
333impl Default for PrivateEventFilter {
334    fn default() -> Self {
335        Self {
336            contract_address: AztecAddress(Fr::zero()),
337            tx_hash: None,
338            from_block: None,
339            to_block: None,
340            after_log: None,
341            scopes: vec![],
342        }
343    }
344}
345
346/// Request to register a contract with the PXE.
347#[derive(Debug, Clone, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct RegisterContractRequest {
350    /// The contract instance to register.
351    pub instance: ContractInstanceWithAddress,
352    /// Optional contract artifact (can be omitted if class is already registered).
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub artifact: Option<ContractArtifact>,
355}
356
357const fn default_skip_proof_generation() -> bool {
358    true
359}
360
361// ---------------------------------------------------------------------------
362// Pxe trait
363// ---------------------------------------------------------------------------
364
365/// Interface for the Aztec Private eXecution Environment (PXE).
366///
367/// The PXE handles private state, transaction simulation, proving,
368/// and account/contract registration. This trait abstracts over
369/// different PXE backends (HTTP client, in-process, mock).
370#[async_trait]
371pub trait Pxe: Send + Sync {
372    /// Get the synced block header.
373    async fn get_synced_block_header(&self) -> Result<BlockHeader, Error>;
374
375    /// Get a contract instance by its address.
376    async fn get_contract_instance(
377        &self,
378        address: &AztecAddress,
379    ) -> Result<Option<ContractInstanceWithAddress>, Error>;
380
381    /// Get a contract artifact by its class ID.
382    async fn get_contract_artifact(&self, id: &Fr) -> Result<Option<ContractArtifact>, Error>;
383
384    /// Get all registered contract addresses.
385    async fn get_contracts(&self) -> Result<Vec<AztecAddress>, Error>;
386
387    /// Register an account with the PXE.
388    async fn register_account(
389        &self,
390        secret_key: &Fr,
391        partial_address: &Fr,
392    ) -> Result<CompleteAddress, Error>;
393
394    /// Get all registered account addresses.
395    async fn get_registered_accounts(&self) -> Result<Vec<CompleteAddress>, Error>;
396
397    /// Register a sender address for private log syncing.
398    async fn register_sender(&self, sender: &AztecAddress) -> Result<AztecAddress, Error>;
399
400    /// Get all registered sender addresses.
401    async fn get_senders(&self) -> Result<Vec<AztecAddress>, Error>;
402
403    /// Remove a registered sender.
404    async fn remove_sender(&self, sender: &AztecAddress) -> Result<(), Error>;
405
406    /// Register a contract class with the PXE (artifact only, no instance).
407    async fn register_contract_class(&self, artifact: &ContractArtifact) -> Result<(), Error>;
408
409    /// Register a contract instance (and optionally its artifact).
410    async fn register_contract(&self, request: RegisterContractRequest) -> Result<(), Error>;
411
412    /// Update a contract's artifact.
413    async fn update_contract(
414        &self,
415        address: &AztecAddress,
416        artifact: &ContractArtifact,
417    ) -> Result<(), Error>;
418
419    /// Simulate a transaction without sending it.
420    async fn simulate_tx(
421        &self,
422        tx_request: &TxExecutionRequest,
423        opts: SimulateTxOpts,
424    ) -> Result<TxSimulationResult, Error>;
425
426    /// Prove a transaction, producing a proven result ready for submission.
427    async fn prove_tx(
428        &self,
429        tx_request: &TxExecutionRequest,
430        scopes: Vec<AztecAddress>,
431    ) -> Result<TxProvingResult, Error>;
432
433    /// Profile a transaction for gas estimation and performance data.
434    async fn profile_tx(
435        &self,
436        tx_request: &TxExecutionRequest,
437        opts: ProfileTxOpts,
438    ) -> Result<TxProfileResult, Error>;
439
440    /// Execute a utility (view/unconstrained) function.
441    async fn execute_utility(
442        &self,
443        call: &FunctionCall,
444        opts: ExecuteUtilityOpts,
445    ) -> Result<UtilityExecutionResult, Error>;
446
447    /// Get private events matching a selector and filter.
448    async fn get_private_events(
449        &self,
450        event_selector: &EventSelector,
451        filter: PrivateEventFilter,
452    ) -> Result<Vec<PackedPrivateEvent>, Error>;
453
454    /// Stop the PXE instance.
455    async fn stop(&self) -> Result<(), Error>;
456}
457
458// ---------------------------------------------------------------------------
459// Tests
460// ---------------------------------------------------------------------------
461
462#[cfg(test)]
463#[allow(clippy::unwrap_used, clippy::expect_used)]
464mod tests {
465    use super::*;
466    use aztec_core::abi::{FunctionSelector, FunctionType};
467    use aztec_core::types::{ContractInstance, PublicKeys};
468    use std::sync::atomic::{AtomicUsize, Ordering};
469    use std::sync::Mutex;
470
471    // -- Serde roundtrip tests --
472
473    #[test]
474    fn block_header_roundtrip() {
475        let json_str = r#"{"globalVariables":{"blockNumber":42},"contentCommitment":"0x01"}"#;
476        let header: BlockHeader = serde_json::from_str(json_str).unwrap();
477        let reserialized = serde_json::to_string(&header).unwrap();
478        let decoded: BlockHeader = serde_json::from_str(&reserialized).unwrap();
479        assert_eq!(decoded.data["globalVariables"]["blockNumber"], 42);
480    }
481
482    #[test]
483    fn tx_execution_request_roundtrip() {
484        let json_str = r#"{"origin":"0x0000000000000000000000000000000000000000000000000000000000000001","functionSelector":"0xaabbccdd"}"#;
485        let req: TxExecutionRequest = serde_json::from_str(json_str).unwrap();
486        let reserialized = serde_json::to_string(&req).unwrap();
487        let decoded: TxExecutionRequest = serde_json::from_str(&reserialized).unwrap();
488        assert_eq!(decoded.data["functionSelector"], "0xaabbccdd");
489    }
490
491    #[test]
492    fn tx_proving_result_roundtrip() {
493        let json_str = r#"{
494            "privateExecutionResult": {"entrypoint": "ok"},
495            "publicInputs": "AQID",
496            "chonkProof": "BAUG",
497            "contractClassLogFields": [{"fields": ["0x01"]}],
498            "publicFunctionCalldata": [{"values": ["0x02"], "hash": "0x03"}]
499        }"#;
500        let result: TxProvingResult = serde_json::from_str(json_str).unwrap();
501        let reserialized = serde_json::to_string(&result).unwrap();
502        let decoded: TxProvingResult = serde_json::from_str(&reserialized).unwrap();
503        assert_eq!(decoded.private_execution_result["entrypoint"], "ok");
504        assert_eq!(decoded.public_inputs.bytes, vec![1, 2, 3]);
505        assert_eq!(decoded.chonk_proof.bytes, vec![4, 5, 6]);
506        assert_eq!(decoded.contract_class_log_fields.len(), 1);
507        assert_eq!(decoded.public_function_calldata.len(), 1);
508    }
509
510    #[test]
511    fn tx_simulation_result_roundtrip() {
512        let json_str = r#"{"returnValues":[42],"gasUsed":{"daGas":100,"l2Gas":200}}"#;
513        let result: TxSimulationResult = serde_json::from_str(json_str).unwrap();
514        let reserialized = serde_json::to_string(&result).unwrap();
515        let decoded: TxSimulationResult = serde_json::from_str(&reserialized).unwrap();
516        assert_eq!(decoded.data["gasUsed"]["l2Gas"], 200);
517    }
518
519    #[test]
520    fn tx_profile_result_roundtrip() {
521        let json_str = r#"{"gateCounts":[10,20],"executionSteps":5}"#;
522        let result: TxProfileResult = serde_json::from_str(json_str).unwrap();
523        let reserialized = serde_json::to_string(&result).unwrap();
524        let decoded: TxProfileResult = serde_json::from_str(&reserialized).unwrap();
525        assert_eq!(decoded.data["executionSteps"], 5);
526    }
527
528    #[test]
529    fn block_hash_roundtrip() {
530        let hash = BlockHash([0x11; 32]);
531        let json = serde_json::to_string(&hash).unwrap();
532        let decoded: BlockHash = serde_json::from_str(&json).unwrap();
533        assert_eq!(decoded, hash);
534    }
535
536    #[test]
537    fn log_id_roundtrip() {
538        let log_id = sample_log_id();
539        let json = serde_json::to_string(&log_id).unwrap();
540        let decoded: LogId = serde_json::from_str(&json).unwrap();
541        assert_eq!(decoded, log_id);
542    }
543
544    #[test]
545    fn utility_execution_result_roundtrip() {
546        let result = UtilityExecutionResult {
547            result: vec![Fr::from(1u64), Fr::from(2u64)],
548            stats: Some(serde_json::json!({"timings": {"total": 1}})),
549        };
550        let json = serde_json::to_string(&result).unwrap();
551        let decoded: UtilityExecutionResult = serde_json::from_str(&json).unwrap();
552        assert_eq!(decoded.result, result.result);
553        assert_eq!(decoded.stats, result.stats);
554    }
555
556    #[test]
557    fn simulate_tx_opts_defaults() {
558        let opts = SimulateTxOpts::default();
559        assert!(!opts.simulate_public);
560        assert!(!opts.skip_tx_validation);
561        assert!(!opts.skip_fee_enforcement);
562        assert!(opts.overrides.is_none());
563        assert!(opts.scopes.is_empty());
564
565        let json = serde_json::to_value(&opts).unwrap();
566        assert_eq!(json["simulatePublic"], false);
567        assert_eq!(json["skipTxValidation"], false);
568        assert_eq!(json["skipFeeEnforcement"], false);
569    }
570
571    #[test]
572    fn profile_tx_opts_defaults() {
573        let opts = ProfileTxOpts::default();
574        assert_eq!(opts.profile_mode, ProfileMode::Full);
575        assert!(opts.skip_proof_generation);
576        assert!(opts.scopes.is_empty());
577    }
578
579    #[test]
580    fn execute_utility_opts_defaults() {
581        let opts = ExecuteUtilityOpts::default();
582        assert!(opts.authwits.is_empty());
583        assert!(opts.scopes.is_empty());
584    }
585
586    #[test]
587    fn packed_private_event_roundtrip() {
588        let event = PackedPrivateEvent {
589            packed_event: vec![Fr::from(1u64), Fr::from(2u64)],
590            tx_hash: TxHash::zero(),
591            l2_block_number: 42,
592            l2_block_hash: sample_block_hash(),
593            event_selector: EventSelector(Fr::from(7u64)),
594        };
595        let json = serde_json::to_string(&event).unwrap();
596        let decoded: PackedPrivateEvent = serde_json::from_str(&json).unwrap();
597        assert_eq!(decoded.packed_event.len(), 2);
598        assert_eq!(decoded.l2_block_number, 42);
599        assert_eq!(decoded.l2_block_hash, sample_block_hash());
600    }
601
602    #[test]
603    fn packed_private_event_minimal() {
604        let event = PackedPrivateEvent {
605            packed_event: vec![],
606            tx_hash: TxHash::zero(),
607            l2_block_number: 0,
608            l2_block_hash: BlockHash::default(),
609            event_selector: EventSelector(Fr::zero()),
610        };
611        let json = serde_json::to_value(&event).unwrap();
612        assert_eq!(json["txHash"], TxHash::zero().to_string());
613        assert_eq!(json["l2BlockNumber"], 0);
614    }
615
616    #[test]
617    fn private_event_filter_default_serializes_minimal() {
618        let filter = PrivateEventFilter::default();
619        let json = serde_json::to_value(&filter).unwrap();
620        assert_eq!(
621            json["contractAddress"],
622            AztecAddress(Fr::zero()).to_string()
623        );
624        assert!(json.get("txHash").is_none());
625        assert!(json.get("fromBlock").is_none());
626        assert!(json.get("toBlock").is_none());
627        assert!(json.get("afterLog").is_none());
628        assert_eq!(json["scopes"], serde_json::json!([]));
629    }
630
631    #[test]
632    fn private_event_filter_with_fields() {
633        let filter = PrivateEventFilter {
634            contract_address: AztecAddress(Fr::from(9u64)),
635            tx_hash: Some(TxHash::zero()),
636            from_block: Some(10),
637            to_block: Some(20),
638            after_log: Some(sample_log_id()),
639            scopes: vec![AztecAddress(Fr::from(1u64))],
640        };
641        let json = serde_json::to_value(&filter).unwrap();
642        assert_eq!(
643            json["contractAddress"],
644            AztecAddress(Fr::from(9u64)).to_string()
645        );
646        assert_eq!(json["txHash"], TxHash::zero().to_string());
647        assert_eq!(json["fromBlock"], 10);
648        assert_eq!(json["toBlock"], 20);
649        assert!(json.get("afterLog").is_some());
650        assert_eq!(json["scopes"].as_array().unwrap().len(), 1);
651    }
652
653    #[test]
654    fn register_contract_request_roundtrip() {
655        let request = RegisterContractRequest {
656            instance: sample_instance(),
657            artifact: None,
658        };
659        let json = serde_json::to_string(&request).unwrap();
660        let decoded: RegisterContractRequest = serde_json::from_str(&json).unwrap();
661        assert_eq!(decoded.instance.address, request.instance.address);
662        assert!(decoded.artifact.is_none());
663    }
664
665    #[test]
666    fn register_contract_request_with_artifact() {
667        let request = RegisterContractRequest {
668            instance: sample_instance(),
669            artifact: Some(ContractArtifact {
670                name: "TestContract".into(),
671                functions: vec![],
672                outputs: None,
673                file_map: None,
674                context_inputs_sizes: None,
675            }),
676        };
677        let json = serde_json::to_string(&request).unwrap();
678        let decoded: RegisterContractRequest = serde_json::from_str(&json).unwrap();
679        assert_eq!(decoded.artifact.unwrap().name, "TestContract");
680    }
681
682    // -- Trait object safety --
683
684    #[test]
685    fn pxe_is_object_safe() {
686        fn _assert_object_safe(_: &dyn Pxe) {}
687    }
688
689    // -- Test helpers --
690
691    fn sample_instance() -> ContractInstanceWithAddress {
692        ContractInstanceWithAddress {
693            address: AztecAddress(Fr::from(1u64)),
694            inner: ContractInstance {
695                version: 1,
696                salt: Fr::from(42u64),
697                deployer: AztecAddress(Fr::from(2u64)),
698                current_contract_class_id: Fr::from(100u64),
699                original_contract_class_id: Fr::from(100u64),
700                initialization_hash: Fr::from(0u64),
701                public_keys: PublicKeys::default(),
702            },
703        }
704    }
705
706    fn sample_block_header() -> BlockHeader {
707        BlockHeader {
708            data: serde_json::json!({"globalVariables": {"blockNumber": 1}}),
709        }
710    }
711
712    fn sample_block_hash() -> BlockHash {
713        BlockHash([0x22; 32])
714    }
715
716    fn sample_log_id() -> LogId {
717        LogId {
718            block_number: 42,
719            block_hash: sample_block_hash(),
720            tx_hash: TxHash::zero(),
721            tx_index: 3,
722            log_index: 7,
723        }
724    }
725
726    // -- MockPxe --
727
728    struct MockPxe {
729        header_results: Mutex<Vec<Result<BlockHeader, Error>>>,
730        accounts: Vec<CompleteAddress>,
731        senders: Mutex<Vec<AztecAddress>>,
732        contracts: Vec<AztecAddress>,
733        call_count: AtomicUsize,
734    }
735
736    impl MockPxe {
737        fn new_ready() -> Self {
738            Self {
739                header_results: Mutex::new(vec![Ok(sample_block_header())]),
740                accounts: vec![],
741                senders: Mutex::new(vec![]),
742                contracts: vec![],
743                call_count: AtomicUsize::new(0),
744            }
745        }
746
747        fn with_accounts(mut self, accounts: Vec<CompleteAddress>) -> Self {
748            self.accounts = accounts;
749            self
750        }
751
752        fn with_contracts(mut self, contracts: Vec<AztecAddress>) -> Self {
753            self.contracts = contracts;
754            self
755        }
756    }
757
758    #[async_trait]
759    impl Pxe for MockPxe {
760        async fn get_synced_block_header(&self) -> Result<BlockHeader, Error> {
761            let idx = self.call_count.fetch_add(1, Ordering::Relaxed);
762            let results = self.header_results.lock().unwrap();
763            if idx < results.len() {
764                match &results[idx] {
765                    Ok(h) => Ok(h.clone()),
766                    Err(e) => Err(Error::Transport(e.to_string())),
767                }
768            } else if let Some(last) = results.last() {
769                match last {
770                    Ok(h) => Ok(h.clone()),
771                    Err(e) => Err(Error::Transport(e.to_string())),
772                }
773            } else {
774                Err(Error::Transport("no mock results configured".into()))
775            }
776        }
777
778        async fn get_contract_instance(
779            &self,
780            address: &AztecAddress,
781        ) -> Result<Option<ContractInstanceWithAddress>, Error> {
782            if *address == AztecAddress(Fr::from(1u64)) {
783                Ok(Some(sample_instance()))
784            } else {
785                Ok(None)
786            }
787        }
788
789        async fn get_contract_artifact(&self, _id: &Fr) -> Result<Option<ContractArtifact>, Error> {
790            Ok(None)
791        }
792
793        async fn get_contracts(&self) -> Result<Vec<AztecAddress>, Error> {
794            Ok(self.contracts.clone())
795        }
796
797        async fn register_account(
798            &self,
799            _secret_key: &Fr,
800            _partial_address: &Fr,
801        ) -> Result<CompleteAddress, Error> {
802            Ok(CompleteAddress::default())
803        }
804
805        async fn get_registered_accounts(&self) -> Result<Vec<CompleteAddress>, Error> {
806            Ok(self.accounts.clone())
807        }
808
809        async fn register_sender(&self, sender: &AztecAddress) -> Result<AztecAddress, Error> {
810            self.senders.lock().unwrap().push(*sender);
811            Ok(*sender)
812        }
813
814        async fn get_senders(&self) -> Result<Vec<AztecAddress>, Error> {
815            Ok(self.senders.lock().unwrap().clone())
816        }
817
818        async fn remove_sender(&self, sender: &AztecAddress) -> Result<(), Error> {
819            self.senders.lock().unwrap().retain(|s| s != sender);
820            Ok(())
821        }
822
823        async fn register_contract_class(&self, _artifact: &ContractArtifact) -> Result<(), Error> {
824            Ok(())
825        }
826
827        async fn register_contract(&self, _request: RegisterContractRequest) -> Result<(), Error> {
828            Ok(())
829        }
830
831        async fn update_contract(
832            &self,
833            _address: &AztecAddress,
834            _artifact: &ContractArtifact,
835        ) -> Result<(), Error> {
836            Ok(())
837        }
838
839        async fn simulate_tx(
840            &self,
841            _tx_request: &TxExecutionRequest,
842            _opts: SimulateTxOpts,
843        ) -> Result<TxSimulationResult, Error> {
844            Ok(TxSimulationResult {
845                data: serde_json::json!({"returnValues": []}),
846            })
847        }
848
849        async fn prove_tx(
850            &self,
851            _tx_request: &TxExecutionRequest,
852            _scopes: Vec<AztecAddress>,
853        ) -> Result<TxProvingResult, Error> {
854            Ok(TxProvingResult {
855                tx_hash: None,
856                private_execution_result: serde_json::json!({}),
857                public_inputs: PrivateKernelTailCircuitPublicInputs::from_bytes(vec![0]),
858                chonk_proof: ChonkProof::from_bytes(vec![0]),
859                contract_class_log_fields: vec![],
860                public_function_calldata: vec![],
861                stats: None,
862            })
863        }
864
865        async fn profile_tx(
866            &self,
867            _tx_request: &TxExecutionRequest,
868            _opts: ProfileTxOpts,
869        ) -> Result<TxProfileResult, Error> {
870            Ok(TxProfileResult {
871                data: serde_json::json!({"gateCounts": []}),
872            })
873        }
874
875        async fn execute_utility(
876            &self,
877            _call: &FunctionCall,
878            _opts: ExecuteUtilityOpts,
879        ) -> Result<UtilityExecutionResult, Error> {
880            Ok(UtilityExecutionResult {
881                result: vec![],
882                stats: None,
883            })
884        }
885
886        async fn get_private_events(
887            &self,
888            _event_selector: &EventSelector,
889            _filter: PrivateEventFilter,
890        ) -> Result<Vec<PackedPrivateEvent>, Error> {
891            Ok(vec![])
892        }
893
894        async fn stop(&self) -> Result<(), Error> {
895            Ok(())
896        }
897    }
898
899    // -- Mock-based async tests --
900
901    #[tokio::test]
902    async fn mock_get_synced_block_header() {
903        let pxe = MockPxe::new_ready();
904        let header = pxe.get_synced_block_header().await.unwrap();
905        assert_eq!(header.data["globalVariables"]["blockNumber"], 1);
906    }
907
908    #[tokio::test]
909    async fn mock_register_account() {
910        let pxe = MockPxe::new_ready();
911        let result = pxe
912            .register_account(&Fr::from(1u64), &Fr::from(2u64))
913            .await
914            .unwrap();
915        assert_eq!(result, CompleteAddress::default());
916    }
917
918    #[tokio::test]
919    async fn mock_get_registered_accounts() {
920        let account = CompleteAddress {
921            address: AztecAddress(Fr::from(99u64)),
922            ..CompleteAddress::default()
923        };
924        let pxe = MockPxe::new_ready().with_accounts(vec![account.clone()]);
925        let accounts = pxe.get_registered_accounts().await.unwrap();
926        assert_eq!(accounts.len(), 1);
927        assert_eq!(accounts[0].address, AztecAddress(Fr::from(99u64)));
928    }
929
930    #[tokio::test]
931    async fn mock_register_and_get_senders() {
932        let pxe = MockPxe::new_ready();
933        let addr = AztecAddress(Fr::from(42u64));
934
935        let result = pxe.register_sender(&addr).await.unwrap();
936        assert_eq!(result, addr);
937
938        let senders = pxe.get_senders().await.unwrap();
939        assert_eq!(senders.len(), 1);
940        assert_eq!(senders[0], addr);
941    }
942
943    #[tokio::test]
944    async fn mock_remove_sender() {
945        let pxe = MockPxe::new_ready();
946        let addr = AztecAddress(Fr::from(42u64));
947
948        pxe.register_sender(&addr).await.unwrap();
949        assert_eq!(pxe.get_senders().await.unwrap().len(), 1);
950
951        pxe.remove_sender(&addr).await.unwrap();
952        assert!(pxe.get_senders().await.unwrap().is_empty());
953    }
954
955    #[tokio::test]
956    async fn mock_get_contract_instance_found() {
957        let pxe = MockPxe::new_ready();
958        let result = pxe
959            .get_contract_instance(&AztecAddress(Fr::from(1u64)))
960            .await
961            .unwrap();
962        assert!(result.is_some());
963        assert_eq!(result.unwrap().address, AztecAddress(Fr::from(1u64)));
964    }
965
966    #[tokio::test]
967    async fn mock_get_contract_instance_not_found() {
968        let pxe = MockPxe::new_ready();
969        let result = pxe
970            .get_contract_instance(&AztecAddress(Fr::from(999u64)))
971            .await
972            .unwrap();
973        assert!(result.is_none());
974    }
975
976    #[tokio::test]
977    async fn mock_get_contracts() {
978        let pxe = MockPxe::new_ready().with_contracts(vec![
979            AztecAddress(Fr::from(1u64)),
980            AztecAddress(Fr::from(2u64)),
981        ]);
982        let contracts = pxe.get_contracts().await.unwrap();
983        assert_eq!(contracts.len(), 2);
984    }
985
986    #[tokio::test]
987    async fn mock_simulate_tx() {
988        let pxe = MockPxe::new_ready();
989        let req = TxExecutionRequest {
990            data: serde_json::json!({}),
991        };
992        let result = pxe
993            .simulate_tx(&req, SimulateTxOpts::default())
994            .await
995            .unwrap();
996        assert_eq!(result.data["returnValues"], serde_json::json!([]));
997    }
998
999    #[tokio::test]
1000    async fn mock_execute_utility() {
1001        let pxe = MockPxe::new_ready();
1002        let call = FunctionCall {
1003            to: AztecAddress(Fr::from(1u64)),
1004            selector: FunctionSelector::from_hex("0xaabbccdd").unwrap(),
1005            args: vec![],
1006            function_type: FunctionType::Utility,
1007            is_static: true,
1008            hide_msg_sender: false,
1009        };
1010        let result = pxe
1011            .execute_utility(&call, ExecuteUtilityOpts::default())
1012            .await
1013            .unwrap();
1014        assert!(result.result.is_empty());
1015        assert!(result.stats.is_none());
1016    }
1017
1018    #[tokio::test]
1019    async fn mock_get_private_events_empty() {
1020        let pxe = MockPxe::new_ready();
1021        let events = pxe
1022            .get_private_events(
1023                &EventSelector(Fr::from(1u64)),
1024                PrivateEventFilter {
1025                    contract_address: AztecAddress(Fr::from(1u64)),
1026                    scopes: vec![AztecAddress(Fr::from(2u64))],
1027                    ..PrivateEventFilter::default()
1028                },
1029            )
1030            .await
1031            .unwrap();
1032        assert!(events.is_empty());
1033    }
1034
1035    #[tokio::test]
1036    async fn mock_register_contract() {
1037        let pxe = MockPxe::new_ready();
1038        let request = RegisterContractRequest {
1039            instance: sample_instance(),
1040            artifact: None,
1041        };
1042        pxe.register_contract(request).await.unwrap();
1043    }
1044
1045    #[tokio::test]
1046    async fn mock_stop() {
1047        let pxe = MockPxe::new_ready();
1048        pxe.stop().await.unwrap();
1049    }
1050}