Skip to main content

miden_client/test_utils/
mock.rs

1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use miden_protocol::Word;
7use miden_protocol::account::delta::AccountUpdateDetails;
8use miden_protocol::account::{AccountCode, AccountId, StorageSlot, StorageSlotContent};
9use miden_protocol::address::NetworkId;
10use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
11use miden_protocol::crypto::merkle::mmr::{Forest, Mmr, MmrProof};
12use miden_protocol::crypto::merkle::smt::SmtProof;
13use miden_protocol::note::{NoteHeader, NoteId, NoteScript, NoteTag, Nullifier};
14use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};
15use miden_testing::{MockChain, MockChainNote};
16use miden_tx::utils::sync::RwLock;
17
18use crate::Client;
19use crate::rpc::domain::account::{
20    AccountDetails,
21    AccountProof,
22    AccountStorageDetails,
23    AccountStorageMapDetails,
24    AccountStorageRequirements,
25    AccountUpdateSummary,
26    AccountVaultDetails,
27    FetchedAccount,
28    StorageMapEntries,
29    StorageMapEntry,
30};
31use crate::rpc::domain::account_vault::{AccountVaultInfo, AccountVaultUpdate};
32use crate::rpc::domain::note::{
33    CommittedNote,
34    CommittedNoteMetadata,
35    FetchedNote,
36    NoteSyncBlock,
37    NoteSyncInfo,
38};
39use crate::rpc::domain::nullifier::NullifierUpdate;
40use crate::rpc::domain::storage_map::{StorageMapInfo, StorageMapUpdate};
41use crate::rpc::domain::sync::ChainMmrInfo;
42use crate::rpc::domain::transaction::{TransactionRecord, TransactionsInfo};
43use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError, RpcStatusInfo};
44
45pub type MockClient<AUTH> = Client<AUTH>;
46
47/// Mock RPC API
48///
49/// This struct implements the RPC API used by the client to communicate with the node. It simulates
50/// most of the functionality of the actual node, with some small differences:
51/// - It uses a [`MockChain`] to simulate the blockchain state.
52/// - Blocks are not automatically created after time passes, but rather new blocks are created when
53///   calling the `prove_block` method.
54/// - Network account and transactions aren't supported in the current version.
55/// - Account update block numbers aren't tracked, so any endpoint that returns when certain account
56///   updates were made will return the chain tip block number instead.
57#[derive(Clone)]
58pub struct MockRpcApi {
59    account_commitment_updates: Arc<RwLock<BTreeMap<BlockNumber, BTreeMap<AccountId, Word>>>>,
60    pub mock_chain: Arc<RwLock<MockChain>>,
61    oversize_threshold: usize,
62    /// Note headers to report as erased in sync transaction responses.
63    erased_notes: Arc<RwLock<Vec<NoteHeader>>>,
64}
65
66impl Default for MockRpcApi {
67    fn default() -> Self {
68        Self::new(MockChain::new())
69    }
70}
71
72impl MockRpcApi {
73    // Constant to use in mocked pagination.
74    const PAGINATION_BLOCK_LIMIT: u32 = 5;
75
76    /// Creates a new [`MockRpcApi`] instance with the state of the provided [`MockChain`].
77    pub fn new(mock_chain: MockChain) -> Self {
78        Self {
79            account_commitment_updates: Arc::new(RwLock::new(build_account_updates(&mock_chain))),
80            mock_chain: Arc::new(RwLock::new(mock_chain)),
81            oversize_threshold: 1000,
82            erased_notes: Arc::new(RwLock::new(Vec::new())),
83        }
84    }
85
86    /// Sets the oversize threshold for `get_account_proof`. Any storage map with more
87    /// entries than this threshold, or a vault with more assets, will have the
88    /// `too_many_entries` / `too_many_assets` flags set in the response.
89    #[must_use]
90    pub fn with_oversize_threshold(mut self, threshold: usize) -> Self {
91        self.oversize_threshold = threshold;
92        self
93    }
94
95    /// Registers a note header to be reported as erased in subsequent sync transaction responses.
96    pub fn mark_note_as_erased(&self, header: NoteHeader) {
97        self.erased_notes.write().push(header);
98    }
99
100    /// Returns the current MMR of the blockchain.
101    pub fn get_mmr(&self) -> Mmr {
102        self.mock_chain.read().blockchain().as_mmr().clone()
103    }
104
105    /// Returns the chain tip block number.
106    pub fn get_chain_tip_block_num(&self) -> BlockNumber {
107        self.mock_chain.read().latest_block_header().block_num()
108    }
109
110    /// Advances the mock chain by proving the next block, committing all pending objects to the
111    /// chain in the process.
112    pub fn prove_block(&self) {
113        let proven_block = self.mock_chain.write().prove_next_block().unwrap();
114        let mut account_commitment_updates = self.account_commitment_updates.write();
115        let block_num = proven_block.header().block_num();
116        let updates: BTreeMap<AccountId, Word> = proven_block
117            .body()
118            .updated_accounts()
119            .iter()
120            .map(|update| (update.account_id(), update.final_state_commitment()))
121            .collect();
122
123        if !updates.is_empty() {
124            account_commitment_updates.insert(block_num, updates);
125        }
126    }
127
128    /// Retrieves a block by its block number.
129    fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
130        self.mock_chain.read().block_header(block_num.as_usize())
131    }
132
133    /// Retrieves account vault updates in a given block range.
134    /// This method tries to simulate pagination by limiting the number of blocks processed per
135    /// request.
136    fn get_sync_account_vault_request(
137        &self,
138        block_from: BlockNumber,
139        block_to: Option<BlockNumber>,
140        account_id: AccountId,
141    ) -> AccountVaultInfo {
142        let chain_tip = self.get_chain_tip_block_num();
143        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
144
145        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
146            .min(target_block.as_u32())
147            .into();
148
149        let mut updates = vec![];
150        for block in self.mock_chain.read().proven_blocks() {
151            let block_number = block.header().block_num();
152            // Only include blocks in range (block_from, page_end_block]
153            if block_number <= block_from || block_number > page_end_block {
154                continue;
155            }
156
157            for update in block
158                .body()
159                .updated_accounts()
160                .iter()
161                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
162            {
163                let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
164                    continue;
165                };
166
167                let vault_delta = account_delta.vault();
168
169                for asset in vault_delta.added_assets() {
170                    let account_vault_update = AccountVaultUpdate {
171                        block_num: block_number,
172                        asset: Some(asset),
173                        vault_key: asset.vault_key(),
174                    };
175                    updates.push(account_vault_update);
176                }
177            }
178        }
179
180        AccountVaultInfo {
181            chain_tip,
182            block_number: page_end_block,
183            updates,
184        }
185    }
186
187    /// Retrieves transactions in a given block range that match the provided account IDs
188    fn get_sync_transactions_request(
189        &self,
190        block_from: BlockNumber,
191        block_to: Option<BlockNumber>,
192        account_ids: &[AccountId],
193    ) -> TransactionsInfo {
194        let chain_tip = self.get_chain_tip_block_num();
195        let block_to = match block_to {
196            Some(block_to) => block_to,
197            None => chain_tip,
198        };
199
200        let mut transaction_records = vec![];
201        for block in self.mock_chain.read().proven_blocks() {
202            let block_number = block.header().block_num();
203            if block_number <= block_from || block_number > block_to {
204                continue;
205            }
206
207            for transaction_header in block.body().transactions().as_slice() {
208                if !account_ids.contains(&transaction_header.account_id()) {
209                    continue;
210                }
211
212                let erased_output_notes = self.erased_notes.read().clone();
213
214                transaction_records.push(TransactionRecord {
215                    block_num: block_number,
216                    transaction_header: transaction_header.clone(),
217                    output_notes: vec![],
218                    erased_output_notes,
219                });
220            }
221        }
222
223        TransactionsInfo {
224            chain_tip,
225            block_num: block_to,
226            transaction_records,
227        }
228    }
229
230    /// Retrieves storage map updates in a given block range.
231    ///
232    /// This method tries to simulate pagination of the real node.
233    fn get_sync_storage_maps_request(
234        &self,
235        block_from: BlockNumber,
236        block_to: Option<BlockNumber>,
237        account_id: AccountId,
238    ) -> StorageMapInfo {
239        let chain_tip = self.get_chain_tip_block_num();
240        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
241
242        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
243            .min(target_block.as_u32())
244            .into();
245
246        let mut updates = vec![];
247        for block in self.mock_chain.read().proven_blocks() {
248            let block_number = block.header().block_num();
249            if block_number <= block_from || block_number > page_end_block {
250                continue;
251            }
252
253            for update in block
254                .body()
255                .updated_accounts()
256                .iter()
257                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
258            {
259                let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
260                    continue;
261                };
262
263                let storage_delta = account_delta.storage();
264
265                for (slot_name, map_delta) in storage_delta.maps() {
266                    for (key, value) in map_delta.entries() {
267                        let storage_map_info = StorageMapUpdate {
268                            block_num: block_number,
269                            slot_name: slot_name.clone(),
270                            key: *key,
271                            value: *value,
272                        };
273                        updates.push(storage_map_info);
274                    }
275                }
276            }
277        }
278
279        StorageMapInfo {
280            chain_tip,
281            block_number: page_end_block,
282            updates,
283        }
284    }
285
286    pub fn get_available_notes(&self) -> Vec<MockChainNote> {
287        self.mock_chain.read().committed_notes().values().cloned().collect()
288    }
289
290    pub fn get_public_available_notes(&self) -> Vec<MockChainNote> {
291        self.mock_chain
292            .read()
293            .committed_notes()
294            .values()
295            .filter(|n| matches!(n, MockChainNote::Public(_, _)))
296            .cloned()
297            .collect()
298    }
299
300    pub fn get_private_available_notes(&self) -> Vec<MockChainNote> {
301        self.mock_chain
302            .read()
303            .committed_notes()
304            .values()
305            .filter(|n| matches!(n, MockChainNote::Private(_, _, _)))
306            .cloned()
307            .collect()
308    }
309
310    pub fn advance_blocks(&self, num_blocks: u32) {
311        let current_height = self.get_chain_tip_block_num();
312        let mut mock_chain = self.mock_chain.write();
313        mock_chain.prove_until_block(current_height + num_blocks).unwrap();
314    }
315}
316#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
317#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
318impl NodeRpcClient for MockRpcApi {
319    fn has_genesis_commitment(&self) -> Option<Word> {
320        None
321    }
322
323    async fn set_genesis_commitment(&self, _commitment: Word) -> Result<(), RpcError> {
324        // The mock client doesn't use accept headers, so we don't need to do anything here.
325        Ok(())
326    }
327
328    /// Returns note updates after the specified block number. Only notes that match the
329    /// provided tags will be returned, grouped by block.
330    async fn sync_notes(
331        &self,
332        block_num: BlockNumber,
333        block_to: Option<BlockNumber>,
334        note_tags: &BTreeSet<NoteTag>,
335    ) -> Result<NoteSyncInfo, RpcError> {
336        let chain_tip = self.get_chain_tip_block_num();
337        let upper_bound = block_to.unwrap_or(chain_tip);
338
339        // Collect all blocks with matching notes in the range (block_num, upper_bound]
340        let mut blocks_with_notes: BTreeMap<BlockNumber, BTreeMap<NoteId, CommittedNote>> =
341            BTreeMap::new();
342        for note in self.mock_chain.read().committed_notes().values() {
343            let note_block = note.inclusion_proof().location().block_num();
344            if note_tags.contains(&note.metadata().tag())
345                && note_block > block_num
346                && note_block <= upper_bound
347            {
348                let committed = CommittedNote::new(
349                    note.id(),
350                    CommittedNoteMetadata::Full(note.metadata().clone()),
351                    note.inclusion_proof().clone(),
352                );
353                blocks_with_notes.entry(note_block).or_default().insert(note.id(), committed);
354            }
355        }
356
357        // Always include the upper_bound block (with empty notes if needed), matching the
358        // node behavior where the range-end block is always present when the scan completes.
359        blocks_with_notes.entry(upper_bound).or_default();
360
361        let blocks: Vec<NoteSyncBlock> = blocks_with_notes
362            .into_iter()
363            .map(|(bn, notes)| {
364                let block_header = self.get_block_by_num(bn);
365                let mmr_path = self.get_mmr().open(bn.as_usize()).unwrap().merkle_path().clone();
366                NoteSyncBlock { block_header, mmr_path, notes }
367            })
368            .collect();
369
370        Ok(NoteSyncInfo { chain_tip, block_to: upper_bound, blocks })
371    }
372
373    async fn sync_chain_mmr(
374        &self,
375        block_from: BlockNumber,
376        block_to: Option<BlockNumber>,
377    ) -> Result<ChainMmrInfo, RpcError> {
378        let chain_tip = self.get_chain_tip_block_num();
379        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
380
381        let from_forest = if block_from == chain_tip {
382            target_block.as_usize()
383        } else {
384            block_from.as_u32() as usize + 1
385        };
386
387        let mmr_delta = self
388            .get_mmr()
389            .get_delta(Forest::new(from_forest), Forest::new(target_block.as_usize()))
390            .unwrap();
391
392        let block_header = self.get_block_by_num(target_block);
393
394        Ok(ChainMmrInfo {
395            block_from,
396            block_to: target_block,
397            mmr_delta,
398            block_header,
399        })
400    }
401
402    /// Retrieves the block header for the specified block number. If the block number is not
403    /// provided, the chain tip block header will be returned.
404    async fn get_block_header_by_number(
405        &self,
406        block_num: Option<BlockNumber>,
407        include_mmr_proof: bool,
408    ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
409        let block = if let Some(block_num) = block_num {
410            self.mock_chain.read().block_header(block_num.as_usize())
411        } else {
412            self.mock_chain.read().latest_block_header()
413        };
414
415        let mmr_proof = if include_mmr_proof {
416            Some(self.get_mmr().open(block_num.unwrap().as_usize()).unwrap())
417        } else {
418            None
419        };
420
421        Ok((block, mmr_proof))
422    }
423
424    /// Returns the node's tracked notes that match the provided note IDs.
425    async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
426        // assume all public notes for now
427        let notes = self.mock_chain.read().committed_notes().clone();
428
429        let hit_notes = note_ids.iter().filter_map(|id| notes.get(id));
430        let mut return_notes = vec![];
431        for note in hit_notes {
432            let fetched_note = match note {
433                MockChainNote::Private(note_id, note_metadata, note_inclusion_proof) => {
434                    let note_header = NoteHeader::new(*note_id, note_metadata.clone());
435                    FetchedNote::Private(note_header, note_inclusion_proof.clone())
436                },
437                MockChainNote::Public(note, note_inclusion_proof) => {
438                    FetchedNote::Public(note.clone(), note_inclusion_proof.clone())
439                },
440            };
441            return_notes.push(fetched_note);
442        }
443        Ok(return_notes)
444    }
445
446    /// Simulates the submission of a proven transaction to the node. This will create a new block
447    /// just for the new transaction and return the block number of the newly created block.
448    async fn submit_proven_transaction(
449        &self,
450        proven_transaction: ProvenTransaction,
451        _tx_inputs: TransactionInputs, // Unnecessary for testing client itself.
452    ) -> Result<BlockNumber, RpcError> {
453        // TODO: add some basic validations to test error cases
454
455        {
456            let mut mock_chain = self.mock_chain.write();
457            mock_chain.add_pending_proven_transaction(proven_transaction.clone());
458        };
459
460        let block_num = self.get_chain_tip_block_num();
461
462        Ok(block_num)
463    }
464
465    /// Returns the node's tracked account details for the specified account ID.
466    /// Always returns the full account for public accounts.
467    async fn get_account_details(&self, account_id: AccountId) -> Result<FetchedAccount, RpcError> {
468        let summary =
469            self.account_commitment_updates
470                .read()
471                .iter()
472                .rev()
473                .find_map(|(block_num, updates)| {
474                    updates.get(&account_id).map(|commitment| AccountUpdateSummary {
475                        commitment: *commitment,
476                        last_block_num: *block_num,
477                    })
478                });
479
480        if let Ok(account) = self.mock_chain.read().committed_account(account_id) {
481            let summary = summary.unwrap_or_else(|| AccountUpdateSummary {
482                commitment: account.to_commitment(),
483                last_block_num: BlockNumber::GENESIS,
484            });
485            Ok(FetchedAccount::new_public(account.clone(), summary))
486        } else if let Some(summary) = summary {
487            Ok(FetchedAccount::new_private(account_id, summary))
488        } else {
489            Err(RpcError::ExpectedDataMissing(format!(
490                "account {account_id} not found in mock commitment updates or mock chain"
491            )))
492        }
493    }
494
495    /// Returns the account proof for the specified account. The `known_account_code` parameter
496    /// is ignored in the mock implementation and the latest account code is always returned.
497    async fn get_account_proof(
498        &self,
499        account_id: AccountId,
500        account_storage_requirements: AccountStorageRequirements,
501        account_state: AccountStateAt,
502        _known_account_code: Option<AccountCode>,
503        _known_vault_commitment: Option<Word>,
504    ) -> Result<(BlockNumber, AccountProof), RpcError> {
505        let mock_chain = self.mock_chain.read();
506
507        let block_number = match account_state {
508            AccountStateAt::Block(number) => number,
509            AccountStateAt::ChainTip => mock_chain.latest_block_header().block_num(),
510        };
511
512        let headers = if account_id.has_public_state() {
513            let account = mock_chain.committed_account(account_id).unwrap();
514
515            let mut map_details = vec![];
516            for slot_name in account_storage_requirements.inner().keys() {
517                if let Some(StorageSlotContent::Map(storage_map)) =
518                    account.storage().get(slot_name).map(StorageSlot::content)
519                {
520                    let entries: Vec<StorageMapEntry> = storage_map
521                        .entries()
522                        .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
523                        .collect();
524
525                    // NOTE: The mock returns all entries even when too_many_entries is set.
526                    // In production, the node would return partial data for oversized maps.
527                    let too_many_entries = entries.len() > self.oversize_threshold;
528                    let account_storage_map_detail = AccountStorageMapDetails {
529                        slot_name: slot_name.clone(),
530                        too_many_entries,
531                        entries: StorageMapEntries::AllEntries(entries),
532                    };
533
534                    map_details.push(account_storage_map_detail);
535                } else {
536                    panic!("Storage slot {slot_name} is not a map");
537                }
538            }
539
540            let storage_details = AccountStorageDetails {
541                header: account.storage().to_header(),
542                map_details,
543            };
544
545            let mut assets = vec![];
546            for asset in account.vault().assets() {
547                assets.push(asset);
548            }
549            let vault_details = AccountVaultDetails {
550                too_many_assets: assets.len() > self.oversize_threshold,
551                assets,
552            };
553
554            Some(AccountDetails {
555                header: account.into(),
556                storage_details,
557                code: account.code().clone(),
558                vault_details,
559            })
560        } else {
561            None
562        };
563
564        let witness = mock_chain.account_tree().open(account_id);
565
566        let proof = AccountProof::new(witness, headers).unwrap();
567
568        Ok((block_number, proof))
569    }
570
571    /// Returns the nullifiers created after the specified block number that match the provided
572    /// prefixes.
573    async fn sync_nullifiers(
574        &self,
575        prefixes: &[u16],
576        from_block_num: BlockNumber,
577        block_to: Option<BlockNumber>,
578    ) -> Result<Vec<NullifierUpdate>, RpcError> {
579        let nullifiers = self
580            .mock_chain
581            .read()
582            .nullifier_tree()
583            .entries()
584            .filter_map(|(nullifier, block_num)| {
585                let within_range = if let Some(to_block) = block_to {
586                    block_num >= from_block_num && block_num <= to_block
587                } else {
588                    block_num >= from_block_num
589                };
590
591                if prefixes.contains(&nullifier.prefix()) && within_range {
592                    Some(NullifierUpdate { nullifier, block_num })
593                } else {
594                    None
595                }
596            })
597            .collect::<Vec<_>>();
598
599        Ok(nullifiers)
600    }
601
602    /// Returns proofs for all the provided nullifiers.
603    async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError> {
604        Ok(nullifiers
605            .iter()
606            .map(|nullifier| self.mock_chain.read().nullifier_tree().open(nullifier).into_proof())
607            .collect())
608    }
609
610    async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError> {
611        let block = self
612            .mock_chain
613            .read()
614            .proven_blocks()
615            .iter()
616            .find(|b| b.header().block_num() == block_num)
617            .unwrap()
618            .clone();
619
620        Ok(block)
621    }
622
623    async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError> {
624        let note = self
625            .get_available_notes()
626            .iter()
627            .find(|note| note.note().is_some_and(|n| n.script().root() == root))
628            .unwrap()
629            .clone();
630
631        Ok(note.note().unwrap().script().clone())
632    }
633
634    async fn sync_storage_maps(
635        &self,
636        block_from: BlockNumber,
637        block_to: Option<BlockNumber>,
638        account_id: AccountId,
639    ) -> Result<StorageMapInfo, RpcError> {
640        let mut all_updates = Vec::new();
641        let mut current_block_from = block_from;
642        let chain_tip = self.get_chain_tip_block_num();
643        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
644
645        loop {
646            let response =
647                self.get_sync_storage_maps_request(current_block_from, block_to, account_id);
648            all_updates.extend(response.updates);
649
650            if response.block_number >= target_block {
651                return Ok(StorageMapInfo {
652                    chain_tip: response.chain_tip,
653                    block_number: response.block_number,
654                    updates: all_updates,
655                });
656            }
657
658            current_block_from = (response.block_number.as_u32() + 1).into();
659        }
660    }
661
662    async fn sync_account_vault(
663        &self,
664        block_from: BlockNumber,
665        block_to: Option<BlockNumber>,
666        account_id: AccountId,
667    ) -> Result<AccountVaultInfo, RpcError> {
668        let mut all_updates = Vec::new();
669        let mut current_block_from = block_from;
670        let chain_tip = self.get_chain_tip_block_num();
671        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
672
673        loop {
674            let response =
675                self.get_sync_account_vault_request(current_block_from, block_to, account_id);
676            all_updates.extend(response.updates);
677
678            if response.block_number >= target_block {
679                return Ok(AccountVaultInfo {
680                    chain_tip: response.chain_tip,
681                    block_number: response.block_number,
682                    updates: all_updates,
683                });
684            }
685
686            current_block_from = (response.block_number.as_u32() + 1).into();
687        }
688    }
689
690    async fn sync_transactions(
691        &self,
692        block_from: BlockNumber,
693        block_to: Option<BlockNumber>,
694        account_ids: Vec<AccountId>,
695    ) -> Result<TransactionsInfo, RpcError> {
696        let response = self.get_sync_transactions_request(block_from, block_to, &account_ids);
697        Ok(response)
698    }
699
700    async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
701        Ok(NetworkId::Testnet)
702    }
703
704    async fn get_rpc_limits(&self) -> Result<crate::rpc::RpcLimits, RpcError> {
705        Ok(crate::rpc::RpcLimits::default())
706    }
707
708    fn has_rpc_limits(&self) -> Option<crate::rpc::RpcLimits> {
709        None
710    }
711
712    async fn set_rpc_limits(&self, _limits: crate::rpc::RpcLimits) {
713        // No-op for mock client
714    }
715
716    async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
717        Ok(RpcStatusInfo {
718            version: env!("CARGO_PKG_VERSION").into(),
719            genesis_commitment: None,
720            store: None,
721            block_producer: None,
722        })
723    }
724}
725
726// CONVERSIONS
727// ================================================================================================
728
729impl From<MockChain> for MockRpcApi {
730    fn from(mock_chain: MockChain) -> Self {
731        MockRpcApi::new(mock_chain)
732    }
733}
734
735// HELPERS
736// ================================================================================================
737
738fn build_account_updates(
739    mock_chain: &MockChain,
740) -> BTreeMap<BlockNumber, BTreeMap<AccountId, Word>> {
741    let mut account_commitment_updates = BTreeMap::new();
742    for block in mock_chain.proven_blocks() {
743        let block_num = block.header().block_num();
744        let mut updates = BTreeMap::new();
745
746        for update in block.body().updated_accounts() {
747            updates.insert(update.account_id(), update.final_state_commitment());
748        }
749
750        if updates.is_empty() {
751            continue;
752        }
753
754        account_commitment_updates.insert(block_num, updates);
755    }
756    account_commitment_updates
757}