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#[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 const PAGINATION_BLOCK_LIMIT: u32 = 5;
70
71 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 pub fn get_mmr(&self) -> Mmr {
81 self.mock_chain.read().blockchain().as_mmr().clone()
82 }
83
84 pub fn get_chain_tip_block_num(&self) -> BlockNumber {
86 self.mock_chain.read().latest_block_header().block_num()
87 }
88
89 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 fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
109 self.mock_chain.read().block_header(block_num.as_usize())
110 }
111
112 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 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(¬e.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 let next_block = self.get_block_by_num(next_block_num);
140
141 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 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 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 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 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 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 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(¬e.metadata().tag())
360 || account_ids.contains(¬e.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 Ok(())
413 }
414
415 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 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 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 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
491 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 async fn submit_proven_transaction(
514 &self,
515 proven_transaction: ProvenTransaction,
516 _tx_inputs: TransactionInputs, ) -> Result<BlockNumber, RpcError> {
518 {
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 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 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 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 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
759impl From<MockChain> for MockRpcApi {
763 fn from(mock_chain: MockChain) -> Self {
764 MockRpcApi::new(mock_chain)
765 }
766}
767
768fn 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}