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#[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}
63
64impl Default for MockRpcApi {
65 fn default() -> Self {
66 Self::new(MockChain::new())
67 }
68}
69
70impl MockRpcApi {
71 const PAGINATION_BLOCK_LIMIT: u32 = 5;
73
74 pub fn new(mock_chain: MockChain) -> Self {
76 Self {
77 account_commitment_updates: Arc::new(RwLock::new(build_account_updates(&mock_chain))),
78 mock_chain: Arc::new(RwLock::new(mock_chain)),
79 oversize_threshold: 1000,
80 }
81 }
82
83 #[must_use]
87 pub fn with_oversize_threshold(mut self, threshold: usize) -> Self {
88 self.oversize_threshold = threshold;
89 self
90 }
91
92 pub fn get_mmr(&self) -> Mmr {
94 self.mock_chain.read().blockchain().as_mmr().clone()
95 }
96
97 pub fn get_chain_tip_block_num(&self) -> BlockNumber {
99 self.mock_chain.read().latest_block_header().block_num()
100 }
101
102 pub fn prove_block(&self) {
105 let proven_block = self.mock_chain.write().prove_next_block().unwrap();
106 let mut account_commitment_updates = self.account_commitment_updates.write();
107 let block_num = proven_block.header().block_num();
108 let updates: BTreeMap<AccountId, Word> = proven_block
109 .body()
110 .updated_accounts()
111 .iter()
112 .map(|update| (update.account_id(), update.final_state_commitment()))
113 .collect();
114
115 if !updates.is_empty() {
116 account_commitment_updates.insert(block_num, updates);
117 }
118 }
119
120 fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
122 self.mock_chain.read().block_header(block_num.as_usize())
123 }
124
125 fn get_sync_account_vault_request(
129 &self,
130 block_from: BlockNumber,
131 block_to: Option<BlockNumber>,
132 account_id: AccountId,
133 ) -> AccountVaultInfo {
134 let chain_tip = self.get_chain_tip_block_num();
135 let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
136
137 let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
138 .min(target_block.as_u32())
139 .into();
140
141 let mut updates = vec![];
142 for block in self.mock_chain.read().proven_blocks() {
143 let block_number = block.header().block_num();
144 if block_number <= block_from || block_number > page_end_block {
146 continue;
147 }
148
149 for update in block
150 .body()
151 .updated_accounts()
152 .iter()
153 .filter(|block_acc_update| block_acc_update.account_id() == account_id)
154 {
155 let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
156 continue;
157 };
158
159 let vault_delta = account_delta.vault();
160
161 for asset in vault_delta.added_assets() {
162 let account_vault_update = AccountVaultUpdate {
163 block_num: block_number,
164 asset: Some(asset),
165 vault_key: asset.vault_key(),
166 };
167 updates.push(account_vault_update);
168 }
169 }
170 }
171
172 AccountVaultInfo {
173 chain_tip,
174 block_number: page_end_block,
175 updates,
176 }
177 }
178
179 fn get_sync_transactions_request(
181 &self,
182 block_from: BlockNumber,
183 block_to: Option<BlockNumber>,
184 account_ids: &[AccountId],
185 ) -> TransactionsInfo {
186 let chain_tip = self.get_chain_tip_block_num();
187 let block_to = match block_to {
188 Some(block_to) => block_to,
189 None => chain_tip,
190 };
191
192 let mut transaction_records = vec![];
193 for block in self.mock_chain.read().proven_blocks() {
194 let block_number = block.header().block_num();
195 if block_number <= block_from || block_number > block_to {
196 continue;
197 }
198
199 for transaction_header in block.body().transactions().as_slice() {
200 if !account_ids.contains(&transaction_header.account_id()) {
201 continue;
202 }
203
204 transaction_records.push(TransactionRecord {
205 block_num: block_number,
206 transaction_header: transaction_header.clone(),
207 output_notes: vec![],
208 erased_output_note_ids: vec![],
209 });
210 }
211 }
212
213 TransactionsInfo {
214 chain_tip,
215 block_num: block_to,
216 transaction_records,
217 }
218 }
219
220 fn get_sync_storage_maps_request(
224 &self,
225 block_from: BlockNumber,
226 block_to: Option<BlockNumber>,
227 account_id: AccountId,
228 ) -> StorageMapInfo {
229 let chain_tip = self.get_chain_tip_block_num();
230 let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
231
232 let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
233 .min(target_block.as_u32())
234 .into();
235
236 let mut updates = vec![];
237 for block in self.mock_chain.read().proven_blocks() {
238 let block_number = block.header().block_num();
239 if block_number <= block_from || block_number > page_end_block {
240 continue;
241 }
242
243 for update in block
244 .body()
245 .updated_accounts()
246 .iter()
247 .filter(|block_acc_update| block_acc_update.account_id() == account_id)
248 {
249 let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
250 continue;
251 };
252
253 let storage_delta = account_delta.storage();
254
255 for (slot_name, map_delta) in storage_delta.maps() {
256 for (key, value) in map_delta.entries() {
257 let storage_map_info = StorageMapUpdate {
258 block_num: block_number,
259 slot_name: slot_name.clone(),
260 key: *key,
261 value: *value,
262 };
263 updates.push(storage_map_info);
264 }
265 }
266 }
267 }
268
269 StorageMapInfo {
270 chain_tip,
271 block_number: page_end_block,
272 updates,
273 }
274 }
275
276 pub fn get_available_notes(&self) -> Vec<MockChainNote> {
277 self.mock_chain.read().committed_notes().values().cloned().collect()
278 }
279
280 pub fn get_public_available_notes(&self) -> Vec<MockChainNote> {
281 self.mock_chain
282 .read()
283 .committed_notes()
284 .values()
285 .filter(|n| matches!(n, MockChainNote::Public(_, _)))
286 .cloned()
287 .collect()
288 }
289
290 pub fn get_private_available_notes(&self) -> Vec<MockChainNote> {
291 self.mock_chain
292 .read()
293 .committed_notes()
294 .values()
295 .filter(|n| matches!(n, MockChainNote::Private(_, _, _)))
296 .cloned()
297 .collect()
298 }
299
300 pub fn advance_blocks(&self, num_blocks: u32) {
301 let current_height = self.get_chain_tip_block_num();
302 let mut mock_chain = self.mock_chain.write();
303 mock_chain.prove_until_block(current_height + num_blocks).unwrap();
304 }
305}
306#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
307#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
308impl NodeRpcClient for MockRpcApi {
309 fn has_genesis_commitment(&self) -> Option<Word> {
310 None
311 }
312
313 async fn set_genesis_commitment(&self, _commitment: Word) -> Result<(), RpcError> {
314 Ok(())
316 }
317
318 async fn sync_notes(
321 &self,
322 block_num: BlockNumber,
323 block_to: Option<BlockNumber>,
324 note_tags: &BTreeSet<NoteTag>,
325 ) -> Result<NoteSyncInfo, RpcError> {
326 let chain_tip = self.get_chain_tip_block_num();
327 let upper_bound = block_to.unwrap_or(chain_tip);
328
329 let mut blocks_with_notes: BTreeMap<BlockNumber, BTreeMap<NoteId, CommittedNote>> =
331 BTreeMap::new();
332 for note in self.mock_chain.read().committed_notes().values() {
333 let note_block = note.inclusion_proof().location().block_num();
334 if note_tags.contains(¬e.metadata().tag())
335 && note_block > block_num
336 && note_block <= upper_bound
337 {
338 let committed = CommittedNote::new(
339 note.id(),
340 CommittedNoteMetadata::Full(note.metadata().clone()),
341 note.inclusion_proof().clone(),
342 );
343 blocks_with_notes.entry(note_block).or_default().insert(note.id(), committed);
344 }
345 }
346
347 blocks_with_notes.entry(upper_bound).or_default();
350
351 let blocks: Vec<NoteSyncBlock> = blocks_with_notes
352 .into_iter()
353 .map(|(bn, notes)| {
354 let block_header = self.get_block_by_num(bn);
355 let mmr_path = self.get_mmr().open(bn.as_usize()).unwrap().merkle_path().clone();
356 NoteSyncBlock { block_header, mmr_path, notes }
357 })
358 .collect();
359
360 Ok(NoteSyncInfo { chain_tip, block_to: upper_bound, blocks })
361 }
362
363 async fn sync_chain_mmr(
364 &self,
365 block_from: BlockNumber,
366 block_to: Option<BlockNumber>,
367 ) -> Result<ChainMmrInfo, RpcError> {
368 let chain_tip = self.get_chain_tip_block_num();
369 let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
370
371 let from_forest = if block_from == chain_tip {
372 target_block.as_usize()
373 } else {
374 block_from.as_u32() as usize + 1
375 };
376
377 let mmr_delta = self
378 .get_mmr()
379 .get_delta(Forest::new(from_forest), Forest::new(target_block.as_usize()))
380 .unwrap();
381
382 let block_header = self.get_block_by_num(target_block);
383
384 Ok(ChainMmrInfo {
385 block_from,
386 block_to: target_block,
387 mmr_delta,
388 block_header,
389 })
390 }
391
392 async fn get_block_header_by_number(
395 &self,
396 block_num: Option<BlockNumber>,
397 include_mmr_proof: bool,
398 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
399 let block = if let Some(block_num) = block_num {
400 self.mock_chain.read().block_header(block_num.as_usize())
401 } else {
402 self.mock_chain.read().latest_block_header()
403 };
404
405 let mmr_proof = if include_mmr_proof {
406 Some(self.get_mmr().open(block_num.unwrap().as_usize()).unwrap())
407 } else {
408 None
409 };
410
411 Ok((block, mmr_proof))
412 }
413
414 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
416 let notes = self.mock_chain.read().committed_notes().clone();
418
419 let hit_notes = note_ids.iter().filter_map(|id| notes.get(id));
420 let mut return_notes = vec![];
421 for note in hit_notes {
422 let fetched_note = match note {
423 MockChainNote::Private(note_id, note_metadata, note_inclusion_proof) => {
424 let note_header = NoteHeader::new(*note_id, note_metadata.clone());
425 FetchedNote::Private(note_header, note_inclusion_proof.clone())
426 },
427 MockChainNote::Public(note, note_inclusion_proof) => {
428 FetchedNote::Public(note.clone(), note_inclusion_proof.clone())
429 },
430 };
431 return_notes.push(fetched_note);
432 }
433 Ok(return_notes)
434 }
435
436 async fn submit_proven_transaction(
439 &self,
440 proven_transaction: ProvenTransaction,
441 _tx_inputs: TransactionInputs, ) -> Result<BlockNumber, RpcError> {
443 {
446 let mut mock_chain = self.mock_chain.write();
447 mock_chain.add_pending_proven_transaction(proven_transaction.clone());
448 };
449
450 let block_num = self.get_chain_tip_block_num();
451
452 Ok(block_num)
453 }
454
455 async fn get_account_details(&self, account_id: AccountId) -> Result<FetchedAccount, RpcError> {
458 let summary =
459 self.account_commitment_updates
460 .read()
461 .iter()
462 .rev()
463 .find_map(|(block_num, updates)| {
464 updates.get(&account_id).map(|commitment| AccountUpdateSummary {
465 commitment: *commitment,
466 last_block_num: *block_num,
467 })
468 });
469
470 if let Ok(account) = self.mock_chain.read().committed_account(account_id) {
471 let summary = summary.unwrap_or_else(|| AccountUpdateSummary {
472 commitment: account.to_commitment(),
473 last_block_num: BlockNumber::GENESIS,
474 });
475 Ok(FetchedAccount::new_public(account.clone(), summary))
476 } else if let Some(summary) = summary {
477 Ok(FetchedAccount::new_private(account_id, summary))
478 } else {
479 Err(RpcError::ExpectedDataMissing(format!(
480 "account {account_id} not found in mock commitment updates or mock chain"
481 )))
482 }
483 }
484
485 async fn get_account_proof(
488 &self,
489 account_id: AccountId,
490 account_storage_requirements: AccountStorageRequirements,
491 account_state: AccountStateAt,
492 _known_account_code: Option<AccountCode>,
493 _known_vault_commitment: Option<Word>,
494 ) -> Result<(BlockNumber, AccountProof), RpcError> {
495 let mock_chain = self.mock_chain.read();
496
497 let block_number = match account_state {
498 AccountStateAt::Block(number) => number,
499 AccountStateAt::ChainTip => mock_chain.latest_block_header().block_num(),
500 };
501
502 let headers = if account_id.has_public_state() {
503 let account = mock_chain.committed_account(account_id).unwrap();
504
505 let mut map_details = vec![];
506 for slot_name in account_storage_requirements.inner().keys() {
507 if let Some(StorageSlotContent::Map(storage_map)) =
508 account.storage().get(slot_name).map(StorageSlot::content)
509 {
510 let entries: Vec<StorageMapEntry> = storage_map
511 .entries()
512 .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
513 .collect();
514
515 let too_many_entries = entries.len() > self.oversize_threshold;
518 let account_storage_map_detail = AccountStorageMapDetails {
519 slot_name: slot_name.clone(),
520 too_many_entries,
521 entries: StorageMapEntries::AllEntries(entries),
522 };
523
524 map_details.push(account_storage_map_detail);
525 } else {
526 panic!("Storage slot {slot_name} is not a map");
527 }
528 }
529
530 let storage_details = AccountStorageDetails {
531 header: account.storage().to_header(),
532 map_details,
533 };
534
535 let mut assets = vec![];
536 for asset in account.vault().assets() {
537 assets.push(asset);
538 }
539 let vault_details = AccountVaultDetails {
540 too_many_assets: assets.len() > self.oversize_threshold,
541 assets,
542 };
543
544 Some(AccountDetails {
545 header: account.into(),
546 storage_details,
547 code: account.code().clone(),
548 vault_details,
549 })
550 } else {
551 None
552 };
553
554 let witness = mock_chain.account_tree().open(account_id);
555
556 let proof = AccountProof::new(witness, headers).unwrap();
557
558 Ok((block_number, proof))
559 }
560
561 async fn sync_nullifiers(
564 &self,
565 prefixes: &[u16],
566 from_block_num: BlockNumber,
567 block_to: Option<BlockNumber>,
568 ) -> Result<Vec<NullifierUpdate>, RpcError> {
569 let nullifiers = self
570 .mock_chain
571 .read()
572 .nullifier_tree()
573 .entries()
574 .filter_map(|(nullifier, block_num)| {
575 let within_range = if let Some(to_block) = block_to {
576 block_num >= from_block_num && block_num <= to_block
577 } else {
578 block_num >= from_block_num
579 };
580
581 if prefixes.contains(&nullifier.prefix()) && within_range {
582 Some(NullifierUpdate { nullifier, block_num })
583 } else {
584 None
585 }
586 })
587 .collect::<Vec<_>>();
588
589 Ok(nullifiers)
590 }
591
592 async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError> {
594 Ok(nullifiers
595 .iter()
596 .map(|nullifier| self.mock_chain.read().nullifier_tree().open(nullifier).into_proof())
597 .collect())
598 }
599
600 async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError> {
601 let block = self
602 .mock_chain
603 .read()
604 .proven_blocks()
605 .iter()
606 .find(|b| b.header().block_num() == block_num)
607 .unwrap()
608 .clone();
609
610 Ok(block)
611 }
612
613 async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError> {
614 let note = self
615 .get_available_notes()
616 .iter()
617 .find(|note| note.note().is_some_and(|n| n.script().root() == root))
618 .unwrap()
619 .clone();
620
621 Ok(note.note().unwrap().script().clone())
622 }
623
624 async fn sync_storage_maps(
625 &self,
626 block_from: BlockNumber,
627 block_to: Option<BlockNumber>,
628 account_id: AccountId,
629 ) -> Result<StorageMapInfo, RpcError> {
630 let mut all_updates = Vec::new();
631 let mut current_block_from = block_from;
632 let chain_tip = self.get_chain_tip_block_num();
633 let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
634
635 loop {
636 let response =
637 self.get_sync_storage_maps_request(current_block_from, block_to, account_id);
638 all_updates.extend(response.updates);
639
640 if response.block_number >= target_block {
641 return Ok(StorageMapInfo {
642 chain_tip: response.chain_tip,
643 block_number: response.block_number,
644 updates: all_updates,
645 });
646 }
647
648 current_block_from = (response.block_number.as_u32() + 1).into();
649 }
650 }
651
652 async fn sync_account_vault(
653 &self,
654 block_from: BlockNumber,
655 block_to: Option<BlockNumber>,
656 account_id: AccountId,
657 ) -> Result<AccountVaultInfo, RpcError> {
658 let mut all_updates = Vec::new();
659 let mut current_block_from = block_from;
660 let chain_tip = self.get_chain_tip_block_num();
661 let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
662
663 loop {
664 let response =
665 self.get_sync_account_vault_request(current_block_from, block_to, account_id);
666 all_updates.extend(response.updates);
667
668 if response.block_number >= target_block {
669 return Ok(AccountVaultInfo {
670 chain_tip: response.chain_tip,
671 block_number: response.block_number,
672 updates: all_updates,
673 });
674 }
675
676 current_block_from = (response.block_number.as_u32() + 1).into();
677 }
678 }
679
680 async fn sync_transactions(
681 &self,
682 block_from: BlockNumber,
683 block_to: Option<BlockNumber>,
684 account_ids: Vec<AccountId>,
685 ) -> Result<TransactionsInfo, RpcError> {
686 let response = self.get_sync_transactions_request(block_from, block_to, &account_ids);
687 Ok(response)
688 }
689
690 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
691 Ok(NetworkId::Testnet)
692 }
693
694 async fn get_rpc_limits(&self) -> Result<crate::rpc::RpcLimits, RpcError> {
695 Ok(crate::rpc::RpcLimits::default())
696 }
697
698 fn has_rpc_limits(&self) -> Option<crate::rpc::RpcLimits> {
699 None
700 }
701
702 async fn set_rpc_limits(&self, _limits: crate::rpc::RpcLimits) {
703 }
705
706 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
707 Ok(RpcStatusInfo {
708 version: env!("CARGO_PKG_VERSION").into(),
709 genesis_commitment: None,
710 store: None,
711 block_producer: None,
712 })
713 }
714}
715
716impl From<MockChain> for MockRpcApi {
720 fn from(mock_chain: MockChain) -> Self {
721 MockRpcApi::new(mock_chain)
722 }
723}
724
725fn build_account_updates(
729 mock_chain: &MockChain,
730) -> BTreeMap<BlockNumber, BTreeMap<AccountId, Word>> {
731 let mut account_commitment_updates = BTreeMap::new();
732 for block in mock_chain.proven_blocks() {
733 let block_num = block.header().block_num();
734 let mut updates = BTreeMap::new();
735
736 for update in block.body().updated_accounts() {
737 updates.insert(update.account_id(), update.final_state_commitment());
738 }
739
740 if updates.is_empty() {
741 continue;
742 }
743
744 account_commitment_updates.insert(block_num, updates);
745 }
746 account_commitment_updates
747}