aztec_node_client/
node.rs

1use std::time::Duration;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::abi::EventSelector;
7use crate::error::Error;
8use crate::rpc::RpcTransport;
9use crate::tx::{TxHash, TxReceipt, TxStatus};
10use crate::types::{AztecAddress, ContractInstanceWithAddress, Fr};
11
12fn block_param_json(block_number: u64) -> serde_json::Value {
13    if block_number == 0 {
14        serde_json::Value::String("latest".to_owned())
15    } else {
16        serde_json::json!(block_number)
17    }
18}
19
20fn block_hash_param_json(block_hash: &Fr) -> serde_json::Value {
21    serde_json::Value::String(block_hash.to_string())
22}
23
24// ---------------------------------------------------------------------------
25// Supporting types
26// ---------------------------------------------------------------------------
27
28/// Information returned by the Aztec node about its current state.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct NodeInfo {
32    /// Semantic version of the node software.
33    pub node_version: String,
34    /// L1 chain ID the node is connected to.
35    pub l1_chain_id: u64,
36    /// Rollup protocol version.
37    pub rollup_version: u64,
38    /// Ethereum Node Record, if available.
39    #[serde(default)]
40    pub enr: Option<String>,
41    /// L1 contract addresses — kept as opaque JSON until the full schema stabilizes.
42    #[serde(default)]
43    pub l1_contract_addresses: serde_json::Value,
44    /// Protocol contract addresses — kept as opaque JSON until the full schema stabilizes.
45    #[serde(default)]
46    pub protocol_contract_addresses: serde_json::Value,
47    /// Whether the node is running with real (non-mock) proofs.
48    pub real_proofs: bool,
49    /// L2 circuits verification key tree root.
50    #[serde(default)]
51    pub l2_circuits_vk_tree_root: Option<String>,
52    /// L2 protocol contracts hash.
53    #[serde(default)]
54    pub l2_protocol_contracts_hash: Option<String>,
55}
56
57/// Identifies a specific log entry within a block.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct LogId {
61    /// Block number containing the log.
62    pub block_number: u64,
63    /// Index of the log within the block.
64    pub log_index: u64,
65}
66
67/// Filter for querying public logs from the node.
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct PublicLogFilter {
71    /// Filter by transaction hash.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub tx_hash: Option<TxHash>,
74    /// Start block (inclusive).
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub from_block: Option<u64>,
77    /// End block (inclusive).
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub to_block: Option<u64>,
80    /// Filter by emitting contract address.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub contract_address: Option<AztecAddress>,
83    /// Filter by event selector.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub selector: Option<EventSelector>,
86    /// Cursor for pagination — fetch logs after this entry.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub after_log: Option<LogId>,
89}
90
91/// Identifier for a public log entry, as returned by the node.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct PublicLogId {
95    /// Block number containing the log.
96    pub block_number: u64,
97    /// Hash of the block containing the log.
98    #[serde(default)]
99    pub block_hash: Option<String>,
100    /// Hash of the transaction that emitted the log.
101    #[serde(default)]
102    pub tx_hash: Option<TxHash>,
103    /// Index of the transaction within the block.
104    #[serde(default)]
105    pub tx_index: Option<u64>,
106    /// Index of the log within the block.
107    pub log_index: u64,
108}
109
110/// The log body as returned by the node.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct PublicLogBody {
114    /// Address of the contract that emitted the log.
115    pub contract_address: AztecAddress,
116    /// Field element data comprising the log payload.
117    pub fields: Vec<Fr>,
118}
119
120/// A single public log entry returned by the node.
121///
122/// The node returns `{ id: { blockNumber, txHash, logIndex, ... }, log: { contractAddress, fields } }`.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct PublicLogEntry {
126    /// Log identifier with block/tx/index metadata.
127    pub id: PublicLogId,
128    /// Log body with contract address and field data.
129    pub log: PublicLogBody,
130}
131
132/// A flattened view of a public log for downstream consumers.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct PublicLog {
136    /// Address of the contract that emitted the log.
137    pub contract_address: AztecAddress,
138    /// Field element data comprising the log payload.
139    pub data: Vec<Fr>,
140    /// Hash of the transaction that emitted the log.
141    #[serde(default)]
142    pub tx_hash: Option<TxHash>,
143    /// Block number containing the log.
144    pub block_number: u64,
145    /// Index of the log within the block.
146    pub log_index: u64,
147}
148
149impl From<PublicLogEntry> for PublicLog {
150    fn from(entry: PublicLogEntry) -> Self {
151        Self {
152            contract_address: entry.log.contract_address,
153            data: entry.log.fields,
154            tx_hash: entry.id.tx_hash,
155            block_number: entry.id.block_number,
156            log_index: entry.id.log_index,
157        }
158    }
159}
160
161/// Raw response from the node's `getPublicLogs` RPC.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164struct RawPublicLogsResponse {
165    logs: Vec<PublicLogEntry>,
166    max_logs_hit: bool,
167}
168
169/// Response from a public logs query.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct PublicLogsResponse {
173    /// Matching log entries.
174    pub logs: Vec<PublicLog>,
175    /// Whether the response was truncated due to the log limit.
176    pub max_logs_hit: bool,
177}
178
179/// Result of validating a transaction against current node state.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(tag = "result", rename_all = "lowercase")]
182pub enum TxValidationResult {
183    /// Transaction is valid for current inclusion rules.
184    Valid,
185    /// Transaction is invalid with one or more validator reasons.
186    Invalid { reason: Vec<String> },
187    /// Transaction validation was skipped with explanation.
188    Skipped { reason: Vec<String> },
189}
190
191/// Options controlling `wait_for_tx` polling behavior.
192#[derive(Debug, Clone)]
193pub struct WaitOpts {
194    /// Total timeout for the polling operation.
195    pub timeout: Duration,
196    /// Interval between retries.
197    pub interval: Duration,
198    /// Minimum status to wait for.
199    pub wait_for_status: TxStatus,
200    /// If true, accept a reverted tx receipt without returning an error.
201    pub dont_throw_on_revert: bool,
202    /// Duration to ignore `TxStatus::Dropped` before treating it as failure.
203    /// Avoids race conditions where a tx briefly appears dropped before inclusion.
204    pub ignore_dropped_receipts_for: Duration,
205}
206
207impl Default for WaitOpts {
208    fn default() -> Self {
209        Self {
210            timeout: Duration::from_secs(300),
211            interval: Duration::from_secs(1),
212            wait_for_status: TxStatus::Checkpointed,
213            dont_throw_on_revert: false,
214            ignore_dropped_receipts_for: Duration::from_secs(5),
215        }
216    }
217}
218
219// ---------------------------------------------------------------------------
220// Trait
221// ---------------------------------------------------------------------------
222
223/// Public read interface for an Aztec node.
224#[async_trait]
225pub trait AztecNode: Send + Sync {
226    /// Fetch information about the node's current state.
227    async fn get_node_info(&self) -> Result<NodeInfo, Error>;
228    /// Get the latest block number.
229    async fn get_block_number(&self) -> Result<u64, Error>;
230    /// Get the latest proven block number.
231    async fn get_proven_block_number(&self) -> Result<u64, Error>;
232    /// Get the receipt for a transaction.
233    async fn get_tx_receipt(&self, tx_hash: &TxHash) -> Result<TxReceipt, Error>;
234    /// Query public logs matching the given filter.
235    async fn get_public_logs(&self, filter: PublicLogFilter) -> Result<PublicLogsResponse, Error>;
236    /// Send a proven transaction to the network for inclusion.
237    async fn send_tx(&self, tx: &serde_json::Value) -> Result<(), Error>;
238    /// Get a publicly deployed contract instance by address.
239    async fn get_contract(
240        &self,
241        address: &AztecAddress,
242    ) -> Result<Option<ContractInstanceWithAddress>, Error>;
243    /// Get a publicly registered contract class by ID.
244    async fn get_contract_class(&self, id: &Fr) -> Result<Option<serde_json::Value>, Error>;
245
246    // --- Phase 1: methods required for EmbeddedPxe simulation ---
247
248    /// Get a block header by block number (0 = latest).
249    async fn get_block_header(&self, block_number: u64) -> Result<serde_json::Value, Error>;
250
251    /// Get a full block by block number.
252    async fn get_block(&self, block_number: u64) -> Result<Option<serde_json::Value>, Error>;
253
254    /// Get a note-hash membership witness for a leaf at a given block.
255    async fn get_note_hash_membership_witness(
256        &self,
257        block_number: u64,
258        note_hash: &Fr,
259    ) -> Result<Option<serde_json::Value>, Error>;
260
261    /// Get a nullifier membership witness at a given block.
262    async fn get_nullifier_membership_witness(
263        &self,
264        block_number: u64,
265        nullifier: &Fr,
266    ) -> Result<Option<serde_json::Value>, Error>;
267
268    /// Get the low nullifier membership witness (for non-membership proofs).
269    async fn get_low_nullifier_membership_witness(
270        &self,
271        block_number: u64,
272        nullifier: &Fr,
273    ) -> Result<Option<serde_json::Value>, Error>;
274
275    /// Read a public storage slot value at a given block.
276    async fn get_public_storage_at(
277        &self,
278        block_number: u64,
279        contract: &AztecAddress,
280        slot: &Fr,
281    ) -> Result<Fr, Error>;
282
283    /// Read a public storage slot value at a specific block hash.
284    async fn get_public_storage_at_by_hash(
285        &self,
286        block_hash: &Fr,
287        contract: &AztecAddress,
288        slot: &Fr,
289    ) -> Result<Fr, Error> {
290        let _ = block_hash;
291        self.get_public_storage_at(0, contract, slot).await
292    }
293
294    /// Get a merkle witness for a public data leaf.
295    async fn get_public_data_witness(
296        &self,
297        block_number: u64,
298        leaf_slot: &Fr,
299    ) -> Result<Option<serde_json::Value>, Error>;
300
301    /// Get a merkle witness for a public data leaf at a specific block hash.
302    async fn get_public_data_witness_by_hash(
303        &self,
304        block_hash: &Fr,
305        leaf_slot: &Fr,
306    ) -> Result<Option<serde_json::Value>, Error> {
307        let _ = block_hash;
308        self.get_public_data_witness(0, leaf_slot).await
309    }
310
311    /// Get a membership witness for an L1-to-L2 message.
312    async fn get_l1_to_l2_message_membership_witness(
313        &self,
314        block_number: u64,
315        entry_key: &Fr,
316    ) -> Result<Option<serde_json::Value>, Error>;
317
318    /// Get the checkpoint number at which an L1-to-L2 message becomes available.
319    ///
320    /// Returns `None` if the message is not yet known to the archiver.
321    async fn get_l1_to_l2_message_checkpoint(
322        &self,
323        l1_to_l2_message: &Fr,
324    ) -> Result<Option<u64>, Error> {
325        // Default: not implemented
326        let _ = l1_to_l2_message;
327        Ok(None)
328    }
329
330    /// Delegate public call simulation to the node.
331    async fn simulate_public_calls(
332        &self,
333        tx: &serde_json::Value,
334        skip_fee_enforcement: bool,
335    ) -> Result<serde_json::Value, Error>;
336
337    /// Validate a simulated transaction.
338    async fn is_valid_tx(&self, tx: &serde_json::Value) -> Result<TxValidationResult, Error>;
339
340    /// Get private logs by tags for note discovery.
341    async fn get_private_logs_by_tags(&self, tags: &[Fr]) -> Result<serde_json::Value, Error>;
342
343    /// Get public logs by tags from a specific contract.
344    async fn get_public_logs_by_tags_from_contract(
345        &self,
346        contract: &AztecAddress,
347        tags: &[Fr],
348    ) -> Result<serde_json::Value, Error>;
349
350    /// Register contract function signatures for debugging.
351    async fn register_contract_function_signatures(
352        &self,
353        signatures: &[String],
354    ) -> Result<(), Error>;
355
356    /// Get the effect of a transaction by its hash.
357    ///
358    /// Returns the full tx effect including nullifiers, block metadata,
359    /// and positional indexes needed for event validation.
360    async fn get_tx_effect(&self, tx_hash: &TxHash) -> Result<Option<serde_json::Value>, Error>;
361
362    // --- Phase 2: methods required for EmbeddedPxe proving ---
363
364    /// Get a block hash membership witness in the archive tree.
365    async fn get_block_hash_membership_witness(
366        &self,
367        block_number: u64,
368        block_hash: &Fr,
369    ) -> Result<Option<serde_json::Value>, Error>;
370
371    /// Find leaf indexes in a specified merkle tree.
372    async fn find_leaves_indexes(
373        &self,
374        block_number: u64,
375        tree_id: &str,
376        leaves: &[Fr],
377    ) -> Result<Vec<Option<u64>>, Error>;
378
379    /// Get a full transaction by its hash.
380    ///
381    /// Returns the wire-format Tx including the serialized kernel public inputs
382    /// buffer and chonk proof.
383    async fn get_tx_by_hash(&self, tx_hash: &TxHash) -> Result<Option<serde_json::Value>, Error>;
384}
385
386// ---------------------------------------------------------------------------
387// HTTP client
388// ---------------------------------------------------------------------------
389
390/// HTTP JSON-RPC backed Aztec node client.
391pub struct HttpNodeClient {
392    transport: RpcTransport,
393}
394
395impl HttpNodeClient {
396    fn new(url: String, timeout: Duration) -> Self {
397        Self {
398            transport: RpcTransport::new(url, timeout),
399        }
400    }
401}
402
403impl Clone for HttpNodeClient {
404    fn clone(&self) -> Self {
405        Self::new(self.transport.url().to_owned(), self.transport.timeout())
406    }
407}
408
409#[async_trait]
410impl AztecNode for HttpNodeClient {
411    async fn get_node_info(&self) -> Result<NodeInfo, Error> {
412        self.transport
413            .call("node_getNodeInfo", serde_json::json!([]))
414            .await
415    }
416
417    async fn get_block_number(&self) -> Result<u64, Error> {
418        self.transport
419            .call("node_getBlockNumber", serde_json::json!([]))
420            .await
421    }
422
423    async fn get_proven_block_number(&self) -> Result<u64, Error> {
424        self.transport
425            .call("node_getProvenBlockNumber", serde_json::json!([]))
426            .await
427    }
428
429    async fn get_tx_receipt(&self, tx_hash: &TxHash) -> Result<TxReceipt, Error> {
430        self.transport
431            .call("node_getTxReceipt", serde_json::json!([tx_hash]))
432            .await
433    }
434
435    async fn get_public_logs(&self, filter: PublicLogFilter) -> Result<PublicLogsResponse, Error> {
436        let raw: RawPublicLogsResponse = self
437            .transport
438            .call("node_getPublicLogs", serde_json::json!([filter]))
439            .await?;
440        Ok(PublicLogsResponse {
441            logs: raw.logs.into_iter().map(PublicLog::from).collect(),
442            max_logs_hit: raw.max_logs_hit,
443        })
444    }
445
446    async fn send_tx(&self, tx: &serde_json::Value) -> Result<(), Error> {
447        self.transport
448            .call_void("node_sendTx", serde_json::json!([tx]))
449            .await
450    }
451
452    async fn get_contract(
453        &self,
454        address: &AztecAddress,
455    ) -> Result<Option<ContractInstanceWithAddress>, Error> {
456        self.transport
457            .call_optional("node_getContract", serde_json::json!([address]))
458            .await
459    }
460
461    async fn get_contract_class(&self, id: &Fr) -> Result<Option<serde_json::Value>, Error> {
462        self.transport
463            .call_optional("node_getContractClass", serde_json::json!([id]))
464            .await
465    }
466
467    async fn get_block_header(&self, block_number: u64) -> Result<serde_json::Value, Error> {
468        self.transport
469            .call(
470                "node_getBlockHeader",
471                serde_json::json!([block_param_json(block_number)]),
472            )
473            .await
474    }
475
476    async fn get_block(&self, block_number: u64) -> Result<Option<serde_json::Value>, Error> {
477        self.transport
478            .call_optional(
479                "node_getBlock",
480                serde_json::json!([block_param_json(block_number)]),
481            )
482            .await
483    }
484
485    async fn get_note_hash_membership_witness(
486        &self,
487        block_number: u64,
488        note_hash: &Fr,
489    ) -> Result<Option<serde_json::Value>, Error> {
490        self.transport
491            .call_optional(
492                "node_getNoteHashMembershipWitness",
493                serde_json::json!([block_param_json(block_number), note_hash]),
494            )
495            .await
496    }
497
498    async fn get_nullifier_membership_witness(
499        &self,
500        block_number: u64,
501        nullifier: &Fr,
502    ) -> Result<Option<serde_json::Value>, Error> {
503        self.transport
504            .call_optional(
505                "node_getNullifierMembershipWitness",
506                serde_json::json!([block_param_json(block_number), nullifier]),
507            )
508            .await
509    }
510
511    async fn get_low_nullifier_membership_witness(
512        &self,
513        block_number: u64,
514        nullifier: &Fr,
515    ) -> Result<Option<serde_json::Value>, Error> {
516        self.transport
517            .call_optional(
518                "node_getLowNullifierMembershipWitness",
519                serde_json::json!([block_param_json(block_number), nullifier]),
520            )
521            .await
522    }
523
524    async fn get_public_storage_at(
525        &self,
526        block_number: u64,
527        contract: &AztecAddress,
528        slot: &Fr,
529    ) -> Result<Fr, Error> {
530        self.transport
531            .call(
532                "node_getPublicStorageAt",
533                serde_json::json!([block_param_json(block_number), contract, slot]),
534            )
535            .await
536    }
537
538    async fn get_public_storage_at_by_hash(
539        &self,
540        block_hash: &Fr,
541        contract: &AztecAddress,
542        slot: &Fr,
543    ) -> Result<Fr, Error> {
544        self.transport
545            .call(
546                "node_getPublicStorageAt",
547                serde_json::json!([block_hash_param_json(block_hash), contract, slot]),
548            )
549            .await
550    }
551
552    async fn get_public_data_witness(
553        &self,
554        block_number: u64,
555        leaf_slot: &Fr,
556    ) -> Result<Option<serde_json::Value>, Error> {
557        self.transport
558            .call_optional(
559                "node_getPublicDataWitness",
560                serde_json::json!([block_param_json(block_number), leaf_slot]),
561            )
562            .await
563    }
564
565    async fn get_public_data_witness_by_hash(
566        &self,
567        block_hash: &Fr,
568        leaf_slot: &Fr,
569    ) -> Result<Option<serde_json::Value>, Error> {
570        self.transport
571            .call_optional(
572                "node_getPublicDataWitness",
573                serde_json::json!([block_hash_param_json(block_hash), leaf_slot]),
574            )
575            .await
576    }
577
578    async fn get_l1_to_l2_message_membership_witness(
579        &self,
580        block_number: u64,
581        entry_key: &Fr,
582    ) -> Result<Option<serde_json::Value>, Error> {
583        self.transport
584            .call_optional(
585                "node_getL1ToL2MessageMembershipWitness",
586                serde_json::json!([block_param_json(block_number), entry_key]),
587            )
588            .await
589    }
590
591    async fn get_l1_to_l2_message_checkpoint(
592        &self,
593        l1_to_l2_message: &Fr,
594    ) -> Result<Option<u64>, Error> {
595        let result: Option<serde_json::Value> = self
596            .transport
597            .call_optional(
598                "node_getL1ToL2MessageCheckpoint",
599                serde_json::json!([l1_to_l2_message]),
600            )
601            .await?;
602        Ok(result.and_then(|v| v.as_u64()))
603    }
604
605    async fn simulate_public_calls(
606        &self,
607        tx: &serde_json::Value,
608        skip_fee_enforcement: bool,
609    ) -> Result<serde_json::Value, Error> {
610        self.transport
611            .call(
612                "node_simulatePublicCalls",
613                serde_json::json!([tx, skip_fee_enforcement]),
614            )
615            .await
616    }
617
618    async fn is_valid_tx(&self, tx: &serde_json::Value) -> Result<TxValidationResult, Error> {
619        self.transport
620            .call("node_isValidTx", serde_json::json!([tx]))
621            .await
622    }
623
624    async fn get_private_logs_by_tags(&self, tags: &[Fr]) -> Result<serde_json::Value, Error> {
625        self.transport
626            .call("node_getPrivateLogsByTags", serde_json::json!([tags]))
627            .await
628    }
629
630    async fn get_public_logs_by_tags_from_contract(
631        &self,
632        contract: &AztecAddress,
633        tags: &[Fr],
634    ) -> Result<serde_json::Value, Error> {
635        self.transport
636            .call(
637                "node_getPublicLogsByTagsFromContract",
638                serde_json::json!([contract, tags]),
639            )
640            .await
641    }
642
643    async fn register_contract_function_signatures(
644        &self,
645        signatures: &[String],
646    ) -> Result<(), Error> {
647        self.transport
648            .call_void(
649                "node_registerContractFunctionSignatures",
650                serde_json::json!([signatures]),
651            )
652            .await
653    }
654
655    async fn get_tx_effect(&self, tx_hash: &TxHash) -> Result<Option<serde_json::Value>, Error> {
656        self.transport
657            .call_optional("node_getTxEffect", serde_json::json!([tx_hash]))
658            .await
659    }
660
661    async fn get_block_hash_membership_witness(
662        &self,
663        block_number: u64,
664        block_hash: &Fr,
665    ) -> Result<Option<serde_json::Value>, Error> {
666        self.transport
667            .call_optional(
668                "node_getBlockHashMembershipWitness",
669                serde_json::json!([block_param_json(block_number), block_hash]),
670            )
671            .await
672    }
673
674    async fn find_leaves_indexes(
675        &self,
676        block_number: u64,
677        tree_id: &str,
678        leaves: &[Fr],
679    ) -> Result<Vec<Option<u64>>, Error> {
680        // The node returns InBlock<bigint>[] — objects with { data, l2BlockNumber, l2BlockHash }
681        // or null for leaves not found. We extract just the `data` field.
682        let raw: Vec<Option<serde_json::Value>> = self
683            .transport
684            .call(
685                "node_findLeavesIndexes",
686                serde_json::json!([
687                    block_param_json(block_number),
688                    tree_id.parse::<u8>().unwrap_or(0),
689                    leaves
690                ]),
691            )
692            .await?;
693        Ok(raw
694            .into_iter()
695            .map(|v| {
696                v.and_then(|val| {
697                    // Could be a plain number, a string number, or an
698                    // InBlock object { data: N | "N" }.
699                    let extract = |v: &serde_json::Value| {
700                        v.as_u64()
701                            .or_else(|| v.as_str().and_then(|s| s.parse::<u64>().ok()))
702                    };
703                    val.get("data").and_then(extract).or_else(|| extract(&val))
704                })
705            })
706            .collect())
707    }
708
709    async fn get_tx_by_hash(&self, tx_hash: &TxHash) -> Result<Option<serde_json::Value>, Error> {
710        self.transport
711            .call_optional("node_getTxByHash", serde_json::json!([tx_hash]))
712            .await
713    }
714}
715
716/// Create an HTTP JSON-RPC backed Aztec node client.
717pub fn create_aztec_node_client(url: impl Into<String>) -> HttpNodeClient {
718    HttpNodeClient::new(url.into(), Duration::from_secs(30))
719}
720
721// ---------------------------------------------------------------------------
722// Polling helpers
723// ---------------------------------------------------------------------------
724
725/// Wait for the node to become ready by retrying `get_node_info`.
726///
727/// Uses a default timeout of 120 seconds with a 1 second polling interval.
728/// Returns the `NodeInfo` on success, or a timeout error.
729pub async fn wait_for_node(node: &(impl AztecNode + ?Sized)) -> Result<NodeInfo, Error> {
730    wait_for_node_opts(node, Duration::from_secs(120), Duration::from_secs(1)).await
731}
732
733async fn wait_for_node_opts(
734    node: &(impl AztecNode + ?Sized),
735    timeout: Duration,
736    interval: Duration,
737) -> Result<NodeInfo, Error> {
738    let deadline = tokio::time::Instant::now() + timeout;
739    loop {
740        match node.get_node_info().await {
741            Ok(info) => return Ok(info),
742            Err(_) if tokio::time::Instant::now() + interval < deadline => {
743                tokio::time::sleep(interval).await;
744            }
745            Err(e) => {
746                return Err(Error::Timeout(format!(
747                    "node not ready after {timeout:?}: {e}"
748                )));
749            }
750        }
751    }
752}
753
754/// Returns `true` if `status` meets or exceeds `target` in the tx lifecycle progression.
755const fn status_reached(status: TxStatus, target: TxStatus) -> bool {
756    status_ordinal(status) >= status_ordinal(target)
757}
758
759const fn status_ordinal(s: TxStatus) -> u8 {
760    match s {
761        TxStatus::Dropped => 0,
762        TxStatus::Pending => 1,
763        TxStatus::Proposed => 2,
764        TxStatus::Checkpointed => 3,
765        TxStatus::Proven => 4,
766        TxStatus::Finalized => 5,
767    }
768}
769
770/// Wait for a transaction to reach a terminal status by polling `get_tx_receipt`.
771///
772/// By default, waits until `Checkpointed` or higher. Set `opts.wait_for_status`
773/// to target a different status. Returns early with an error on `Dropped`
774/// (after the grace period) or reverted execution results (unless
775/// `opts.dont_throw_on_revert` is set).
776pub async fn wait_for_tx(
777    node: &(impl AztecNode + ?Sized),
778    tx_hash: &TxHash,
779    opts: WaitOpts,
780) -> Result<TxReceipt, Error> {
781    let start = tokio::time::Instant::now();
782    let deadline = start + opts.timeout;
783    let target = opts.wait_for_status;
784
785    loop {
786        match node.get_tx_receipt(tx_hash).await {
787            Ok(receipt) => {
788                if receipt.is_dropped() {
789                    let elapsed = start.elapsed();
790                    if elapsed >= opts.ignore_dropped_receipts_for {
791                        return Err(Error::Reverted(format!(
792                            "tx {tx_hash} was dropped: {}",
793                            receipt.error.as_deref().unwrap_or("unknown reason")
794                        )));
795                    }
796                    // Within grace period — keep polling.
797                } else {
798                    if receipt.has_execution_reverted() && !opts.dont_throw_on_revert {
799                        return Err(Error::Reverted(format!(
800                            "tx {tx_hash} execution reverted: {}",
801                            receipt.error.as_deref().unwrap_or("unknown reason")
802                        )));
803                    }
804                    if status_reached(receipt.status, target) {
805                        return Ok(receipt);
806                    }
807                }
808            }
809            Err(e) => {
810                if tokio::time::Instant::now() + opts.interval >= deadline {
811                    return Err(Error::Timeout(format!(
812                        "timed out waiting for tx {tx_hash}: {e}"
813                    )));
814                }
815            }
816        }
817
818        if tokio::time::Instant::now() + opts.interval >= deadline {
819            return Err(Error::Timeout(format!(
820                "tx {tx_hash} did not reach {target:?} within {:?}",
821                opts.timeout
822            )));
823        }
824        tokio::time::sleep(opts.interval).await;
825    }
826}
827
828/// Options for [`wait_for_proven`].
829#[derive(Debug, Clone)]
830pub struct WaitForProvenOpts {
831    /// Timeout for proven status polling (default: 600s / 10 minutes).
832    pub proven_timeout: Duration,
833    /// Polling interval (default: 1s).
834    pub interval: Duration,
835}
836
837impl Default for WaitForProvenOpts {
838    fn default() -> Self {
839        Self {
840            proven_timeout: Duration::from_secs(600),
841            interval: Duration::from_secs(1),
842        }
843    }
844}
845
846/// Wait until the block containing the given receipt is proven on L1.
847///
848/// Takes a receipt (which must have a `block_number`) and polls the node
849/// until the proven block number >= receipt's block number.
850/// Returns the proven block number on success.
851pub async fn wait_for_proven(
852    node: &(impl AztecNode + ?Sized),
853    receipt: &TxReceipt,
854    opts: WaitForProvenOpts,
855) -> Result<u64, Error> {
856    let receipt_block = receipt.block_number.ok_or_else(|| {
857        Error::InvalidData("receipt has no block_number — cannot wait for proven".into())
858    })?;
859
860    let deadline = tokio::time::Instant::now() + opts.proven_timeout;
861
862    loop {
863        match node.get_proven_block_number().await {
864            Ok(proven_block) if proven_block >= receipt_block => {
865                return Ok(proven_block);
866            }
867            Ok(_) => {}
868            Err(e) => {
869                if tokio::time::Instant::now() + opts.interval >= deadline {
870                    return Err(Error::Timeout(format!(
871                        "timed out waiting for block {receipt_block} to be proven: {e}"
872                    )));
873                }
874            }
875        }
876
877        if tokio::time::Instant::now() + opts.interval >= deadline {
878            return Err(Error::Timeout(format!(
879                "block {receipt_block} was not proven within {:?}",
880                opts.proven_timeout
881            )));
882        }
883        tokio::time::sleep(opts.interval).await;
884    }
885}
886
887// ---------------------------------------------------------------------------
888// Tests
889// ---------------------------------------------------------------------------
890
891#[cfg(test)]
892#[allow(clippy::unwrap_used, clippy::expect_used)]
893mod tests {
894    use super::*;
895    use crate::tx::TxExecutionResult;
896    use std::sync::atomic::{AtomicUsize, Ordering};
897    use std::sync::Mutex;
898
899    // -- NodeInfo fixture deserialization --
900
901    #[test]
902    fn node_info_deserializes() {
903        let json = r#"{
904            "nodeVersion": "0.42.0",
905            "l1ChainId": 31337,
906            "rollupVersion": 1,
907            "enr": "enr:-abc123",
908            "l1ContractAddresses": {"rollup": "0x1234"},
909            "protocolContractAddresses": {"classRegisterer": "0xabcd"},
910            "realProofs": false
911        }"#;
912
913        let info: NodeInfo = serde_json::from_str(json).unwrap();
914        assert_eq!(info.node_version, "0.42.0");
915        assert_eq!(info.l1_chain_id, 31337);
916        assert_eq!(info.rollup_version, 1);
917        assert_eq!(info.enr.as_deref(), Some("enr:-abc123"));
918        assert!(!info.real_proofs);
919    }
920
921    #[test]
922    fn node_info_roundtrip() {
923        let info = NodeInfo {
924            node_version: "1.0.0".into(),
925            l1_chain_id: 1,
926            rollup_version: 2,
927            enr: None,
928            l1_contract_addresses: serde_json::json!({}),
929            protocol_contract_addresses: serde_json::json!({}),
930            l2_circuits_vk_tree_root: None,
931            l2_protocol_contracts_hash: None,
932            real_proofs: true,
933        };
934        let json = serde_json::to_string(&info).unwrap();
935        let decoded: NodeInfo = serde_json::from_str(&json).unwrap();
936        assert_eq!(decoded.node_version, "1.0.0");
937        assert_eq!(decoded.l1_chain_id, 1);
938        assert_eq!(decoded.rollup_version, 2);
939        assert!(decoded.real_proofs);
940    }
941
942    #[test]
943    fn node_info_minimal_json() {
944        let json = r#"{
945            "nodeVersion": "0.1.0",
946            "l1ChainId": 1,
947            "rollupVersion": 1,
948            "realProofs": false
949        }"#;
950        let info: NodeInfo = serde_json::from_str(json).unwrap();
951        assert!(info.enr.is_none());
952        assert_eq!(info.rollup_version, 1);
953    }
954
955    // -- PublicLogFilter --
956
957    #[test]
958    fn public_log_filter_default_serializes_empty() {
959        let filter = PublicLogFilter::default();
960        let json = serde_json::to_value(&filter).unwrap();
961        assert_eq!(json, serde_json::json!({}));
962    }
963
964    #[test]
965    fn public_log_filter_with_fields() {
966        let tx_hash =
967            TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
968                .unwrap();
969        let filter = PublicLogFilter {
970            tx_hash: Some(tx_hash),
971            from_block: Some(10),
972            to_block: Some(20),
973            ..Default::default()
974        };
975        let json = serde_json::to_value(&filter).unwrap();
976        assert_eq!(json["txHash"], tx_hash.to_string());
977        assert_eq!(json["fromBlock"], 10);
978        assert_eq!(json["toBlock"], 20);
979        assert!(json.get("contractAddress").is_none());
980    }
981
982    // -- PublicLogsResponse --
983
984    #[test]
985    fn public_logs_response_roundtrip() {
986        let resp = PublicLogsResponse {
987            logs: vec![],
988            max_logs_hit: false,
989        };
990        let json = serde_json::to_string(&resp).unwrap();
991        let decoded: PublicLogsResponse = serde_json::from_str(&json).unwrap();
992        assert!(!decoded.max_logs_hit);
993        assert!(decoded.logs.is_empty());
994    }
995
996    // -- WaitOpts --
997
998    #[test]
999    fn wait_opts_defaults() {
1000        let opts = WaitOpts::default();
1001        assert_eq!(opts.timeout, Duration::from_secs(300));
1002        assert_eq!(opts.interval, Duration::from_secs(1));
1003        assert_eq!(opts.wait_for_status, TxStatus::Checkpointed);
1004        assert!(!opts.dont_throw_on_revert);
1005        assert_eq!(opts.ignore_dropped_receipts_for, Duration::from_secs(5));
1006    }
1007
1008    // -- Status ordering --
1009
1010    #[test]
1011    fn status_ordering() {
1012        assert!(status_reached(TxStatus::Finalized, TxStatus::Checkpointed));
1013        assert!(status_reached(TxStatus::Proven, TxStatus::Checkpointed));
1014        assert!(status_reached(
1015            TxStatus::Checkpointed,
1016            TxStatus::Checkpointed
1017        ));
1018        assert!(!status_reached(TxStatus::Proposed, TxStatus::Checkpointed));
1019        assert!(!status_reached(TxStatus::Pending, TxStatus::Checkpointed));
1020        assert!(!status_reached(TxStatus::Dropped, TxStatus::Checkpointed));
1021
1022        assert!(status_reached(TxStatus::Finalized, TxStatus::Proven));
1023        assert!(status_reached(TxStatus::Proven, TxStatus::Proven));
1024        assert!(!status_reached(TxStatus::Checkpointed, TxStatus::Proven));
1025    }
1026
1027    // -- Mock node for trait-based tests --
1028
1029    struct MockNode {
1030        info_result: Mutex<Vec<Result<NodeInfo, Error>>>,
1031        block_number: u64,
1032        proven_block_results: Mutex<Vec<Result<u64, Error>>>,
1033        receipt_results: Mutex<Vec<Result<TxReceipt, Error>>>,
1034        call_count: AtomicUsize,
1035    }
1036
1037    impl MockNode {
1038        fn new_ready(info: NodeInfo) -> Self {
1039            Self {
1040                info_result: Mutex::new(vec![Ok(info)]),
1041                block_number: 0,
1042                proven_block_results: Mutex::new(vec![]),
1043                receipt_results: Mutex::new(vec![]),
1044                call_count: AtomicUsize::new(0),
1045            }
1046        }
1047
1048        fn new_with_info_sequence(results: Vec<Result<NodeInfo, Error>>) -> Self {
1049            Self {
1050                info_result: Mutex::new(results),
1051                block_number: 0,
1052                proven_block_results: Mutex::new(vec![]),
1053                receipt_results: Mutex::new(vec![]),
1054                call_count: AtomicUsize::new(0),
1055            }
1056        }
1057
1058        fn new_with_receipt_sequence(results: Vec<Result<TxReceipt, Error>>) -> Self {
1059            Self {
1060                info_result: Mutex::new(vec![]),
1061                block_number: 0,
1062                proven_block_results: Mutex::new(vec![]),
1063                receipt_results: Mutex::new(results),
1064                call_count: AtomicUsize::new(0),
1065            }
1066        }
1067
1068        fn new_with_proven_block_sequence(results: Vec<Result<u64, Error>>) -> Self {
1069            Self {
1070                info_result: Mutex::new(vec![]),
1071                block_number: 0,
1072                proven_block_results: Mutex::new(results),
1073                receipt_results: Mutex::new(vec![]),
1074                call_count: AtomicUsize::new(0),
1075            }
1076        }
1077
1078        fn sample_info() -> NodeInfo {
1079            NodeInfo {
1080                node_version: "test-0.1.0".into(),
1081                l1_chain_id: 31337,
1082                rollup_version: 1,
1083                enr: None,
1084                l1_contract_addresses: serde_json::json!({}),
1085                protocol_contract_addresses: serde_json::json!({}),
1086                l2_circuits_vk_tree_root: None,
1087                l2_protocol_contracts_hash: None,
1088                real_proofs: false,
1089            }
1090        }
1091
1092        fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
1093            TxReceipt {
1094                tx_hash: TxHash::zero(),
1095                status,
1096                execution_result: exec,
1097                error: None,
1098                transaction_fee: None,
1099                block_hash: None,
1100                block_number: None,
1101                epoch_number: None,
1102            }
1103        }
1104    }
1105
1106    #[async_trait]
1107    impl AztecNode for MockNode {
1108        async fn get_node_info(&self) -> Result<NodeInfo, Error> {
1109            let idx = self.call_count.fetch_add(1, Ordering::Relaxed);
1110            let results = self.info_result.lock().unwrap();
1111            if idx < results.len() {
1112                match &results[idx] {
1113                    Ok(info) => Ok(info.clone()),
1114                    Err(e) => Err(Error::Transport(e.to_string())),
1115                }
1116            } else if let Some(last) = results.last() {
1117                match last {
1118                    Ok(info) => Ok(info.clone()),
1119                    Err(e) => Err(Error::Transport(e.to_string())),
1120                }
1121            } else {
1122                Err(Error::Transport("no mock results configured".into()))
1123            }
1124        }
1125
1126        async fn get_block_number(&self) -> Result<u64, Error> {
1127            Ok(self.block_number)
1128        }
1129
1130        async fn get_proven_block_number(&self) -> Result<u64, Error> {
1131            let idx = self.call_count.fetch_add(1, Ordering::Relaxed);
1132            let results = self.proven_block_results.lock().unwrap();
1133            if idx < results.len() {
1134                match &results[idx] {
1135                    Ok(n) => Ok(*n),
1136                    Err(e) => Err(Error::Transport(e.to_string())),
1137                }
1138            } else if let Some(last) = results.last() {
1139                match last {
1140                    Ok(n) => Ok(*n),
1141                    Err(e) => Err(Error::Transport(e.to_string())),
1142                }
1143            } else {
1144                Ok(0)
1145            }
1146        }
1147
1148        async fn get_tx_receipt(&self, _tx_hash: &TxHash) -> Result<TxReceipt, Error> {
1149            let idx = self.call_count.fetch_add(1, Ordering::Relaxed);
1150            let results = self.receipt_results.lock().unwrap();
1151            if idx < results.len() {
1152                match &results[idx] {
1153                    Ok(r) => Ok(r.clone()),
1154                    Err(e) => Err(Error::Transport(e.to_string())),
1155                }
1156            } else if let Some(last) = results.last() {
1157                match last {
1158                    Ok(r) => Ok(r.clone()),
1159                    Err(e) => Err(Error::Transport(e.to_string())),
1160                }
1161            } else {
1162                Err(Error::Transport("no mock results configured".into()))
1163            }
1164        }
1165
1166        async fn get_public_logs(
1167            &self,
1168            _filter: PublicLogFilter,
1169        ) -> Result<PublicLogsResponse, Error> {
1170            Ok(PublicLogsResponse {
1171                logs: vec![],
1172                max_logs_hit: false,
1173            })
1174        }
1175
1176        async fn send_tx(&self, _tx: &serde_json::Value) -> Result<(), Error> {
1177            Ok(())
1178        }
1179
1180        async fn get_contract(
1181            &self,
1182            _address: &AztecAddress,
1183        ) -> Result<Option<ContractInstanceWithAddress>, Error> {
1184            Ok(None)
1185        }
1186
1187        async fn get_contract_class(&self, _id: &Fr) -> Result<Option<serde_json::Value>, Error> {
1188            Ok(None)
1189        }
1190
1191        async fn get_block_header(&self, _block_number: u64) -> Result<serde_json::Value, Error> {
1192            Ok(serde_json::json!({"blockNumber": 1}))
1193        }
1194        async fn get_block(&self, _block_number: u64) -> Result<Option<serde_json::Value>, Error> {
1195            Ok(None)
1196        }
1197        async fn get_tx_effect(
1198            &self,
1199            _tx_hash: &TxHash,
1200        ) -> Result<Option<serde_json::Value>, Error> {
1201            Ok(None)
1202        }
1203        async fn get_note_hash_membership_witness(
1204            &self,
1205            _block_number: u64,
1206            _note_hash: &Fr,
1207        ) -> Result<Option<serde_json::Value>, Error> {
1208            Ok(None)
1209        }
1210        async fn get_nullifier_membership_witness(
1211            &self,
1212            _block_number: u64,
1213            _nullifier: &Fr,
1214        ) -> Result<Option<serde_json::Value>, Error> {
1215            Ok(None)
1216        }
1217        async fn get_low_nullifier_membership_witness(
1218            &self,
1219            _block_number: u64,
1220            _nullifier: &Fr,
1221        ) -> Result<Option<serde_json::Value>, Error> {
1222            Ok(None)
1223        }
1224        async fn get_public_storage_at(
1225            &self,
1226            _block_number: u64,
1227            _contract: &AztecAddress,
1228            _slot: &Fr,
1229        ) -> Result<Fr, Error> {
1230            Ok(Fr::zero())
1231        }
1232        async fn get_public_data_witness(
1233            &self,
1234            _block_number: u64,
1235            _leaf_slot: &Fr,
1236        ) -> Result<Option<serde_json::Value>, Error> {
1237            Ok(None)
1238        }
1239        async fn get_l1_to_l2_message_membership_witness(
1240            &self,
1241            _block_number: u64,
1242            _entry_key: &Fr,
1243        ) -> Result<Option<serde_json::Value>, Error> {
1244            Ok(None)
1245        }
1246        async fn simulate_public_calls(
1247            &self,
1248            _tx: &serde_json::Value,
1249            _skip_fee_enforcement: bool,
1250        ) -> Result<serde_json::Value, Error> {
1251            Ok(serde_json::Value::Null)
1252        }
1253        async fn is_valid_tx(&self, _tx: &serde_json::Value) -> Result<TxValidationResult, Error> {
1254            Ok(TxValidationResult::Valid)
1255        }
1256        async fn get_private_logs_by_tags(&self, _tags: &[Fr]) -> Result<serde_json::Value, Error> {
1257            Ok(serde_json::json!([]))
1258        }
1259        async fn get_public_logs_by_tags_from_contract(
1260            &self,
1261            _contract: &AztecAddress,
1262            _tags: &[Fr],
1263        ) -> Result<serde_json::Value, Error> {
1264            Ok(serde_json::json!([]))
1265        }
1266        async fn register_contract_function_signatures(
1267            &self,
1268            _signatures: &[String],
1269        ) -> Result<(), Error> {
1270            Ok(())
1271        }
1272        async fn get_block_hash_membership_witness(
1273            &self,
1274            _block_number: u64,
1275            _block_hash: &Fr,
1276        ) -> Result<Option<serde_json::Value>, Error> {
1277            Ok(None)
1278        }
1279        async fn find_leaves_indexes(
1280            &self,
1281            _block_number: u64,
1282            _tree_id: &str,
1283            _leaves: &[Fr],
1284        ) -> Result<Vec<Option<u64>>, Error> {
1285            Ok(vec![])
1286        }
1287        async fn get_tx_by_hash(
1288            &self,
1289            _tx_hash: &TxHash,
1290        ) -> Result<Option<serde_json::Value>, Error> {
1291            Ok(None)
1292        }
1293    }
1294
1295    // -- Mock-based RPC tests --
1296
1297    #[tokio::test]
1298    async fn mock_get_node_info() {
1299        let node = MockNode::new_ready(MockNode::sample_info());
1300        let info = node.get_node_info().await.unwrap();
1301        assert_eq!(info.node_version, "test-0.1.0");
1302        assert_eq!(info.l1_chain_id, 31337);
1303    }
1304
1305    #[tokio::test]
1306    async fn mock_get_block_number() {
1307        let node = MockNode {
1308            block_number: 42,
1309            ..MockNode::new_ready(MockNode::sample_info())
1310        };
1311        let bn = node.get_block_number().await.unwrap();
1312        assert_eq!(bn, 42);
1313    }
1314
1315    #[tokio::test]
1316    async fn mock_get_tx_receipt() {
1317        let receipt =
1318            MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success));
1319        let node = MockNode::new_with_receipt_sequence(vec![Ok(receipt.clone())]);
1320        let result = node.get_tx_receipt(&TxHash::zero()).await.unwrap();
1321        assert_eq!(result.status, TxStatus::Checkpointed);
1322        assert!(result.has_execution_succeeded());
1323    }
1324
1325    #[tokio::test]
1326    async fn mock_get_public_logs() {
1327        let node = MockNode::new_ready(MockNode::sample_info());
1328        let resp = node
1329            .get_public_logs(PublicLogFilter::default())
1330            .await
1331            .unwrap();
1332        assert!(resp.logs.is_empty());
1333        assert!(!resp.max_logs_hit);
1334    }
1335
1336    // -- wait_for_node tests --
1337
1338    #[tokio::test]
1339    async fn wait_for_node_immediate_success() {
1340        let node = MockNode::new_ready(MockNode::sample_info());
1341        let info = wait_for_node_opts(&node, Duration::from_secs(5), Duration::from_millis(10))
1342            .await
1343            .unwrap();
1344        assert_eq!(info.node_version, "test-0.1.0");
1345    }
1346
1347    #[tokio::test]
1348    async fn wait_for_node_delayed_success() {
1349        let node = MockNode::new_with_info_sequence(vec![
1350            Err(Error::Transport("not ready".into())),
1351            Err(Error::Transport("not ready".into())),
1352            Ok(MockNode::sample_info()),
1353        ]);
1354        let info = wait_for_node_opts(&node, Duration::from_secs(5), Duration::from_millis(10))
1355            .await
1356            .unwrap();
1357        assert_eq!(info.node_version, "test-0.1.0");
1358    }
1359
1360    #[tokio::test]
1361    async fn wait_for_node_timeout() {
1362        let node =
1363            MockNode::new_with_info_sequence(vec![Err(Error::Transport("not ready".into()))]);
1364        let result =
1365            wait_for_node_opts(&node, Duration::from_millis(50), Duration::from_millis(100)).await;
1366        assert!(result.is_err());
1367        let err = result.unwrap_err();
1368        assert!(matches!(err, Error::Timeout(_)));
1369    }
1370
1371    // -- wait_for_tx tests --
1372
1373    #[tokio::test]
1374    async fn wait_for_tx_immediate_success() {
1375        let receipt =
1376            MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success));
1377        let node = MockNode::new_with_receipt_sequence(vec![Ok(receipt)]);
1378        let opts = WaitOpts {
1379            timeout: Duration::from_secs(5),
1380            interval: Duration::from_millis(10),
1381            ..WaitOpts::default()
1382        };
1383        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1384        assert_eq!(result.status, TxStatus::Checkpointed);
1385    }
1386
1387    #[tokio::test]
1388    async fn wait_for_tx_delayed_success() {
1389        let pending = MockNode::make_receipt(TxStatus::Pending, None);
1390        let proposed = MockNode::make_receipt(TxStatus::Proposed, Some(TxExecutionResult::Success));
1391        let checkpointed =
1392            MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success));
1393
1394        let node =
1395            MockNode::new_with_receipt_sequence(vec![Ok(pending), Ok(proposed), Ok(checkpointed)]);
1396        let opts = WaitOpts {
1397            timeout: Duration::from_secs(5),
1398            interval: Duration::from_millis(10),
1399            ..WaitOpts::default()
1400        };
1401        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1402        assert_eq!(result.status, TxStatus::Checkpointed);
1403    }
1404
1405    #[tokio::test]
1406    async fn wait_for_tx_proven() {
1407        let checkpointed =
1408            MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success));
1409        let proven = MockNode::make_receipt(TxStatus::Proven, Some(TxExecutionResult::Success));
1410
1411        let node = MockNode::new_with_receipt_sequence(vec![Ok(checkpointed), Ok(proven)]);
1412        let opts = WaitOpts {
1413            timeout: Duration::from_secs(5),
1414            interval: Duration::from_millis(10),
1415            wait_for_status: TxStatus::Proven,
1416            ..WaitOpts::default()
1417        };
1418        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1419        assert_eq!(result.status, TxStatus::Proven);
1420    }
1421
1422    #[tokio::test]
1423    async fn wait_for_tx_dropped() {
1424        let receipt = MockNode::make_receipt(TxStatus::Dropped, None);
1425        let node = MockNode::new_with_receipt_sequence(vec![Ok(receipt)]);
1426        let opts = WaitOpts {
1427            timeout: Duration::from_secs(5),
1428            interval: Duration::from_millis(10),
1429            ignore_dropped_receipts_for: Duration::ZERO,
1430            ..WaitOpts::default()
1431        };
1432        let result = wait_for_tx(&node, &TxHash::zero(), opts).await;
1433        assert!(result.is_err());
1434        assert!(matches!(result.unwrap_err(), Error::Reverted(_)));
1435    }
1436
1437    #[tokio::test]
1438    async fn wait_for_tx_reverted() {
1439        let receipt = MockNode::make_receipt(
1440            TxStatus::Checkpointed,
1441            Some(TxExecutionResult::AppLogicReverted),
1442        );
1443        let node = MockNode::new_with_receipt_sequence(vec![Ok(receipt)]);
1444        let opts = WaitOpts {
1445            timeout: Duration::from_secs(5),
1446            interval: Duration::from_millis(10),
1447            ..WaitOpts::default()
1448        };
1449        let result = wait_for_tx(&node, &TxHash::zero(), opts).await;
1450        assert!(result.is_err());
1451        assert!(matches!(result.unwrap_err(), Error::Reverted(_)));
1452    }
1453
1454    #[tokio::test]
1455    async fn wait_for_tx_timeout() {
1456        let pending = MockNode::make_receipt(TxStatus::Pending, None);
1457        let node = MockNode::new_with_receipt_sequence(vec![Ok(pending)]);
1458        let opts = WaitOpts {
1459            timeout: Duration::from_millis(50),
1460            interval: Duration::from_millis(100),
1461            ..WaitOpts::default()
1462        };
1463        let result = wait_for_tx(&node, &TxHash::zero(), opts).await;
1464        assert!(result.is_err());
1465        assert!(matches!(result.unwrap_err(), Error::Timeout(_)));
1466    }
1467
1468    // -- Receipt progression tests --
1469
1470    #[tokio::test]
1471    async fn wait_for_tx_finalized_exceeds_checkpointed() {
1472        let receipt = MockNode::make_receipt(TxStatus::Finalized, Some(TxExecutionResult::Success));
1473        let node = MockNode::new_with_receipt_sequence(vec![Ok(receipt)]);
1474        let opts = WaitOpts {
1475            timeout: Duration::from_secs(5),
1476            interval: Duration::from_millis(10),
1477            ..WaitOpts::default()
1478        };
1479        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1480        assert_eq!(result.status, TxStatus::Finalized);
1481    }
1482
1483    #[tokio::test]
1484    async fn wait_for_tx_respects_wait_for_status() {
1485        let proposed = MockNode::make_receipt(TxStatus::Proposed, Some(TxExecutionResult::Success));
1486        let proven = MockNode::make_receipt(TxStatus::Proven, Some(TxExecutionResult::Success));
1487        let node = MockNode::new_with_receipt_sequence(vec![Ok(proposed), Ok(proven)]);
1488        let opts = WaitOpts {
1489            timeout: Duration::from_secs(5),
1490            interval: Duration::from_millis(10),
1491            wait_for_status: TxStatus::Proven,
1492            ..WaitOpts::default()
1493        };
1494        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1495        assert_eq!(result.status, TxStatus::Proven);
1496    }
1497
1498    #[tokio::test]
1499    async fn wait_for_tx_dont_throw_on_revert() {
1500        let receipt = MockNode::make_receipt(
1501            TxStatus::Checkpointed,
1502            Some(TxExecutionResult::AppLogicReverted),
1503        );
1504        let node = MockNode::new_with_receipt_sequence(vec![Ok(receipt)]);
1505        let opts = WaitOpts {
1506            timeout: Duration::from_secs(5),
1507            interval: Duration::from_millis(10),
1508            dont_throw_on_revert: true,
1509            ..WaitOpts::default()
1510        };
1511        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1512        assert_eq!(result.status, TxStatus::Checkpointed);
1513        assert!(result.has_execution_reverted());
1514    }
1515
1516    #[tokio::test]
1517    async fn wait_for_tx_ignores_dropped_within_grace_period() {
1518        let dropped = MockNode::make_receipt(TxStatus::Dropped, None);
1519        let checkpointed =
1520            MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success));
1521        let node = MockNode::new_with_receipt_sequence(vec![Ok(dropped), Ok(checkpointed)]);
1522        let opts = WaitOpts {
1523            timeout: Duration::from_secs(5),
1524            interval: Duration::from_millis(10),
1525            ignore_dropped_receipts_for: Duration::from_secs(60),
1526            ..WaitOpts::default()
1527        };
1528        let result = wait_for_tx(&node, &TxHash::zero(), opts).await.unwrap();
1529        assert_eq!(result.status, TxStatus::Checkpointed);
1530    }
1531
1532    #[tokio::test]
1533    async fn wait_for_tx_fails_dropped_after_grace_period() {
1534        let dropped = MockNode::make_receipt(TxStatus::Dropped, None);
1535        let node = MockNode::new_with_receipt_sequence(vec![Ok(dropped)]);
1536        let opts = WaitOpts {
1537            timeout: Duration::from_secs(5),
1538            interval: Duration::from_millis(10),
1539            ignore_dropped_receipts_for: Duration::ZERO,
1540            ..WaitOpts::default()
1541        };
1542        let result = wait_for_tx(&node, &TxHash::zero(), opts).await;
1543        assert!(result.is_err());
1544        assert!(matches!(result.unwrap_err(), Error::Reverted(_)));
1545    }
1546
1547    // -- wait_for_proven tests --
1548
1549    #[tokio::test]
1550    async fn wait_for_proven_returns_when_proven() {
1551        let node = MockNode::new_with_proven_block_sequence(vec![Ok(5), Ok(10)]);
1552        let receipt = TxReceipt {
1553            block_number: Some(8),
1554            ..MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success))
1555        };
1556        let opts = WaitForProvenOpts {
1557            proven_timeout: Duration::from_secs(5),
1558            interval: Duration::from_millis(10),
1559        };
1560        let result = wait_for_proven(&node, &receipt, opts).await.unwrap();
1561        assert!(result >= 8);
1562    }
1563
1564    #[tokio::test]
1565    async fn wait_for_proven_times_out() {
1566        let node = MockNode::new_with_proven_block_sequence(vec![Ok(1)]);
1567        let receipt = TxReceipt {
1568            block_number: Some(100),
1569            ..MockNode::make_receipt(TxStatus::Checkpointed, Some(TxExecutionResult::Success))
1570        };
1571        let opts = WaitForProvenOpts {
1572            proven_timeout: Duration::from_millis(50),
1573            interval: Duration::from_millis(100),
1574        };
1575        let result = wait_for_proven(&node, &receipt, opts).await;
1576        assert!(result.is_err());
1577        assert!(matches!(result.unwrap_err(), Error::Timeout(_)));
1578    }
1579
1580    #[tokio::test]
1581    async fn wait_for_proven_rejects_receipt_without_block() {
1582        let node = MockNode::new_with_proven_block_sequence(vec![Ok(10)]);
1583        let receipt = MockNode::make_receipt(TxStatus::Pending, None);
1584        let opts = WaitForProvenOpts::default();
1585        let result = wait_for_proven(&node, &receipt, opts).await;
1586        assert!(result.is_err());
1587        assert!(
1588            result.unwrap_err().to_string().contains("no block_number"),
1589            "should mention missing block_number"
1590        );
1591    }
1592
1593    // -- create_aztec_node_client --
1594
1595    #[test]
1596    fn create_client_does_not_panic() {
1597        let _client = create_aztec_node_client("http://localhost:8080");
1598    }
1599
1600    // -- Trait object safety --
1601
1602    #[test]
1603    fn aztec_node_is_object_safe() {
1604        fn _assert_object_safe(_: &dyn AztecNode) {}
1605    }
1606}