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