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 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 const PAGINATION_BLOCK_LIMIT: u32 = 5;
75
76 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 #[must_use]
90 pub fn with_oversize_threshold(mut self, threshold: usize) -> Self {
91 self.oversize_threshold = threshold;
92 self
93 }
94
95 pub fn mark_note_as_erased(&self, header: NoteHeader) {
97 self.erased_notes.write().push(header);
98 }
99
100 pub fn get_mmr(&self) -> Mmr {
102 self.mock_chain.read().blockchain().as_mmr().clone()
103 }
104
105 pub fn get_chain_tip_block_num(&self) -> BlockNumber {
107 self.mock_chain.read().latest_block_header().block_num()
108 }
109
110 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 fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
130 self.mock_chain.read().block_header(block_num.as_usize())
131 }
132
133 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 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 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 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 Ok(())
326 }
327
328 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 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(¬e.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 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 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 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
426 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 async fn submit_proven_transaction(
449 &self,
450 proven_transaction: ProvenTransaction,
451 _tx_inputs: TransactionInputs, ) -> Result<BlockNumber, RpcError> {
453 {
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 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 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 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 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 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 }
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
726impl From<MockChain> for MockRpcApi {
730 fn from(mock_chain: MockChain) -> Self {
731 MockRpcApi::new(mock_chain)
732 }
733}
734
735fn 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}