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