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::{AccountId, StorageSlot, StorageSlotContent, StorageSlotType};
9use miden_protocol::address::NetworkId;
10use miden_protocol::batch::{ProposedBatch, ProvenBatch};
11use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
12use miden_protocol::crypto::merkle::mmr::{Forest, Mmr, MmrProof};
13use miden_protocol::note::{NoteAttachments, NoteHeader, NoteId, NoteScript, NoteTag};
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 AccountVaultDetails,
25 GetAccountRequest,
26 StorageMapEntries,
27 StorageMapEntry,
28 StorageMapFetch,
29};
30use crate::rpc::domain::account_vault::{AccountVaultInfo, AccountVaultUpdate};
31use crate::rpc::domain::note::{CommittedNote, FetchedNote, NoteSyncBlock};
32use crate::rpc::domain::nullifier::NullifierUpdate;
33use crate::rpc::domain::status::NetworkNoteStatusInfo;
34use crate::rpc::domain::storage_map::{StorageMapInfo, StorageMapUpdate};
35use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
36use crate::rpc::domain::transaction::TransactionRecord;
37use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError, RpcStatusInfo};
38
39pub type MockClient<AUTH> = Client<AUTH>;
40
41#[derive(Clone)]
52pub struct MockRpcApi {
53 account_commitment_updates: Arc<RwLock<BTreeMap<BlockNumber, BTreeMap<AccountId, Word>>>>,
54 pub mock_chain: Arc<RwLock<MockChain>>,
55 oversize_threshold: usize,
56 erased_notes: Arc<RwLock<Vec<NoteHeader>>>,
58 private_note_attachments: Arc<RwLock<BTreeMap<NoteId, NoteAttachments>>>,
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 erased_notes: Arc::new(RwLock::new(Vec::new())),
81 private_note_attachments: Arc::new(RwLock::new(BTreeMap::new())),
82 }
83 }
84
85 pub fn register_private_note_attachments(&self, note_id: NoteId, attachments: NoteAttachments) {
88 self.private_note_attachments.write().insert(note_id, attachments);
89 }
90
91 #[must_use]
95 pub fn with_oversize_threshold(mut self, threshold: usize) -> Self {
96 self.oversize_threshold = threshold;
97 self
98 }
99
100 pub fn mark_note_as_erased(&self, header: NoteHeader) {
102 self.erased_notes.write().push(header);
103 }
104
105 pub fn get_mmr(&self) -> Mmr {
107 self.mock_chain.read().blockchain().as_mmr().clone()
108 }
109
110 pub fn get_chain_tip_block_num(&self) -> BlockNumber {
112 self.mock_chain.read().latest_block_header().block_num()
113 }
114
115 pub fn prove_block(&self) {
118 let proven_block = self.mock_chain.write().prove_next_block().unwrap();
119 let mut account_commitment_updates = self.account_commitment_updates.write();
120 let block_num = proven_block.header().block_num();
121 let updates: BTreeMap<AccountId, Word> = proven_block
122 .body()
123 .updated_accounts()
124 .iter()
125 .map(|update| (update.account_id(), update.final_state_commitment()))
126 .collect();
127
128 if !updates.is_empty() {
129 account_commitment_updates.insert(block_num, updates);
130 }
131 }
132
133 fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
135 self.mock_chain.read().block_header(block_num.as_usize())
136 }
137
138 fn get_sync_account_vault_request(
142 &self,
143 block_from: BlockNumber,
144 block_to: BlockNumber,
145 account_id: AccountId,
146 ) -> AccountVaultInfo {
147 let chain_tip = self.get_chain_tip_block_num();
148 let target_block = block_to.min(chain_tip);
149
150 let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
151 .min(target_block.as_u32())
152 .into();
153
154 let mut updates = vec![];
155 for block in self.mock_chain.read().proven_blocks() {
156 let block_number = block.header().block_num();
157 if block_number < block_from || block_number > page_end_block {
159 continue;
160 }
161
162 for update in block
163 .body()
164 .updated_accounts()
165 .iter()
166 .filter(|block_acc_update| block_acc_update.account_id() == account_id)
167 {
168 let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
169 continue;
170 };
171
172 let vault_delta = account_delta.vault();
173
174 for asset in vault_delta.added_assets() {
175 let account_vault_update = AccountVaultUpdate {
176 block_num: block_number,
177 asset: Some(asset),
178 vault_key: asset.vault_key(),
179 };
180 updates.push(account_vault_update);
181 }
182 }
183 }
184
185 AccountVaultInfo {
186 chain_tip,
187 block_number: page_end_block,
188 updates,
189 }
190 }
191
192 fn get_sync_transactions_request(
194 &self,
195 block_from: BlockNumber,
196 block_to: BlockNumber,
197 account_ids: &[AccountId],
198 ) -> Vec<TransactionRecord> {
199 let mut transactions = Vec::new();
200 for block in self.mock_chain.read().proven_blocks() {
201 let block_number = block.header().block_num();
202 if block_number < block_from || block_number > block_to {
203 continue;
204 }
205
206 for transaction_header in block.body().transactions().as_slice() {
207 if !account_ids.contains(&transaction_header.account_id()) {
208 continue;
209 }
210
211 let erased_output_notes = self.erased_notes.read().clone();
212
213 transactions.push(TransactionRecord {
214 block_num: block_number,
215 transaction_header: transaction_header.clone(),
216 output_notes: vec![],
217 erased_output_notes,
218 });
219 }
220 }
221
222 transactions
223 }
224
225 fn get_sync_storage_maps_request(
229 &self,
230 block_from: BlockNumber,
231 block_to: BlockNumber,
232 account_id: AccountId,
233 ) -> StorageMapInfo {
234 let chain_tip = self.get_chain_tip_block_num();
235 let target_block = block_to.min(chain_tip);
236
237 let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
238 .min(target_block.as_u32())
239 .into();
240
241 let mut updates = vec![];
242 for block in self.mock_chain.read().proven_blocks() {
243 let block_number = block.header().block_num();
244 if block_number < block_from || block_number > page_end_block {
246 continue;
247 }
248
249 for update in block
250 .body()
251 .updated_accounts()
252 .iter()
253 .filter(|block_acc_update| block_acc_update.account_id() == account_id)
254 {
255 let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
256 continue;
257 };
258
259 let storage_delta = account_delta.storage();
260
261 for (slot_name, map_delta) in storage_delta.maps() {
262 for (key, value) in map_delta.entries() {
263 let storage_map_info = StorageMapUpdate {
264 block_num: block_number,
265 slot_name: slot_name.clone(),
266 key: *key,
267 value: *value,
268 };
269 updates.push(storage_map_info);
270 }
271 }
272 }
273 }
274
275 StorageMapInfo {
276 chain_tip,
277 block_number: page_end_block,
278 updates,
279 }
280 }
281
282 pub fn get_available_notes(&self) -> Vec<MockChainNote> {
283 self.mock_chain.read().committed_notes().values().cloned().collect()
284 }
285
286 pub fn get_public_available_notes(&self) -> Vec<MockChainNote> {
287 self.mock_chain
288 .read()
289 .committed_notes()
290 .values()
291 .filter(|n| matches!(n, MockChainNote::Public(_, _)))
292 .cloned()
293 .collect()
294 }
295
296 pub fn get_private_available_notes(&self) -> Vec<MockChainNote> {
297 self.mock_chain
298 .read()
299 .committed_notes()
300 .values()
301 .filter(|n| matches!(n, MockChainNote::Private(_, _, _)))
302 .cloned()
303 .collect()
304 }
305
306 pub fn advance_blocks(&self, num_blocks: u32) {
307 let current_height = self.get_chain_tip_block_num();
308 let mut mock_chain = self.mock_chain.write();
309 mock_chain.prove_until_block(current_height + num_blocks).unwrap();
310 }
311}
312#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
313#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
314impl NodeRpcClient for MockRpcApi {
315 fn has_genesis_commitment(&self) -> Option<Word> {
316 None
317 }
318
319 async fn set_genesis_commitment(&self, _commitment: Word) -> Result<(), RpcError> {
320 Ok(())
322 }
323
324 async fn sync_notes(
327 &self,
328 block_from: BlockNumber,
329 block_to: BlockNumber,
330 note_tags: &BTreeSet<NoteTag>,
331 ) -> Result<Vec<NoteSyncBlock>, RpcError> {
332 let mut blocks_with_notes: BTreeMap<BlockNumber, BTreeMap<NoteId, CommittedNote>> =
333 BTreeMap::new();
334 for note in self.mock_chain.read().committed_notes().values() {
335 let note_block = note.inclusion_proof().location().block_num();
336 if note_tags.contains(¬e.metadata().tag())
337 && note_block >= block_from
338 && note_block <= block_to
339 {
340 let committed =
341 CommittedNote::new(note.id(), *note.metadata(), note.inclusion_proof().clone());
342 blocks_with_notes.entry(note_block).or_default().insert(note.id(), committed);
343 }
344 }
345
346 Ok(blocks_with_notes
347 .into_iter()
348 .map(|(bn, notes)| {
349 let block_header = self.get_block_by_num(bn);
350 let mmr_path = self.get_mmr().open(bn.as_usize()).unwrap().merkle_path().clone();
351 NoteSyncBlock { block_header, mmr_path, notes }
352 })
353 .collect())
354 }
355
356 async fn sync_chain_mmr(
357 &self,
358 current_block_height: BlockNumber,
359 upper_bound: SyncTarget,
360 ) -> Result<ChainMmrInfo, RpcError> {
361 let chain_tip = self.get_chain_tip_block_num();
362 let target_block = match upper_bound {
364 SyncTarget::CommittedChainTip | SyncTarget::ProvenChainTip => chain_tip,
365 };
366
367 let from_forest = if current_block_height == target_block {
368 target_block.as_usize()
369 } else {
370 current_block_height.as_u32() as usize + 1
371 };
372
373 let mmr_delta = self
374 .get_mmr()
375 .get_delta(
376 Forest::new(from_forest).unwrap(),
377 Forest::new(target_block.as_usize()).unwrap(),
378 )
379 .unwrap();
380
381 let block_header = self.get_block_by_num(target_block);
382
383 Ok(ChainMmrInfo {
384 block_from: current_block_height,
385 block_to: target_block,
386 mmr_delta,
387 block_header,
388 })
389 }
390
391 async fn get_block_header_by_number(
394 &self,
395 block_num: Option<BlockNumber>,
396 include_mmr_proof: bool,
397 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
398 let block = if let Some(block_num) = block_num {
399 self.mock_chain.read().block_header(block_num.as_usize())
400 } else {
401 self.mock_chain.read().latest_block_header()
402 };
403
404 let mmr_proof = if include_mmr_proof {
405 Some(self.get_mmr().open(block_num.unwrap().as_usize()).unwrap())
406 } else {
407 None
408 };
409
410 Ok((block, mmr_proof))
411 }
412
413 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
415 let notes = self.mock_chain.read().committed_notes().clone();
417
418 let hit_notes = note_ids.iter().filter_map(|id| notes.get(id));
419 let mut return_notes = vec![];
420 for note in hit_notes {
421 let fetched_note = match note {
422 MockChainNote::Private(note_id, note_metadata, note_inclusion_proof) => {
423 let attachments = self
424 .private_note_attachments
425 .read()
426 .get(note_id)
427 .cloned()
428 .unwrap_or_else(NoteAttachments::empty);
429 FetchedNote::Private(
430 *note_id,
431 *note_metadata,
432 attachments,
433 note_inclusion_proof.clone(),
434 )
435 },
436 MockChainNote::Public(note, note_inclusion_proof) => {
437 FetchedNote::Public(note.clone(), note_inclusion_proof.clone())
438 },
439 };
440 return_notes.push(fetched_note);
441 }
442 Ok(return_notes)
443 }
444
445 async fn submit_proven_transaction(
448 &self,
449 proven_transaction: ProvenTransaction,
450 _tx_inputs: TransactionInputs, ) -> Result<BlockNumber, RpcError> {
452 {
455 let mut mock_chain = self.mock_chain.write();
456 mock_chain.add_pending_proven_transaction(proven_transaction.clone());
457 };
458
459 let block_num = self.get_chain_tip_block_num();
460
461 Ok(block_num)
462 }
463
464 async fn submit_proven_batch(
469 &self,
470 proven_batch: ProvenBatch,
471 _proposed_batch: ProposedBatch,
472 _transaction_inputs: Vec<TransactionInputs>,
473 ) -> Result<BlockNumber, RpcError> {
474 let mut mock_chain = self.mock_chain.write();
475 mock_chain.add_pending_batch(proven_batch);
476 drop(mock_chain);
477
478 let block_num = self.get_chain_tip_block_num();
479
480 Ok(block_num)
481 }
482
483 async fn get_account(
488 &self,
489 account_id: AccountId,
490 request: GetAccountRequest,
491 ) -> Result<(BlockNumber, AccountProof), RpcError> {
492 let mock_chain = self.mock_chain.read();
493
494 let block_number = match request.at {
495 AccountStateAt::Block(number) => number,
496 AccountStateAt::ChainTip => mock_chain.latest_block_header().block_num(),
497 };
498
499 let headers = if account_id.is_public() {
500 let account = mock_chain.committed_account(account_id).unwrap();
501
502 let requested_slots: Vec<_> = match &request.storage {
505 StorageMapFetch::Skip => Vec::new(),
506 StorageMapFetch::Slots(reqs) => reqs.inner().keys().cloned().collect(),
507 StorageMapFetch::All => account
508 .storage()
509 .to_header()
510 .slots()
511 .filter(|slot| slot.slot_type() == StorageSlotType::Map)
512 .map(|slot| slot.name().clone())
513 .collect(),
514 };
515
516 let mut map_details = vec![];
517 for slot_name in &requested_slots {
518 if let Some(StorageSlotContent::Map(storage_map)) =
519 account.storage().get(slot_name).map(StorageSlot::content)
520 {
521 let entries: Vec<StorageMapEntry> = storage_map
522 .entries()
523 .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
524 .collect();
525
526 let too_many_entries = entries.len() > self.oversize_threshold;
529 let account_storage_map_detail = AccountStorageMapDetails {
530 slot_name: slot_name.clone(),
531 too_many_entries,
532 entries: StorageMapEntries::AllEntries(entries),
533 };
534
535 map_details.push(account_storage_map_detail);
536 } else {
537 panic!("Storage slot {slot_name} is not a map");
538 }
539 }
540
541 let storage_details = AccountStorageDetails {
542 header: account.storage().to_header(),
543 map_details,
544 };
545
546 let mut assets = vec![];
547 for asset in account.vault().assets() {
548 assets.push(asset);
549 }
550 let vault_details = AccountVaultDetails {
551 too_many_assets: assets.len() > self.oversize_threshold,
552 assets,
553 };
554
555 Some(AccountDetails {
556 header: account.into(),
557 storage_details,
558 code: account.code().clone(),
559 vault_details,
560 })
561 } else {
562 None
563 };
564
565 let witness = mock_chain.account_tree().open(account_id);
566
567 let proof = AccountProof::new(witness, headers).unwrap();
568
569 Ok((block_number, proof))
570 }
571
572 async fn sync_nullifiers(
575 &self,
576 prefixes: &[u16],
577 block_from: BlockNumber,
578 block_to: BlockNumber,
579 ) -> Result<Vec<NullifierUpdate>, RpcError> {
580 let nullifiers = self
581 .mock_chain
582 .read()
583 .nullifier_tree()
584 .entries()
585 .filter_map(|(nullifier, block_num)| {
586 let within_range = block_num >= block_from && block_num <= block_to;
587
588 if prefixes.contains(&nullifier.prefix()) && within_range {
589 Some(NullifierUpdate { nullifier, block_num })
590 } else {
591 None
592 }
593 })
594 .collect::<Vec<_>>();
595
596 Ok(nullifiers)
597 }
598
599 async fn get_block_by_number(
600 &self,
601 block_num: BlockNumber,
602 _include_proof: bool,
603 ) -> Result<ProvenBlock, RpcError> {
604 let block = self
605 .mock_chain
606 .read()
607 .proven_blocks()
608 .iter()
609 .find(|b| b.header().block_num() == block_num)
610 .unwrap()
611 .clone();
612
613 Ok(block)
614 }
615
616 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
617 let script = self
618 .get_available_notes()
619 .iter()
620 .filter_map(|note| note.note())
621 .find(|n| Word::from(n.script().root()) == root)
622 .map(|n| n.script().clone());
623
624 Ok(script)
625 }
626
627 async fn sync_storage_maps(
628 &self,
629 block_from: BlockNumber,
630 block_to: BlockNumber,
631 account_id: AccountId,
632 ) -> Result<StorageMapInfo, RpcError> {
633 let mut all_updates = Vec::new();
634 let mut current_block_from = block_from;
635 let chain_tip = self.get_chain_tip_block_num();
636 let target_block = block_to.min(chain_tip);
637
638 loop {
639 let response =
640 self.get_sync_storage_maps_request(current_block_from, block_to, account_id);
641 all_updates.extend(response.updates);
642
643 if response.block_number >= target_block {
644 return Ok(StorageMapInfo {
645 chain_tip: response.chain_tip,
646 block_number: response.block_number,
647 updates: all_updates,
648 });
649 }
650
651 current_block_from = (response.block_number.as_u32() + 1).into();
652 }
653 }
654
655 async fn sync_account_vault(
656 &self,
657 block_from: BlockNumber,
658 block_to: BlockNumber,
659 account_id: AccountId,
660 ) -> Result<AccountVaultInfo, RpcError> {
661 let mut all_updates = Vec::new();
662 let mut current_block_from = block_from;
663 let chain_tip = self.get_chain_tip_block_num();
664 let target_block = block_to.min(chain_tip);
665
666 loop {
667 let response =
668 self.get_sync_account_vault_request(current_block_from, block_to, account_id);
669 all_updates.extend(response.updates);
670
671 if response.block_number >= target_block {
672 return Ok(AccountVaultInfo {
673 chain_tip: response.chain_tip,
674 block_number: response.block_number,
675 updates: all_updates,
676 });
677 }
678
679 current_block_from = (response.block_number.as_u32() + 1).into();
680 }
681 }
682
683 async fn sync_transactions(
684 &self,
685 block_from: BlockNumber,
686 block_to: BlockNumber,
687 account_ids: Vec<AccountId>,
688 ) -> Result<Vec<TransactionRecord>, RpcError> {
689 Ok(self.get_sync_transactions_request(block_from, block_to, &account_ids))
690 }
691
692 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
693 Ok(NetworkId::Testnet)
694 }
695
696 async fn get_rpc_limits(&self) -> Result<crate::rpc::RpcLimits, RpcError> {
697 Ok(crate::rpc::RpcLimits::default())
698 }
699
700 fn has_rpc_limits(&self) -> Option<crate::rpc::RpcLimits> {
701 None
702 }
703
704 async fn set_rpc_limits(&self, _limits: crate::rpc::RpcLimits) {
705 }
707
708 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
709 Ok(RpcStatusInfo {
710 version: env!("CARGO_PKG_VERSION").into(),
711 genesis_commitment: None,
712 chain_tip: 0,
713 block_producer: None,
714 })
715 }
716
717 async fn get_network_note_status(
718 &self,
719 _note_id: NoteId,
720 ) -> Result<NetworkNoteStatusInfo, RpcError> {
721 todo!("We need to check if we want to implement this for the mockchain");
722 }
723}
724
725impl From<MockChain> for MockRpcApi {
729 fn from(mock_chain: MockChain) -> Self {
730 MockRpcApi::new(mock_chain)
731 }
732}
733
734fn build_account_updates(
738 mock_chain: &MockChain,
739) -> BTreeMap<BlockNumber, BTreeMap<AccountId, Word>> {
740 let mut account_commitment_updates = BTreeMap::new();
741 for block in mock_chain.proven_blocks() {
742 let block_num = block.header().block_num();
743 let mut updates = BTreeMap::new();
744
745 for update in block.body().updated_accounts() {
746 updates.insert(update.account_id(), update.final_state_commitment());
747 }
748
749 if updates.is_empty() {
750 continue;
751 }
752
753 account_commitment_updates.insert(block_num, updates);
754 }
755 account_commitment_updates
756}