Skip to main content

miden_client/test_utils/
mock.rs

1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::string::String;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7
8use miden_protocol::Word;
9use miden_protocol::account::{
10    AccountId,
11    AccountUpdateDetails,
12    AccountVaultPatch,
13    StorageMapKey,
14    StorageMapPatchEntries,
15    StorageSlot,
16    StorageSlotContent,
17    StorageSlotName,
18    StorageSlotType,
19};
20use miden_protocol::address::NetworkId;
21use miden_protocol::batch::{ProposedBatch, ProvenBatch};
22use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock};
23use miden_protocol::crypto::merkle::MerklePath;
24use miden_protocol::crypto::merkle::mmr::{Forest, Mmr, MmrProof};
25use miden_protocol::crypto::merkle::smt::PartialSmt;
26use miden_protocol::note::{NoteAttachments, NoteHeader, NoteId, NoteScript, NoteTag};
27use miden_protocol::protocol_config::ProtocolConfig;
28use miden_protocol::transaction::{ExecutedTransaction, OutputNote, ProvenTransaction};
29use miden_protocol::vm::ExecutionProof;
30use miden_testing::{MockChain, MockChainNote};
31use miden_tx::utils::sync::RwLock;
32
33use crate::Client;
34use crate::rpc::domain::account::{
35    AccountDetails,
36    AccountProof,
37    AccountStorageDetails,
38    AccountStorageMapDetails,
39    AccountVaultDetails,
40    GetAccountRequest,
41    StorageMapEntries,
42    StorageMapEntry,
43    StorageMapFetch,
44    VaultFetch,
45};
46use crate::rpc::domain::account_vault::AccountVaultInfo;
47use crate::rpc::domain::note::{CommittedNote, FetchedNote, SyncNotesBlock};
48use crate::rpc::domain::nullifier::NullifierUpdate;
49use crate::rpc::domain::status::NetworkNoteStatusInfo;
50use crate::rpc::domain::storage_map::StorageMapInfo;
51use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
52use crate::rpc::domain::transaction::TransactionRecord;
53use crate::rpc::encryption::{AttestedTransactionEncryptionKey, SealedTransactionInputs};
54use crate::rpc::{AccountStateAt, NodeRpcClient, RpcEndpoint, RpcError, RpcStatusInfo};
55
56pub type MockClient<AUTH> = Client<AUTH>;
57
58#[derive(Clone, Copy)]
59struct BlockHeaderRequest {
60    block_num: Option<BlockNumber>,
61    include_mmr_proof: bool,
62}
63
64/// Mock RPC API
65///
66/// This struct implements the RPC API used by the client to communicate with the node. It simulates
67/// most of the functionality of the actual node, with some small differences:
68/// - It uses a [`MockChain`] to simulate the blockchain state.
69/// - Blocks are not automatically created after time passes, but rather new blocks are created when
70///   calling the `prove_block` method.
71/// - Network account and transactions aren't supported in the current version.
72/// - Account update block numbers aren't tracked, so any endpoint that returns when certain account
73///   updates were made will return the chain tip block number instead.
74#[derive(Clone)]
75pub struct MockRpcApi {
76    account_commitment_updates: Arc<RwLock<BTreeMap<BlockNumber, BTreeMap<AccountId, Word>>>>,
77    pub mock_chain: Arc<RwLock<MockChain>>,
78    /// Chain snapshots used to answer block-pinned account queries.
79    historical_chains: Arc<RwLock<BTreeMap<BlockNumber, Arc<MockChain>>>>,
80    oversize_threshold: usize,
81    /// Note headers to report as erased in sync transaction responses.
82    erased_notes: Arc<RwLock<Vec<NoteHeader>>>,
83    /// Attachment content `get_notes_by_id` serves for private notes, populated by
84    /// `submit_proven_transaction` and by `register_private_note_attachments`. A note absent here
85    /// is served with empty attachments, which is how a test simulates a withholding node.
86    private_note_attachments: Arc<RwLock<BTreeMap<NoteId, NoteAttachments>>>,
87    /// Test overrides for the MMR paths returned by `sync_notes`, keyed by block number.
88    sync_notes_mmr_path_overrides: Arc<RwLock<BTreeMap<BlockNumber, MerklePath>>>,
89    /// Number of `get_notes_by_id` requests served, so a test can assert that a flow avoided the
90    /// round trip.
91    get_notes_by_id_calls: Arc<AtomicUsize>,
92    /// Block header requests, recorded with the requested block and proof flag.
93    block_header_requests: Arc<RwLock<Vec<BlockHeaderRequest>>>,
94    /// Number of `get_account` requests served, so a test can assert that a flow avoided the round
95    /// trip.
96    get_account_calls: Arc<AtomicUsize>,
97    /// Failures to serve instead of answering, keyed by [`RpcEndpoint::proto_name`] and set by
98    /// [`MockRpcApi::fail_next_call`]. An entry is removed when served, so the call after it
99    /// answers normally and a test can exercise a retry.
100    next_call_failures: Arc<RwLock<BTreeMap<&'static str, RpcError>>>,
101    /// Invitation code each account was registered with, recorded by `register_account`.
102    registered_accounts: Arc<RwLock<BTreeMap<AccountId, String>>>,
103    /// Whether `is_account_allowed` consults `registered_accounts`. A node that does not enforce
104    /// the allowlist answers `true` for every account, which is the default here so that tests
105    /// which deploy accounts need no registration.
106    allowlist_enforced: Arc<AtomicBool>,
107    /// Number of `is_account_allowed` requests served, so a test can assert that a flow avoided the
108    /// round trip.
109    is_account_allowed_calls: Arc<AtomicUsize>,
110    /// Sealed inputs handed to `submit_proven_batch`, one entry per call and recorded before any
111    /// staged failure is served, so a test can assert that a resubmission sealed again instead of
112    /// reusing a cached ciphertext.
113    submitted_batch_sealed_inputs: Arc<RwLock<Vec<Vec<SealedTransactionInputs>>>>,
114}
115
116impl Default for MockRpcApi {
117    fn default() -> Self {
118        Self::new(MockChain::new())
119    }
120}
121
122impl MockRpcApi {
123    // Constant to use in mocked pagination.
124    const PAGINATION_BLOCK_LIMIT: u32 = 5;
125
126    /// Creates a new [`MockRpcApi`] instance with the state of the provided [`MockChain`].
127    pub fn new(mock_chain: MockChain) -> Self {
128        Self {
129            account_commitment_updates: Arc::new(RwLock::new(build_account_updates(&mock_chain))),
130            mock_chain: Arc::new(RwLock::new(mock_chain)),
131            historical_chains: Arc::new(RwLock::new(BTreeMap::new())),
132            oversize_threshold: 1000,
133            erased_notes: Arc::new(RwLock::new(Vec::new())),
134            private_note_attachments: Arc::new(RwLock::new(BTreeMap::new())),
135            sync_notes_mmr_path_overrides: Arc::new(RwLock::new(BTreeMap::new())),
136            get_notes_by_id_calls: Arc::new(AtomicUsize::new(0)),
137            block_header_requests: Arc::new(RwLock::new(Vec::new())),
138            get_account_calls: Arc::new(AtomicUsize::new(0)),
139            next_call_failures: Arc::new(RwLock::new(BTreeMap::new())),
140            registered_accounts: Arc::new(RwLock::new(BTreeMap::new())),
141            allowlist_enforced: Arc::new(AtomicBool::new(false)),
142            is_account_allowed_calls: Arc::new(AtomicUsize::new(0)),
143            submitted_batch_sealed_inputs: Arc::new(RwLock::new(Vec::new())),
144        }
145    }
146
147    /// Makes `is_account_allowed` answer from the recorded registrations, modelling a node that
148    /// enforces the account allowlist. Without this the mock answers `true` for every account.
149    pub fn enforce_account_allowlist(&self) {
150        self.allowlist_enforced.store(true, Ordering::SeqCst);
151    }
152
153    /// Id of the first account updated in the mock chain's proven blocks, in block then
154    /// within-block order. Tests use it to get hold of an account the chain already knows.
155    ///
156    /// Panics if the chain has no account updates.
157    pub fn first_account_id(&self) -> AccountId {
158        self.mock_chain
159            .read()
160            .proven_blocks()
161            .iter()
162            .flat_map(|block| block.body().updated_accounts())
163            .next()
164            .expect("the mock chain must have at least one account update")
165            .account_id()
166    }
167
168    /// Sealed inputs recorded by `submit_proven_batch`, one entry per call, including calls that
169    /// went on to be served a staged failure. Within an entry the order matches the batch's
170    /// transaction order.
171    pub fn submitted_batch_sealed_inputs(&self) -> Vec<Vec<SealedTransactionInputs>> {
172        self.submitted_batch_sealed_inputs.read().clone()
173    }
174
175    /// Makes the next call to `endpoint` fail with `error` instead of answering. The failure is
176    /// consumed, so the call after it answers normally and a test can exercise a retry.
177    ///
178    /// Staging a failure for an endpoint whose mock implementation does not look for one is a
179    /// silent no-op.
180    pub fn fail_next_call(&self, endpoint: RpcEndpoint, error: RpcError) {
181        self.next_call_failures.write().insert(endpoint.proto_name(), error);
182    }
183
184    /// Returns the failure staged for `endpoint`, removing it so it is served once.
185    fn take_failure(&self, endpoint: RpcEndpoint) -> Option<RpcError> {
186        self.next_call_failures.write().remove(endpoint.proto_name())
187    }
188
189    /// Returns the invitation code `account_id` was registered with, or `None` if this API served
190    /// no registration for it.
191    pub fn registered_invitation_code(&self, account_id: AccountId) -> Option<String> {
192        self.registered_accounts.read().get(&account_id).cloned()
193    }
194
195    /// Registers the attachment content for a private note so that subsequent `get_notes_by_id`
196    /// responses include it, mirroring a node that stores private-note attachments on-chain.
197    pub fn register_private_note_attachments(&self, note_id: NoteId, attachments: NoteAttachments) {
198        self.private_note_attachments.write().insert(note_id, attachments);
199    }
200
201    /// Returns how many `get_notes_by_id` requests this API has served.
202    pub fn get_notes_by_id_call_count(&self) -> usize {
203        self.get_notes_by_id_calls.load(Ordering::Relaxed)
204    }
205
206    /// Returns the proof flags of requests for `block_num`.
207    pub fn block_header_requests(&self, block_num: BlockNumber) -> Vec<bool> {
208        self.block_header_requests
209            .read()
210            .iter()
211            .filter_map(|request| {
212                (request.block_num == Some(block_num)).then_some(request.include_mmr_proof)
213            })
214            .collect()
215    }
216
217    /// Returns how many `get_account` requests this API has served.
218    pub fn get_account_call_count(&self) -> usize {
219        self.get_account_calls.load(Ordering::Relaxed)
220    }
221
222    /// Returns how many `is_account_allowed` requests this API has served.
223    pub fn is_account_allowed_call_count(&self) -> usize {
224        self.is_account_allowed_calls.load(Ordering::Relaxed)
225    }
226
227    /// Overrides the MMR path returned by `sync_notes` for the specified block.
228    pub fn set_sync_notes_mmr_path(&self, block_num: BlockNumber, path: MerklePath) {
229        self.sync_notes_mmr_path_overrides.write().insert(block_num, path);
230    }
231
232    /// Sets the oversize threshold for `get_account`. A storage map whose entries were requested in
233    /// full comes back as `StorageMapEntries::LimitExceeded` past this threshold, and a vault with
234    /// more assets than it comes back with the `too_many_assets` flag set.
235    #[must_use]
236    pub fn with_oversize_threshold(mut self, threshold: usize) -> Self {
237        self.oversize_threshold = threshold;
238        self
239    }
240
241    /// Registers a note header to be reported as erased in subsequent sync transaction responses.
242    pub fn mark_note_as_erased(&self, header: NoteHeader) {
243        self.erased_notes.write().push(header);
244    }
245
246    /// Returns the current MMR of the blockchain.
247    pub fn get_mmr(&self) -> Mmr {
248        self.mock_chain.read().blockchain().as_mmr().clone()
249    }
250
251    /// Returns the protocol configuration the mock chain commits to.
252    pub fn protocol_config(&self) -> ProtocolConfig {
253        self.mock_chain.read().protocol_config().clone()
254    }
255
256    /// Returns the chain tip block number.
257    pub fn get_chain_tip_block_num(&self) -> BlockNumber {
258        self.mock_chain.read().latest_block_header().block_num()
259    }
260
261    /// Adds an executed transaction to the pending transactions of the mock chain with a dummy
262    /// proof. The next [`Self::prove_block`] call commits it.
263    ///
264    /// Tests use this method to put a transaction on chain without the cost of a real proof.
265    pub fn add_pending_executed_transaction(&self, executed_transaction: &ExecutedTransaction) {
266        self.mock_chain
267            .write()
268            .add_pending_executed_transaction(executed_transaction)
269            .expect("mock chain should accept the executed transaction");
270    }
271
272    /// Advances the mock chain by proving the next block, committing all pending objects to the
273    /// chain in the process.
274    pub fn prove_block(&self) {
275        let proven_block = {
276            let mut mock_chain = self.mock_chain.write();
277            let historical_block_num = mock_chain.latest_block_header().block_num();
278            let snapshot = Arc::new(mock_chain.clone());
279            let proven_block = mock_chain.prove_next_block().unwrap();
280            self.historical_chains.write().insert(historical_block_num, snapshot);
281            proven_block
282        };
283        let block_num = proven_block.header().block_num();
284        let mut account_commitment_updates = self.account_commitment_updates.write();
285        let updates: BTreeMap<AccountId, Word> = proven_block
286            .body()
287            .updated_accounts()
288            .iter()
289            .map(|update| (update.account_id(), update.final_state_commitment()))
290            .collect();
291
292        if !updates.is_empty() {
293            account_commitment_updates.insert(block_num, updates);
294        }
295    }
296
297    /// Removes the account-state snapshot for the specified block.
298    ///
299    /// Tests use this method to model a node that pruned historical account state.
300    pub fn prune_account_state_at(&self, block_num: BlockNumber) {
301        self.historical_chains.write().remove(&block_num);
302    }
303
304    /// Retrieves a block by its block number.
305    fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
306        self.mock_chain.read().block_header(block_num.as_usize())
307    }
308
309    /// Retrieves account vault updates in a given block range. This method tries to simulate
310    /// pagination by limiting the number of blocks processed per request.
311    fn get_sync_account_vault_request(
312        &self,
313        block_from: BlockNumber,
314        block_to: BlockNumber,
315        account_id: AccountId,
316    ) -> (BlockNumber, BlockNumber, AccountVaultPatch) {
317        let chain_tip = self.get_chain_tip_block_num();
318        let target_block = block_to.min(chain_tip);
319
320        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
321            .min(target_block.as_u32())
322            .into();
323
324        // Blocks are iterated in ascending order, so later blocks win per asset ID.
325        let mut vault_patch = AccountVaultPatch::default();
326        for block in self.mock_chain.read().proven_blocks() {
327            let block_number = block.header().block_num();
328            // Only include blocks in range [block_from, page_end_block]
329            if block_number < block_from || block_number > page_end_block {
330                continue;
331            }
332
333            for update in block
334                .body()
335                .updated_accounts()
336                .iter()
337                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
338            {
339                let AccountUpdateDetails::Public(patch) = update.details().clone() else {
340                    continue;
341                };
342
343                vault_patch.merge(patch.vault().clone());
344            }
345        }
346
347        (chain_tip, page_end_block, vault_patch)
348    }
349
350    /// Retrieves transactions in a given block range that match the provided account IDs
351    fn get_sync_transactions_request(
352        &self,
353        block_from: BlockNumber,
354        block_to: BlockNumber,
355        account_ids: &[AccountId],
356    ) -> Vec<TransactionRecord> {
357        let mut transactions = Vec::new();
358        for block in self.mock_chain.read().proven_blocks() {
359            let block_number = block.header().block_num();
360            if block_number < block_from || block_number > block_to {
361                continue;
362            }
363
364            for transaction_header in block.body().transactions().as_slice() {
365                if !account_ids.contains(&transaction_header.account_id()) {
366                    continue;
367                }
368
369                let erased_output_notes = self.erased_notes.read().clone();
370
371                transactions.push(TransactionRecord {
372                    block_num: block_number,
373                    transaction_header: transaction_header.clone(),
374                    output_notes: vec![],
375                    erased_output_notes,
376                    consumed_note_refs: vec![],
377                });
378            }
379        }
380
381        transactions
382    }
383
384    /// Retrieves storage map updates in a given block range.
385    ///
386    /// This method tries to simulate pagination of the real node.
387    fn get_sync_storage_maps_request(
388        &self,
389        block_from: BlockNumber,
390        block_to: BlockNumber,
391        account_id: AccountId,
392    ) -> (BlockNumber, BlockNumber, BTreeMap<StorageSlotName, StorageMapPatchEntries>) {
393        let chain_tip = self.get_chain_tip_block_num();
394        let target_block = block_to.min(chain_tip);
395
396        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
397            .min(target_block.as_u32())
398            .into();
399
400        // Blocks are iterated in ascending order, so later blocks win per `(slot, key)`.
401        let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
402        for block in self.mock_chain.read().proven_blocks() {
403            let block_number = block.header().block_num();
404            // Only include blocks in range [block_from, page_end_block]
405            if block_number < block_from || block_number > page_end_block {
406                continue;
407            }
408
409            for update in block
410                .body()
411                .updated_accounts()
412                .iter()
413                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
414            {
415                let AccountUpdateDetails::Public(patch) = update.details().clone() else {
416                    continue;
417                };
418
419                for (slot_name, map_patch) in patch.storage().maps() {
420                    if let Some(entries) = map_patch.entries() {
421                        map_entries
422                            .entry(slot_name.clone())
423                            .or_default()
424                            .as_map_mut()
425                            .extend(entries.as_map().clone());
426                    }
427                }
428            }
429        }
430
431        (chain_tip, page_end_block, map_entries)
432    }
433
434    pub fn get_available_notes(&self) -> Vec<MockChainNote> {
435        self.mock_chain.read().committed_notes().values().cloned().collect()
436    }
437
438    pub fn get_public_available_notes(&self) -> Vec<MockChainNote> {
439        self.mock_chain
440            .read()
441            .committed_notes()
442            .values()
443            .filter(|n| matches!(n, MockChainNote::Public(_, _)))
444            .cloned()
445            .collect()
446    }
447
448    pub fn advance_blocks(&self, num_blocks: u32) {
449        let mut mock_chain = self.mock_chain.write();
450        let block_num = mock_chain.latest_block_header().block_num();
451        let snapshot = Arc::new(mock_chain.clone());
452        mock_chain.prove_until_block(block_num + num_blocks).unwrap();
453        self.historical_chains.write().insert(block_num, snapshot);
454    }
455}
456#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
457#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
458impl NodeRpcClient for MockRpcApi {
459    /// Always reports the commitment as unset, unlike a real client.
460    ///
461    /// A real client's RPC connection is its own, so whoever set the commitment also stored the
462    /// header. Tests share one mock across clients with separate stores, where a commitment set by
463    /// the first would stop every later client from storing genesis at all.
464    fn has_genesis_commitment(&self) -> Option<Word> {
465        None
466    }
467
468    async fn set_genesis_commitment(&self, _commitment: Word) -> Result<(), RpcError> {
469        // The mock sends no request headers, so there is nothing to pin the commitment to.
470        Ok(())
471    }
472
473    /// Returns note updates in the inclusive block range `[block_from, block_to]`. Only notes that
474    /// match the provided tags will be returned, grouped by block.
475    async fn sync_notes(
476        &self,
477        block_from: BlockNumber,
478        block_to: BlockNumber,
479        note_tags: &BTreeSet<NoteTag>,
480    ) -> Result<Vec<SyncNotesBlock>, RpcError> {
481        let mut blocks_with_notes: BTreeMap<BlockNumber, BTreeMap<NoteId, CommittedNote>> =
482            BTreeMap::new();
483        for note in self.mock_chain.read().committed_notes().values() {
484            let note_block = note.inclusion_proof().location().block_num();
485            if note_tags.contains(&note.metadata().tag())
486                && note_block >= block_from
487                && note_block <= block_to
488            {
489                let mut committed =
490                    CommittedNote::new(note.id(), *note.metadata(), note.inclusion_proof().clone());
491                // Mirror the node: a single-word attachment is sent verbatim and the record is
492                // complete. A larger one is sent as a commitment only.
493                let attachments = note.attachments();
494                if attachments.iter().all(|attachment| attachment.num_words() == 1) {
495                    committed = committed
496                        .with_attachments(attachments.clone())
497                        .expect("the note's own attachments match its commitment");
498                }
499                blocks_with_notes.entry(note_block).or_default().insert(note.id(), committed);
500            }
501        }
502
503        Ok(blocks_with_notes
504            .into_iter()
505            .map(|(bn, notes)| {
506                let block_header = self.get_block_by_num(bn);
507                let mmr_path =
508                    self.sync_notes_mmr_path_overrides.read().get(&bn).cloned().unwrap_or_else(
509                        || self.get_mmr().open(bn.as_usize()).unwrap().merkle_path().clone(),
510                    );
511                SyncNotesBlock { block_header, mmr_path, notes }
512            })
513            .collect())
514    }
515
516    async fn sync_chain_mmr(
517        &self,
518        current_block_height: BlockNumber,
519        upper_bound: SyncTarget,
520    ) -> Result<ChainMmrInfo, RpcError> {
521        let chain_tip = self.get_chain_tip_block_num();
522        // The mock chain doesn't distinguish committed vs proven tips.
523        let target_block = match upper_bound {
524            SyncTarget::CommittedChainTip | SyncTarget::ProvenChainTip => chain_tip,
525        };
526
527        let from_forest = if current_block_height == target_block {
528            target_block.as_usize()
529        } else {
530            current_block_height.as_u32() as usize + 1
531        };
532
533        let mmr_delta = self
534            .get_mmr()
535            .get_delta(
536                Forest::new(from_forest).unwrap(),
537                Forest::new(target_block.as_usize()).unwrap(),
538            )
539            .unwrap();
540
541        let block_header = self.get_block_by_num(target_block);
542        let block_signatures = self
543            .mock_chain
544            .read()
545            .proven_blocks()
546            .iter()
547            .find(|block| block.header().block_num() == target_block)
548            .expect("the mock chain contains the target block")
549            .signatures()
550            .clone();
551
552        // Mirrors the node: send the configuration when the caller starts at genesis, or when the
553        // commitment changed over the range. A caller already at the target gets nothing.
554        let protocol_config = if current_block_height == BlockNumber::GENESIS {
555            Some(self.protocol_config())
556        } else if current_block_height == target_block {
557            None
558        } else {
559            let commitment_at_start =
560                self.get_block_by_num(current_block_height).protocol_config_commitment();
561            (commitment_at_start != block_header.protocol_config_commitment())
562                .then(|| self.protocol_config())
563        };
564
565        Ok(ChainMmrInfo {
566            block_from: current_block_height,
567            block_to: target_block,
568            mmr_delta,
569            block_header,
570            protocol_config,
571            block_signatures,
572        })
573    }
574
575    /// Retrieves the block header for the specified block number. If the block number is not
576    /// provided, the chain tip block header will be returned.
577    async fn get_block_header_by_number(
578        &self,
579        block_num: Option<BlockNumber>,
580        include_mmr_proof: bool,
581    ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
582        self.block_header_requests
583            .write()
584            .push(BlockHeaderRequest { block_num, include_mmr_proof });
585
586        let block = if let Some(block_num) = block_num {
587            self.mock_chain.read().block_header(block_num.as_usize())
588        } else {
589            self.mock_chain.read().latest_block_header()
590        };
591
592        let mmr_proof = if include_mmr_proof {
593            Some(self.get_mmr().open(block_num.unwrap().as_usize()).unwrap())
594        } else {
595            None
596        };
597
598        Ok((block, mmr_proof))
599    }
600
601    /// Returns the node's tracked notes that match the provided note IDs.
602    async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
603        self.get_notes_by_id_calls.fetch_add(1, Ordering::Relaxed);
604
605        let notes = self.mock_chain.read().committed_notes().clone();
606
607        let hit_notes = note_ids.iter().filter_map(|id| notes.get(id));
608        let mut return_notes = vec![];
609        for note in hit_notes {
610            let fetched_note = match note {
611                MockChainNote::Private(note_id, note_metadata, _, note_inclusion_proof) => {
612                    let attachments = self
613                        .private_note_attachments
614                        .read()
615                        .get(note_id)
616                        .cloned()
617                        .unwrap_or_else(NoteAttachments::empty);
618                    FetchedNote::Private(
619                        *note_id,
620                        *note_metadata,
621                        attachments,
622                        note_inclusion_proof.clone(),
623                    )
624                },
625                MockChainNote::Public(note, note_inclusion_proof) => {
626                    FetchedNote::Public(note.clone(), note_inclusion_proof.clone())
627                },
628            };
629            return_notes.push(fetched_note);
630        }
631        Ok(return_notes)
632    }
633
634    /// The mock does not serve the encryption key. Verifying an attestation needs a validator
635    /// signature the mock chain cannot produce, so tests that submit transactions seed the key
636    /// directly through `Client::seed_transaction_encryption_key` instead.
637    async fn get_transaction_encryption_key(
638        &self,
639    ) -> Result<AttestedTransactionEncryptionKey, RpcError> {
640        Err(RpcError::TransactionEncryptionKeyRejected(
641            "the mock RPC client does not serve a transaction encryption key".into(),
642        ))
643    }
644
645    /// Simulates the submission of a proven transaction to the node. This will create a new block
646    /// just for the new transaction and return the block number of the newly created block.
647    async fn submit_proven_transaction(
648        &self,
649        proven_transaction: &ProvenTransaction,
650        _sealed_transaction_inputs: SealedTransactionInputs, /* Unnecessary for testing client
651                                                              * itself. */
652    ) -> Result<BlockNumber, RpcError> {
653        if let Some(error) = self.take_failure(RpcEndpoint::SubmitProvenTx) {
654            return Err(error);
655        }
656
657        // Record private-note attachment content the way a real node does: attachments are stored
658        // on-chain even for private notes, so `get_notes_by_id` must be able to serve them. The
659        // mock chain itself only keeps private note headers.
660        for note in proven_transaction.output_notes().iter() {
661            if let OutputNote::Private(private_note) = note
662                && !private_note.attachments().is_empty()
663            {
664                self.private_note_attachments
665                    .write()
666                    .insert(private_note.id(), private_note.attachments().clone());
667            }
668        }
669
670        {
671            let mut mock_chain = self.mock_chain.write();
672            mock_chain.add_pending_proven_transaction(proven_transaction.clone());
673        };
674
675        let block_num = self.get_chain_tip_block_num();
676
677        Ok(block_num)
678    }
679
680    /// Simulates the submission of a proven batch to the node by adding it to the mock chain's
681    /// pending batches. The `proposed_batch` argument is accepted to match the trait signature but
682    /// is unused: the mock relies on the `ProvenBatch` alone. The sealed inputs are recorded rather
683    /// than decrypted, so a test can inspect what each attempt sent.
684    async fn submit_proven_batch(
685        &self,
686        proven_batch: &ProvenBatch,
687        _proposed_batch: &ProposedBatch,
688        sealed_transaction_inputs: Vec<SealedTransactionInputs>,
689    ) -> Result<BlockNumber, RpcError> {
690        // Recorded before the staged failure is served: a submission whose response is lost still
691        // reached the node, so a test can compare what that attempt sent against the retry.
692        self.submitted_batch_sealed_inputs.write().push(sealed_transaction_inputs);
693
694        if let Some(error) = self.take_failure(RpcEndpoint::SubmitProvenBatch) {
695            return Err(error);
696        }
697
698        let mut mock_chain = self.mock_chain.write();
699        mock_chain.add_pending_batch(proven_batch.clone());
700        drop(mock_chain);
701
702        let block_num = self.get_chain_tip_block_num();
703
704        Ok(block_num)
705    }
706
707    /// Returns the account proof for the specified account. The `known_code` and `vault` fields are
708    /// ignored: full account data is returned, with truncation flags set when it exceeds
709    /// `oversize_threshold`.
710    async fn get_account(
711        &self,
712        account_id: AccountId,
713        request: GetAccountRequest,
714    ) -> Result<(BlockNumber, AccountProof), RpcError> {
715        self.get_account_calls.fetch_add(1, Ordering::Relaxed);
716
717        if let Some(error) = self.take_failure(RpcEndpoint::GetAccount) {
718            return Err(error);
719        }
720
721        let current_chain = self.mock_chain.read();
722        let current_block_number = current_chain.latest_block_header().block_num();
723        let block_number = match request.at {
724            AccountStateAt::Block(number) => number,
725            AccountStateAt::ChainTip => current_block_number,
726        };
727        let historical_chain = match request.at {
728            AccountStateAt::Block(_) if block_number != current_block_number => Some(
729                self.historical_chains.read().get(&block_number).cloned().ok_or_else(|| {
730                    RpcError::InvalidResponse(alloc::format!(
731                        "no mock chain snapshot at block {block_number}"
732                    ))
733                })?,
734            ),
735            AccountStateAt::ChainTip | AccountStateAt::Block(_) => None,
736        };
737        let mock_chain = historical_chain.as_deref().unwrap_or(&*current_chain);
738
739        let headers = if account_id.is_public() {
740            let account = mock_chain.committed_account(account_id).unwrap();
741
742            // `All` enumerates the account's map slots directly — the mock can introspect the
743            // account, so it simulates the (not-yet-on-the-wire) "all storage maps" request. A slot
744            // maps to the keys requested for it, empty meaning "every entry".
745            let requested_slots: Vec<(StorageSlotName, Vec<StorageMapKey>)> = match &request.storage
746            {
747                StorageMapFetch::Skip => Vec::new(),
748                StorageMapFetch::Slots(reqs) => {
749                    reqs.inner().iter().map(|(name, keys)| (name.clone(), keys.clone())).collect()
750                },
751                StorageMapFetch::All => account
752                    .storage()
753                    .to_header()
754                    .slots()
755                    .filter(|slot| slot.slot_type() == StorageSlotType::Map)
756                    .map(|slot| (slot.name().clone(), Vec::new()))
757                    .collect(),
758            };
759
760            let mut map_details = vec![];
761            for (slot_name, requested_keys) in &requested_slots {
762                if let Some(StorageSlotContent::Map(storage_map)) =
763                    account.storage().get(slot_name).map(StorageSlot::content)
764                {
765                    // Mirror the node: named keys come back as one partial SMT covering them, and
766                    // an empty key list comes back as the whole map, or as `LimitExceeded` once it
767                    // grows past the threshold.
768                    let entries = if requested_keys.is_empty() {
769                        let entries: Vec<StorageMapEntry> = storage_map
770                            .entries()
771                            .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
772                            .collect();
773
774                        if entries.len() > self.oversize_threshold {
775                            StorageMapEntries::LimitExceeded
776                        } else {
777                            StorageMapEntries::AllEntries(entries)
778                        }
779                    } else {
780                        let partial_smt = PartialSmt::from_proofs(
781                            requested_keys.iter().map(|key| storage_map.open(key).into()),
782                        )
783                        .expect("proofs from one map share a root");
784
785                        StorageMapEntries::PartialMap {
786                            map_keys: requested_keys.clone(),
787                            partial_smt,
788                        }
789                    };
790
791                    map_details
792                        .push(AccountStorageMapDetails { slot_name: slot_name.clone(), entries });
793                } else {
794                    panic!("Storage slot {slot_name} is not a map");
795                }
796            }
797
798            let storage_details = AccountStorageDetails {
799                header: account.storage().to_header(),
800                map_details,
801            };
802
803            // Mirror the node: `Skip` sends no assets, and `IfChangedFrom` omits them when the
804            // account's vault root already equals the sent commitment.
805            let include_assets = match request.vault {
806                VaultFetch::Skip => false,
807                VaultFetch::Always => true,
808                VaultFetch::IfChangedFrom(root) => root != account.vault().root(),
809            };
810            let mut assets = vec![];
811            if include_assets {
812                for asset in account.vault().assets() {
813                    assets.push(asset);
814                }
815            }
816            let vault_details = AccountVaultDetails {
817                too_many_assets: assets.len() > self.oversize_threshold,
818                assets,
819            };
820
821            Some(AccountDetails {
822                header: account.into(),
823                storage_details,
824                code: account.code().clone(),
825                vault_details,
826            })
827        } else {
828            None
829        };
830
831        let witness = mock_chain.account_tree().open(account_id);
832
833        let proof = AccountProof::new(witness, headers).unwrap();
834
835        Ok((block_number, proof))
836    }
837
838    async fn register_account(
839        &self,
840        invitation_code: &str,
841        account_id: AccountId,
842    ) -> Result<(), RpcError> {
843        if let Some(error) = self.take_failure(RpcEndpoint::RegisterAccount) {
844            return Err(error);
845        }
846
847        // The mock holds no invitations, so it accepts any code and records the pair. Stage a
848        // failure with `fail_next_call` to exercise a rejection.
849        self.registered_accounts
850            .write()
851            .insert(account_id, String::from(invitation_code));
852
853        Ok(())
854    }
855
856    async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, RpcError> {
857        self.is_account_allowed_calls.fetch_add(1, Ordering::Relaxed);
858
859        if let Some(error) = self.take_failure(RpcEndpoint::IsAccountAllowed) {
860            return Err(error);
861        }
862
863        // A node that does not enforce the allowlist allows every account. Call
864        // `enforce_account_allowlist` to answer from the recorded registrations instead.
865        if !self.allowlist_enforced.load(Ordering::SeqCst) {
866            return Ok(true);
867        }
868
869        Ok(self.registered_accounts.read().contains_key(&account_id))
870    }
871
872    /// Returns the nullifiers created after the specified block number that match the provided
873    /// prefixes.
874    async fn sync_nullifiers(
875        &self,
876        prefixes: &[u16],
877        block_from: BlockNumber,
878        block_to: BlockNumber,
879    ) -> Result<Vec<NullifierUpdate>, RpcError> {
880        let nullifiers = self
881            .mock_chain
882            .read()
883            .nullifier_tree()
884            .entries()
885            .filter_map(|(nullifier, block_num)| {
886                let within_range = block_num >= block_from && block_num <= block_to;
887
888                if prefixes.contains(&nullifier.prefix()) && within_range {
889                    Some(NullifierUpdate { nullifier, block_num })
890                } else {
891                    None
892                }
893            })
894            .collect::<Vec<_>>();
895
896        Ok(nullifiers)
897    }
898
899    async fn get_block_by_number(
900        &self,
901        block_num: BlockNumber,
902        include_proof: bool,
903    ) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError> {
904        let block = self
905            .mock_chain
906            .read()
907            .proven_blocks()
908            .iter()
909            .find(|b| b.header().block_num() == block_num)
910            .unwrap()
911            .clone();
912        let (header, body, signatures, proof) = block.into_parts();
913
914        Ok((
915            SignedBlock::new_unchecked(header, body, signatures),
916            include_proof.then_some(proof),
917        ))
918    }
919
920    async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
921        let script = self
922            .get_available_notes()
923            .iter()
924            .filter_map(|note| note.note())
925            .find(|n| Word::from(n.script().root()) == root)
926            .map(|n| n.script().clone());
927
928        Ok(script)
929    }
930
931    async fn sync_storage_maps(
932        &self,
933        block_from: BlockNumber,
934        block_to: BlockNumber,
935        account_id: AccountId,
936    ) -> Result<StorageMapInfo, RpcError> {
937        let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
938        let mut current_block_from = block_from;
939        let chain_tip = self.get_chain_tip_block_num();
940        let target_block = block_to.min(chain_tip);
941
942        loop {
943            let (page_chain_tip, page_block_number, page_entries) =
944                self.get_sync_storage_maps_request(current_block_from, block_to, account_id);
945            for (slot_name, entries) in page_entries {
946                map_entries
947                    .entry(slot_name)
948                    .or_default()
949                    .as_map_mut()
950                    .extend(entries.into_map());
951            }
952
953            if page_block_number >= target_block {
954                return Ok(StorageMapInfo {
955                    chain_tip: page_chain_tip,
956                    block_number: page_block_number,
957                    map_entries,
958                });
959            }
960
961            current_block_from = (page_block_number.as_u32() + 1).into();
962        }
963    }
964
965    async fn sync_account_vault(
966        &self,
967        block_from: BlockNumber,
968        block_to: BlockNumber,
969        account_id: AccountId,
970    ) -> Result<AccountVaultInfo, RpcError> {
971        let mut vault_patch = AccountVaultPatch::default();
972        let mut current_block_from = block_from;
973        let chain_tip = self.get_chain_tip_block_num();
974        let target_block = block_to.min(chain_tip);
975
976        loop {
977            let (page_chain_tip, page_block_number, page_patch) =
978                self.get_sync_account_vault_request(current_block_from, block_to, account_id);
979            vault_patch.merge(page_patch);
980
981            if page_block_number >= target_block {
982                return Ok(AccountVaultInfo {
983                    chain_tip: page_chain_tip,
984                    block_number: page_block_number,
985                    vault_patch,
986                });
987            }
988
989            current_block_from = (page_block_number.as_u32() + 1).into();
990        }
991    }
992
993    async fn sync_transactions(
994        &self,
995        block_from: BlockNumber,
996        block_to: BlockNumber,
997        account_ids: Vec<AccountId>,
998    ) -> Result<Vec<TransactionRecord>, RpcError> {
999        Ok(self.get_sync_transactions_request(block_from, block_to, &account_ids))
1000    }
1001
1002    async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
1003        Ok(NetworkId::Testnet)
1004    }
1005
1006    async fn get_rpc_limits(&self) -> Result<crate::rpc::RpcLimits, RpcError> {
1007        Ok(crate::rpc::RpcLimits::default())
1008    }
1009
1010    fn has_rpc_limits(&self) -> Option<crate::rpc::RpcLimits> {
1011        None
1012    }
1013
1014    async fn set_rpc_limits(&self, _limits: crate::rpc::RpcLimits) {
1015        // No-op for mock client
1016    }
1017
1018    async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
1019        Ok(RpcStatusInfo {
1020            version: env!("CARGO_PKG_VERSION").into(),
1021            genesis_commitment: None,
1022            chain_tip: 0,
1023            block_producer: None,
1024        })
1025    }
1026
1027    async fn get_network_note_status(
1028        &self,
1029        _note_id: NoteId,
1030    ) -> Result<NetworkNoteStatusInfo, RpcError> {
1031        todo!("We need to check if we want to implement this for the mockchain");
1032    }
1033}
1034
1035// CONVERSIONS
1036// ================================================================================================
1037
1038impl From<MockChain> for MockRpcApi {
1039    fn from(mock_chain: MockChain) -> Self {
1040        MockRpcApi::new(mock_chain)
1041    }
1042}
1043
1044// HELPERS
1045// ================================================================================================
1046
1047fn build_account_updates(
1048    mock_chain: &MockChain,
1049) -> BTreeMap<BlockNumber, BTreeMap<AccountId, Word>> {
1050    let mut account_commitment_updates = BTreeMap::new();
1051    for block in mock_chain.proven_blocks() {
1052        let block_num = block.header().block_num();
1053        let mut updates = BTreeMap::new();
1054
1055        for update in block.body().updated_accounts() {
1056            updates.insert(update.account_id(), update.final_state_commitment());
1057        }
1058
1059        if updates.is_empty() {
1060            continue;
1061        }
1062
1063        account_commitment_updates.insert(block_num, updates);
1064    }
1065    account_commitment_updates
1066}