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