1use alloc::borrow::ToOwned;
2use alloc::boxed::Box;
3use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::error::Error;
7use core::pin::Pin;
8
9use miden_protocol::vm::FutureMaybeSend;
10
11type RpcFuture<T> = Pin<Box<dyn FutureMaybeSend<T>>>;
12
13use miden_protocol::account::{
14 AccountCode,
15 AccountId,
16 AccountVaultPatch,
17 StorageMapPatchEntries,
18 StorageSlotName,
19};
20use miden_protocol::address::NetworkId;
21use miden_protocol::batch::{ProposedBatch, ProvenBatch};
22use miden_protocol::block::account_tree::AccountWitness;
23use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
24use miden_protocol::crypto::merkle::MerklePath;
25use miden_protocol::crypto::merkle::mmr::{Forest, MmrPath, MmrProof};
26use miden_protocol::note::{NoteId, NoteScript, NoteTag};
27use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};
28use miden_protocol::utils::serde::Deserializable;
29use miden_protocol::{EMPTY_WORD, Word};
30use miden_tx::utils::serde::Serializable;
31use miden_tx::utils::sync::RwLock;
32use tonic::Status;
33use tracing::info;
34
35use super::domain::account::{
36 AccountProof,
37 AccountStorageRequirements,
38 GetAccountRequest,
39 StorageMapFetch,
40};
41use super::domain::note::{CommittedNote, FetchedNote, NoteSyncBlock};
42use super::domain::nullifier::NullifierUpdate;
43use super::generated::rpc::AccountRequest;
44use super::generated::rpc::account_request::AccountDetailRequest;
45use super::{Endpoint, NodeRpcClient, RpcEndpoint, RpcError, RpcStatusInfo};
46use crate::rpc::domain::account_vault::AccountVaultInfo;
47use crate::rpc::domain::limits::RpcLimits;
48use crate::rpc::domain::status::NetworkNoteStatusInfo;
49use crate::rpc::domain::storage_map::StorageMapInfo;
50use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
51use crate::rpc::domain::transaction::TransactionRecord;
52use crate::rpc::errors::node::parse_node_error;
53use crate::rpc::errors::{AcceptHeaderContext, AcceptHeaderError, GrpcError, RpcConversionError};
54use crate::rpc::generated::rpc::BlockRange;
55use crate::rpc::{AccountStateAt, generated as proto};
56
57mod api_client;
58mod retry;
59
60use api_client::api_client_wrapper::ApiClient;
61
62struct BlockPagination {
64 current_block_from: BlockNumber,
65 block_to: BlockNumber,
66 iterations: u32,
67}
68
69enum PaginationResult {
70 Continue,
71 Done {
72 chain_tip: BlockNumber,
73 block_num: BlockNumber,
74 },
75}
76
77impl BlockPagination {
78 const MAX_ITERATIONS: u32 = 1000;
83
84 fn new(block_from: BlockNumber, block_to: BlockNumber) -> Self {
85 Self {
86 current_block_from: block_from,
87 block_to,
88 iterations: 0,
89 }
90 }
91
92 fn current_block_from(&self) -> BlockNumber {
93 self.current_block_from
94 }
95
96 fn block_to(&self) -> BlockNumber {
97 self.block_to
98 }
99
100 fn advance(
101 &mut self,
102 block_num: BlockNumber,
103 chain_tip: BlockNumber,
104 ) -> Result<PaginationResult, RpcError> {
105 if self.iterations >= Self::MAX_ITERATIONS {
106 return Err(RpcError::PaginationError(
107 "too many pagination iterations, possible infinite loop".to_owned(),
108 ));
109 }
110 self.iterations += 1;
111
112 if block_num < self.current_block_from {
113 return Err(RpcError::PaginationError(
114 "invalid pagination: block_num went backwards".to_owned(),
115 ));
116 }
117
118 let target_block = self.block_to.min(chain_tip);
119
120 if block_num >= target_block {
121 return Ok(PaginationResult::Done { chain_tip, block_num });
122 }
123
124 self.current_block_from = BlockNumber::from(block_num.as_u32().saturating_add(1));
125
126 Ok(PaginationResult::Continue)
127 }
128}
129
130fn ensure_requested_nullifiers(
133 requested_prefixes: &BTreeSet<u16>,
134 batch: &[NullifierUpdate],
135) -> Result<(), RpcError> {
136 for update in batch {
137 let prefix = update.nullifier.prefix();
138 if !requested_prefixes.contains(&prefix) {
139 let requested = requested_prefixes
140 .iter()
141 .map(ToString::to_string)
142 .collect::<Vec<_>>()
143 .join(", ");
144 return Err(RpcError::InvalidResponse(format!(
145 "node returned nullifier with prefix {prefix} but [{requested}] were requested"
146 )));
147 }
148 }
149 Ok(())
150}
151
152fn ensure_requested_tags(
154 requested: &BTreeSet<NoteTag>,
155 returned: impl IntoIterator<Item = NoteTag>,
156) -> Result<(), RpcError> {
157 for tag in returned {
158 if !requested.contains(&tag) {
159 let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
160 return Err(RpcError::InvalidResponse(format!(
161 "node returned note with tag {tag} but [{list}] were requested"
162 )));
163 }
164 }
165 Ok(())
166}
167
168fn ensure_requested_note_ids(
170 requested: &BTreeSet<NoteId>,
171 returned: impl IntoIterator<Item = NoteId>,
172) -> Result<(), RpcError> {
173 for id in returned {
174 if !requested.contains(&id) {
175 let list = requested.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
176 return Err(RpcError::InvalidResponse(format!(
177 "node returned note {id} but [{list}] were requested"
178 )));
179 }
180 }
181 Ok(())
182}
183
184const DEFAULT_MAX_RESPONSE_SIZE_BYTES: usize = 4 * 1024 * 1024 * 115 / 100;
190
191pub struct GrpcClient {
202 client: RwLock<Option<ApiClient>>,
204 endpoint: String,
206 timeout_ms: u64,
208 genesis_commitment: RwLock<Option<Word>>,
210 limits: RwLock<Option<RpcLimits>>,
212 max_retries: u32,
214 retry_interval_ms: u64,
216 bearer_token: Option<String>,
220 max_decoding_message_size: usize,
223}
224
225impl GrpcClient {
226 pub fn new(endpoint: &Endpoint, timeout_ms: u64) -> GrpcClient {
229 GrpcClient {
230 client: RwLock::new(None),
231 endpoint: endpoint.to_string(),
232 timeout_ms,
233 genesis_commitment: RwLock::new(None),
234 limits: RwLock::new(None),
235 max_retries: retry::DEFAULT_MAX_RETRIES,
236 retry_interval_ms: retry::DEFAULT_RETRY_INTERVAL_MS,
237 bearer_token: None,
238 max_decoding_message_size: DEFAULT_MAX_RESPONSE_SIZE_BYTES,
239 }
240 }
241
242 #[must_use]
245 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
246 self.max_retries = max_retries;
247 self
248 }
249
250 #[must_use]
253 pub fn with_retry_interval_ms(mut self, retry_interval_ms: u64) -> Self {
254 self.retry_interval_ms = retry_interval_ms;
255 self
256 }
257
258 #[must_use]
265 pub fn with_max_decoding_message_size(mut self, max_decoding_message_size: usize) -> Self {
266 self.max_decoding_message_size = max_decoding_message_size;
267 self
268 }
269
270 #[must_use]
295 pub fn with_bearer_auth(mut self, token: String) -> Self {
296 self.bearer_token = Some(token);
297 self
298 }
299
300 async fn ensure_connected(&self) -> Result<ApiClient, RpcError> {
303 if self.client.read().is_none() {
304 self.connect().await?;
305 }
306
307 Ok(self.client.read().as_ref().expect("rpc_api should be initialized").clone())
308 }
309
310 async fn connect(&self) -> Result<(), RpcError> {
313 let genesis_commitment = *self.genesis_commitment.read();
314 let new_client = ApiClient::new_client(
315 self.endpoint.clone(),
316 self.timeout_ms,
317 genesis_commitment,
318 self.bearer_token.clone(),
319 self.max_decoding_message_size,
320 )
321 .await?;
322 let mut client = self.client.write();
323 client.replace(new_client);
324
325 Ok(())
326 }
327
328 fn rpc_error_from_status(&self, endpoint: RpcEndpoint, status: Status) -> RpcError {
329 let genesis_commitment = self
330 .genesis_commitment
331 .read()
332 .as_ref()
333 .map_or_else(|| "none".to_string(), Word::to_hex);
334 let context = AcceptHeaderContext {
335 client_version: env!("CARGO_PKG_VERSION").to_string(),
336 genesis_commitment,
337 };
338 RpcError::from_grpc_error_with_context(endpoint, status, context)
339 }
340
341 async fn call_with_retry<T: Send + 'static>(
352 &self,
353 endpoint: RpcEndpoint,
354 mut call: impl FnMut(ApiClient) -> RpcFuture<Result<tonic::Response<T>, Status>>,
355 ) -> Result<tonic::Response<T>, RpcError> {
356 let mut retry_state = retry::RetryState::new(self.max_retries, self.retry_interval_ms);
357
358 loop {
359 let rpc_api = self.ensure_connected().await?;
360
361 match call(rpc_api).await {
362 Ok(response) => return Ok(response),
363 Err(status) if retry_state.should_retry(&status).await => {},
364 Err(status) => return Err(self.rpc_error_from_status(endpoint, status)),
365 }
366 }
367 }
368
369 pub async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
376 let mut rpc_api = ApiClient::new_client_without_accept_header(
377 self.endpoint.clone(),
378 self.timeout_ms,
379 self.bearer_token.clone(),
380 self.max_decoding_message_size,
381 )
382 .await?;
383 rpc_api
384 .status(())
385 .await
386 .map_err(|status| self.rpc_error_from_status(RpcEndpoint::Status, status))
387 .map(tonic::Response::into_inner)
388 .and_then(RpcStatusInfo::try_from)
389 }
390}
391
392#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
393#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
394impl NodeRpcClient for GrpcClient {
395 fn has_genesis_commitment(&self) -> Option<Word> {
400 *self.genesis_commitment.read()
401 }
402
403 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
404 if self.genesis_commitment.read().is_some() {
406 return Ok(());
408 }
409
410 self.genesis_commitment.write().replace(commitment);
412
413 let mut client_guard = self.client.write();
416 if let Some(client) = client_guard.as_mut() {
417 client.set_genesis_commitment(commitment);
418 }
419
420 Ok(())
421 }
422
423 async fn submit_proven_transaction(
424 &self,
425 proven_transaction: ProvenTransaction,
426 transaction_inputs: TransactionInputs,
427 ) -> Result<BlockNumber, RpcError> {
428 let request = proto::transaction::ProvenTransaction {
429 transaction: proven_transaction.to_bytes(),
430 transaction_inputs: Some(transaction_inputs.to_bytes()),
431 };
432
433 let api_response = self
434 .call_with_retry(RpcEndpoint::SubmitProvenTx, |mut rpc_api| {
435 let request = request.clone();
436 Box::pin(async move { rpc_api.submit_proven_tx(request).await })
437 })
438 .await?;
439
440 Ok(BlockNumber::from(api_response.into_inner().block_num))
441 }
442
443 async fn submit_proven_batch(
444 &self,
445 proven_batch: ProvenBatch,
446 proposed_batch: ProposedBatch,
447 transaction_inputs: Vec<TransactionInputs>,
448 ) -> Result<BlockNumber, RpcError> {
449 let request = proto::transaction::TransactionBatch {
450 batch_proof: proven_batch.to_bytes(),
451 proposed_batch: Some(proposed_batch.to_bytes()),
452 transaction_inputs: transaction_inputs.iter().map(Serializable::to_bytes).collect(),
453 };
454
455 let api_response = self
456 .call_with_retry(RpcEndpoint::SubmitProvenBatch, |mut rpc_api| {
457 let request = request.clone();
458 Box::pin(async move { rpc_api.submit_proven_tx_batch(request).await })
459 })
460 .await?;
461
462 Ok(BlockNumber::from(api_response.into_inner().block_num))
463 }
464
465 async fn get_block_header_by_number(
466 &self,
467 block_num: Option<BlockNumber>,
468 include_mmr_proof: bool,
469 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
470 let request = proto::rpc::BlockHeaderByNumberRequest {
471 block_num: block_num.as_ref().map(BlockNumber::as_u32),
472 include_mmr_proof: Some(include_mmr_proof),
473 };
474
475 info!("Calling GetBlockHeaderByNumber: {:?}", request);
476
477 let api_response = self
478 .call_with_retry(RpcEndpoint::GetBlockHeaderByNumber, |mut rpc_api| {
479 Box::pin(async move { rpc_api.get_block_header_by_number(request).await })
480 })
481 .await?;
482
483 let response = api_response.into_inner();
484
485 let block_header: BlockHeader = response
486 .block_header
487 .ok_or(RpcError::ExpectedDataMissing("BlockHeader".into()))?
488 .try_into()?;
489
490 if let Some(requested) = block_num
491 && block_header.block_num() != requested
492 {
493 return Err(RpcError::InvalidResponse(format!(
494 "node returned header for block {} but block {requested} was requested",
495 block_header.block_num(),
496 )));
497 }
498
499 let mmr_proof = if include_mmr_proof {
500 let forest = response
501 .chain_length
502 .ok_or(RpcError::ExpectedDataMissing("ChainLength".into()))?;
503 let merkle_path: MerklePath = response
504 .mmr_path
505 .ok_or(RpcError::ExpectedDataMissing("MmrPath".into()))?
506 .try_into()?;
507
508 let forest_size = usize::try_from(forest).expect("u64 should fit in usize");
509 let forest = Forest::new(forest_size).map_err(|_| {
510 RpcError::InvalidResponse(format!("invalid forest size: {forest_size}"))
511 })?;
512 Some(MmrProof::new(
513 MmrPath::new(forest, block_header.block_num().as_usize(), merkle_path),
514 block_header.commitment(),
515 ))
516 } else {
517 None
518 };
519
520 Ok((block_header, mmr_proof))
521 }
522
523 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
524 let limits = self.get_rpc_limits().await?;
525 let requested_ids: BTreeSet<NoteId> = note_ids.iter().copied().collect();
526 let mut notes = Vec::with_capacity(note_ids.len());
527 for chunk in note_ids.chunks(limits.note_ids_limit as usize) {
528 let request = proto::note::NoteIdList {
529 ids: chunk.iter().map(|id| (*id).into()).collect(),
530 };
531
532 let api_response = self
533 .call_with_retry(RpcEndpoint::GetNotesById, |mut rpc_api| {
534 let request = request.clone();
535 Box::pin(async move { rpc_api.get_notes_by_id(request).await })
536 })
537 .await?;
538
539 let response_notes = api_response
540 .into_inner()
541 .notes
542 .into_iter()
543 .map(FetchedNote::try_from)
544 .collect::<Result<Vec<FetchedNote>, RpcConversionError>>()?;
545
546 ensure_requested_note_ids(&requested_ids, response_notes.iter().map(FetchedNote::id))?;
547
548 notes.extend(response_notes);
549 }
550 Ok(notes)
551 }
552
553 async fn sync_chain_mmr(
554 &self,
555 current_block_height: BlockNumber,
556 upper_bound: SyncTarget,
557 ) -> Result<ChainMmrInfo, RpcError> {
558 let finality_level: proto::rpc::FinalityLevel = upper_bound.into();
559
560 let request = proto::rpc::SyncChainMmrRequest {
561 current_client_block_height: current_block_height.as_u32(),
562 finality_level: finality_level.into(),
563 };
564
565 let response = self
566 .call_with_retry(RpcEndpoint::SyncChainMmr, |mut rpc_api| {
567 Box::pin(async move { rpc_api.sync_chain_mmr(request).await })
568 })
569 .await?;
570
571 response.into_inner().try_into()
572 }
573
574 async fn get_account(
587 &self,
588 account_id: AccountId,
589 request: GetAccountRequest,
590 ) -> Result<(BlockNumber, AccountProof), RpcError> {
591 let GetAccountRequest { storage, at, known_code, vault } = request;
592
593 let known_code_commitment = known_code.as_ref().map_or(EMPTY_WORD, AccountCode::commitment);
594 let mut known_codes_by_commitment: BTreeMap<Word, AccountCode> = BTreeMap::new();
595 if let Some(account_code) = known_code {
596 known_codes_by_commitment.insert(account_code.commitment(), account_code);
597 }
598
599 let requirements = match storage.clone() {
601 StorageMapFetch::Slots(reqs) => reqs,
602 StorageMapFetch::Skip | StorageMapFetch::All => AccountStorageRequirements::default(),
603 };
604
605 let account_details = if account_id.is_public() {
608 Some(AccountDetailRequest {
609 code_commitment: Some(known_code_commitment.into()),
610 asset_vault_commitment: vault.into(),
611 storage_request: storage.into(),
612 })
613 } else {
614 None
615 };
616
617 let block_num = match at {
618 AccountStateAt::Block(number) => Some(number.into()),
619 AccountStateAt::ChainTip => None,
620 };
621
622 let proto_request = AccountRequest {
623 account_id: Some(account_id.into()),
624 block_num,
625 details: account_details,
626 };
627
628 let response = self
629 .call_with_retry(RpcEndpoint::GetAccount, |mut rpc_api| {
630 let request = proto_request.clone();
631 Box::pin(async move { rpc_api.get_account(request).await })
632 })
633 .await?
634 .into_inner();
635
636 let account_witness: AccountWitness = response
637 .witness
638 .ok_or(RpcError::ExpectedDataMissing("AccountWitness".to_string()))?
639 .try_into()?;
640
641 let response_block_num: BlockNumber = response
642 .block_num
643 .ok_or(RpcError::ExpectedDataMissing("response block num".to_string()))?
644 .block_num
645 .into();
646
647 if let Some(requested) = block_num
648 && requested.block_num != response_block_num.as_u32()
649 {
650 return Err(RpcError::InvalidResponse(format!(
651 "node returned header for block {} but block {} was requested",
652 response_block_num.as_u32(),
653 requested.block_num
654 )));
655 }
656
657 let headers = if account_witness.id().is_public() {
659 let details = response
660 .details
661 .ok_or(RpcError::ExpectedDataMissing("Account.Details".to_string()))?
662 .into_domain(&known_codes_by_commitment, &requirements)?;
663
664 Some(details)
665 } else {
666 None
667 };
668
669 let proof = AccountProof::new(account_witness, headers)
670 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
671
672 Ok((response_block_num, proof))
673 }
674
675 async fn sync_notes(
681 &self,
682 block_from: BlockNumber,
683 block_to: BlockNumber,
684 note_tags: &BTreeSet<NoteTag>,
685 ) -> Result<Vec<NoteSyncBlock>, RpcError> {
686 if note_tags.is_empty() {
687 return Ok(Vec::new());
688 }
689
690 let limits = self.get_rpc_limits().await?;
691 let tags: Vec<NoteTag> = note_tags.iter().copied().collect();
692
693 let mut merged_blocks: BTreeMap<BlockNumber, NoteSyncBlock> = BTreeMap::new();
696
697 for chunk in tags.chunks(limits.note_tags_limit as usize) {
698 let proto_tags: Vec<u32> = chunk.iter().map(|&t| t.into()).collect();
699 let requested_tags: BTreeSet<NoteTag> = chunk.iter().copied().collect();
700 let mut pagination = BlockPagination::new(block_from, block_to);
701
702 loop {
703 let request = proto::rpc::SyncNotesRequest {
704 block_range: Some(BlockRange {
705 block_from: pagination.current_block_from().as_u32(),
706 block_to: block_to.as_u32(),
707 }),
708 note_tags: proto_tags.clone(),
709 };
710
711 let response = self
712 .call_with_retry(RpcEndpoint::SyncNotes, |mut rpc_api| {
713 let request = request.clone();
714 Box::pin(async move { rpc_api.sync_notes(request).await })
715 })
716 .await?
717 .into_inner();
718
719 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
720 "SyncNotesResponse.pagination_info".to_owned(),
721 ))?;
722 let page_chain_tip = BlockNumber::from(page.chain_tip);
723 let page_block_to = BlockNumber::from(page.block_num);
724
725 for proto_block in response.blocks {
726 let block: NoteSyncBlock = proto_block.try_into()?;
727 ensure_requested_tags(
728 &requested_tags,
729 block.notes.values().map(CommittedNote::tag),
730 )?;
731 let bn = block.block_header.block_num();
732 if let Some(existing) = merged_blocks.get_mut(&bn) {
733 for (id, note) in block.notes {
734 existing.notes.entry(id).or_insert(note);
735 }
736 } else {
737 merged_blocks.insert(bn, block);
738 }
739 }
740
741 match pagination.advance(page_block_to, page_chain_tip)? {
742 PaginationResult::Continue => {},
743 PaginationResult::Done { .. } => break,
744 }
745 }
746 }
747
748 Ok(merged_blocks.into_values().collect())
749 }
750
751 async fn sync_nullifiers(
752 &self,
753 prefixes: &[u16],
754 block_from: BlockNumber,
755 block_to: BlockNumber,
756 ) -> Result<Vec<NullifierUpdate>, RpcError> {
757 let limits = self.get_rpc_limits().await?;
758 let mut all_nullifiers = BTreeSet::new();
759
760 for chunk in prefixes.chunks(limits.nullifiers_limit as usize) {
763 let proto_prefixes: Vec<u32> = chunk.iter().map(|&x| u32::from(x)).collect();
764 let requested_prefixes: BTreeSet<u16> = chunk.iter().copied().collect();
765 let mut pagination = BlockPagination::new(block_from, block_to);
766
767 loop {
768 let request = proto::rpc::SyncNullifiersRequest {
769 nullifiers: proto_prefixes.clone(),
770 prefix_len: 16,
771 block_range: Some(BlockRange {
772 block_from: pagination.current_block_from().as_u32(),
773 block_to: pagination.block_to().as_u32(),
774 }),
775 };
776
777 let response = self
778 .call_with_retry(RpcEndpoint::SyncNullifiers, |mut rpc_api| {
779 let request = request.clone();
780 Box::pin(async move { rpc_api.sync_nullifiers(request).await })
781 })
782 .await?
783 .into_inner();
784
785 let batch_nullifiers = response
786 .nullifiers
787 .iter()
788 .map(TryFrom::try_from)
789 .collect::<Result<Vec<NullifierUpdate>, _>>()
790 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
791
792 ensure_requested_nullifiers(&requested_prefixes, &batch_nullifiers)?;
793 all_nullifiers.extend(batch_nullifiers);
794
795 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
796 "SyncNullifiersResponse.pagination_info".to_owned(),
797 ))?;
798
799 match pagination.advance(page.block_num.into(), page.chain_tip.into())? {
800 PaginationResult::Continue => {},
801 PaginationResult::Done { .. } => break,
802 }
803 }
804 }
805 Ok(all_nullifiers.into_iter().collect::<Vec<_>>())
806 }
807
808 async fn get_block_by_number(
809 &self,
810 block_num: BlockNumber,
811 include_proof: bool,
812 ) -> Result<ProvenBlock, RpcError> {
813 let request = proto::blockchain::BlockRequest {
814 block_num: block_num.as_u32(),
815 include_proof: Some(include_proof),
816 };
817
818 let response = self
819 .call_with_retry(RpcEndpoint::GetBlockByNumber, |mut rpc_api| {
820 Box::pin(async move { rpc_api.get_block_by_number(request).await })
821 })
822 .await?;
823
824 let response = response.into_inner();
825 let block =
826 ProvenBlock::read_from_bytes(&response.block.ok_or(RpcError::ExpectedDataMissing(
827 "GetBlockByNumberResponse.block".to_string(),
828 ))?)?;
829
830 if block.header().block_num() != block_num {
831 return Err(RpcError::InvalidResponse(format!(
832 "node returned header for block {} but block {block_num} was requested",
833 block.header().block_num(),
834 )));
835 }
836
837 Ok(block)
838 }
839
840 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
841 let request = proto::note::NoteScriptRoot { root: Some(root.into()) };
842
843 let response = self
844 .call_with_retry(RpcEndpoint::GetNoteScriptByRoot, |mut rpc_api| {
845 Box::pin(async move { rpc_api.get_note_script_by_root(request).await })
846 })
847 .await?;
848
849 let Some(script) = response.into_inner().script else {
851 return Ok(None);
852 };
853 let note_script = NoteScript::try_from(script)?;
854
855 let fetched_root = note_script.root();
856 if Word::from(fetched_root) != root {
857 return Err(RpcError::InvalidResponse(format!(
858 "node returned note script with root {fetched_root} for requested root {root}",
859 )));
860 }
861
862 Ok(Some(note_script))
863 }
864
865 async fn sync_storage_maps(
866 &self,
867 block_from: BlockNumber,
868 block_to: BlockNumber,
869 account_id: AccountId,
870 ) -> Result<StorageMapInfo, RpcError> {
871 let mut pagination = BlockPagination::new(block_from, block_to);
872 let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
873
874 let (chain_tip, block_number) = loop {
875 let request = proto::rpc::SyncAccountStorageMapsRequest {
876 block_range: Some(BlockRange {
877 block_from: pagination.current_block_from().as_u32(),
878 block_to: block_to.as_u32(),
879 }),
880 account_id: Some(account_id.into()),
881 };
882 let response = self
883 .call_with_retry(RpcEndpoint::SyncStorageMaps, |mut rpc_api| {
884 let request = request.clone();
885 Box::pin(async move { rpc_api.sync_account_storage_maps(request).await })
886 })
887 .await?;
888 let page = StorageMapInfo::try_from(response.into_inner())?;
889
890 for (slot_name, entries) in page.map_entries {
891 map_entries
892 .entry(slot_name)
893 .or_default()
894 .as_map_mut()
895 .extend(entries.into_map());
896 }
897
898 match pagination.advance(page.block_number, page.chain_tip)? {
899 PaginationResult::Continue => {},
900 PaginationResult::Done {
901 chain_tip: final_chain_tip,
902 block_num: final_block_num,
903 } => break (final_chain_tip, final_block_num),
904 }
905 };
906
907 Ok(StorageMapInfo { chain_tip, block_number, map_entries })
908 }
909
910 async fn sync_account_vault(
911 &self,
912 block_from: BlockNumber,
913 block_to: BlockNumber,
914 account_id: AccountId,
915 ) -> Result<AccountVaultInfo, RpcError> {
916 let mut pagination = BlockPagination::new(block_from, block_to);
917 let mut vault_patch = AccountVaultPatch::default();
918
919 let (chain_tip, block_number) = loop {
920 let request = proto::rpc::SyncAccountVaultRequest {
921 block_range: Some(BlockRange {
922 block_from: pagination.current_block_from().as_u32(),
923 block_to: block_to.as_u32(),
924 }),
925 account_id: Some(account_id.into()),
926 };
927 let response = self
928 .call_with_retry(RpcEndpoint::SyncAccountVault, |mut rpc_api| {
929 let request = request.clone();
930 Box::pin(async move { rpc_api.sync_account_vault(request).await })
931 })
932 .await?;
933 let page = AccountVaultInfo::try_from(response.into_inner())?;
934
935 vault_patch.merge(page.vault_patch);
936
937 match pagination.advance(page.block_number, page.chain_tip)? {
938 PaginationResult::Continue => {},
939 PaginationResult::Done {
940 chain_tip: final_chain_tip,
941 block_num: final_block_num,
942 } => break (final_chain_tip, final_block_num),
943 }
944 };
945
946 Ok(AccountVaultInfo { chain_tip, block_number, vault_patch })
947 }
948
949 async fn sync_transactions(
955 &self,
956 block_from: BlockNumber,
957 block_to: BlockNumber,
958 account_ids: Vec<AccountId>,
959 ) -> Result<Vec<TransactionRecord>, RpcError> {
960 if account_ids.is_empty() {
961 return Ok(Vec::new());
962 }
963
964 let limits = self.get_rpc_limits().await?;
965 let mut transactions: Vec<TransactionRecord> = Vec::new();
966
967 for chunk in account_ids.chunks(limits.account_ids_limit as usize) {
968 let proto_account_ids: Vec<_> = chunk.iter().map(|acc_id| (*acc_id).into()).collect();
969 let mut pagination = BlockPagination::new(block_from, block_to);
970
971 loop {
972 let request = proto::rpc::SyncTransactionsRequest {
973 block_range: Some(BlockRange {
974 block_from: pagination.current_block_from().as_u32(),
975 block_to: block_to.as_u32(),
976 }),
977 account_ids: proto_account_ids.clone(),
978 };
979
980 let response = self
981 .call_with_retry(RpcEndpoint::SyncTransactions, |mut rpc_api| {
982 let request = request.clone();
983 Box::pin(async move { rpc_api.sync_transactions(request).await })
984 })
985 .await?
986 .into_inner();
987
988 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
989 "SyncTransactionsResponse.pagination_info".to_owned(),
990 ))?;
991 let page_chain_tip = BlockNumber::from(page.chain_tip);
992 let page_block_to = BlockNumber::from(page.block_num);
993
994 for proto_tx in response.transactions {
995 transactions.push(TransactionRecord::try_from(proto_tx)?);
996 }
997
998 match pagination.advance(page_block_to, page_chain_tip)? {
999 PaginationResult::Continue => {},
1000 PaginationResult::Done { .. } => break,
1001 }
1002 }
1003 }
1004
1005 Ok(transactions)
1006 }
1007
1008 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
1009 let endpoint: Endpoint =
1010 Endpoint::try_from(self.endpoint.as_str()).map_err(RpcError::InvalidNodeEndpoint)?;
1011 Ok(endpoint.to_network_id())
1012 }
1013
1014 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
1015 if let Some(limits) = *self.limits.read() {
1017 return Ok(limits);
1018 }
1019
1020 let response = self
1022 .call_with_retry(RpcEndpoint::GetLimits, |mut rpc_api| {
1023 Box::pin(async move { rpc_api.get_limits(()).await })
1024 })
1025 .await?;
1026 let limits = RpcLimits::try_from(response.into_inner()).map_err(RpcError::from)?;
1027
1028 self.limits.write().replace(limits);
1030 Ok(limits)
1031 }
1032
1033 fn has_rpc_limits(&self) -> Option<RpcLimits> {
1034 *self.limits.read()
1035 }
1036
1037 async fn set_rpc_limits(&self, limits: RpcLimits) {
1038 self.limits.write().replace(limits);
1039 }
1040
1041 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
1042 GrpcClient::get_status_unversioned(self).await
1043 }
1044
1045 async fn get_network_note_status(
1046 &self,
1047 note_id: NoteId,
1048 ) -> Result<NetworkNoteStatusInfo, RpcError> {
1049 let request = proto::note::NoteId { id: Some(note_id.into()) };
1050
1051 let response = self
1052 .call_with_retry(RpcEndpoint::GetNetworkNoteStatus, |mut rpc_api| {
1053 Box::pin(async move { rpc_api.get_network_note_status(request).await })
1054 })
1055 .await?;
1056
1057 response.into_inner().try_into()
1058 }
1059}
1060
1061impl RpcError {
1065 pub fn from_grpc_error_with_context(
1066 endpoint: RpcEndpoint,
1067 status: Status,
1068 context: AcceptHeaderContext,
1069 ) -> Self {
1070 if let Some(accept_error) =
1071 AcceptHeaderError::try_from_message_with_context(status.message(), context)
1072 {
1073 return Self::AcceptHeaderError(accept_error);
1074 }
1075
1076 let endpoint_error = parse_node_error(&endpoint, status.details(), status.message());
1078
1079 let error_kind = GrpcError::from(&status);
1080 let source = Box::new(status) as Box<dyn Error + Send + Sync + 'static>;
1081
1082 Self::RequestError {
1083 endpoint,
1084 error_kind,
1085 endpoint_error,
1086 source: Some(source),
1087 }
1088 }
1089}
1090
1091impl From<&Status> for GrpcError {
1092 fn from(status: &Status) -> Self {
1093 GrpcError::from_code(status.code() as i32, Some(status.message().to_string()))
1094 }
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099 use core::slice;
1100 use std::boxed::Box;
1101 use std::collections::BTreeSet;
1102
1103 use miden_protocol::block::BlockNumber;
1104 use miden_protocol::note::{NoteId, NoteTag, Nullifier};
1105 use miden_protocol::{Felt, Word};
1106
1107 use super::{
1108 BlockPagination,
1109 DEFAULT_MAX_RESPONSE_SIZE_BYTES,
1110 GrpcClient,
1111 NullifierUpdate,
1112 PaginationResult,
1113 ensure_requested_note_ids,
1114 ensure_requested_nullifiers,
1115 ensure_requested_tags,
1116 };
1117 use crate::alloc::string::ToString;
1118 use crate::rpc::{Endpoint, NodeRpcClient, RpcError};
1119
1120 fn assert_send_sync<T: Send + Sync>() {}
1121
1122 #[test]
1123 fn is_send_sync() {
1124 assert_send_sync::<GrpcClient>();
1125 assert_send_sync::<Box<dyn NodeRpcClient>>();
1126 }
1127
1128 #[test]
1129 fn block_pagination_errors_when_block_num_goes_backwards() {
1130 let mut pagination = BlockPagination::new(10_u32.into(), 20_u32.into());
1131
1132 let res = pagination.advance(9_u32.into(), 20_u32.into());
1133 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1134 }
1135
1136 #[test]
1137 fn block_pagination_errors_after_max_iterations() {
1138 let mut pagination = BlockPagination::new(0_u32.into(), 10_000_u32.into());
1139 let chain_tip: BlockNumber = 10_000_u32.into();
1140
1141 for _ in 0..BlockPagination::MAX_ITERATIONS {
1142 let current = pagination.current_block_from();
1143 let res = pagination
1144 .advance(current, chain_tip)
1145 .expect("expected pagination to continue within iteration limit");
1146 assert!(matches!(res, PaginationResult::Continue));
1147 }
1148
1149 let res = pagination.advance(pagination.current_block_from(), chain_tip);
1150 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1151 }
1152
1153 #[test]
1154 fn block_pagination_stops_at_min_of_block_to_and_chain_tip() {
1155 let mut pagination = BlockPagination::new(0_u32.into(), 50_u32.into());
1157
1158 let res = pagination
1159 .advance(30_u32.into(), 30_u32.into())
1160 .expect("expected pagination to succeed");
1161
1162 assert!(matches!(
1163 res,
1164 PaginationResult::Done {
1165 chain_tip,
1166 block_num
1167 } if chain_tip.as_u32() == 30 && block_num.as_u32() == 30
1168 ));
1169 }
1170
1171 #[test]
1172 fn block_pagination_advances_cursor_by_one() {
1173 let mut pagination = BlockPagination::new(5_u32.into(), 100_u32.into());
1174
1175 let res = pagination
1176 .advance(5_u32.into(), 100_u32.into())
1177 .expect("expected pagination to succeed");
1178 assert!(matches!(res, PaginationResult::Continue));
1179 assert_eq!(pagination.current_block_from().as_u32(), 6);
1180 }
1181
1182 async fn dyn_trait_send_fut(client: Box<dyn NodeRpcClient>) {
1184 let res = client.get_block_header_by_number(None, false).await;
1186 assert!(res.is_ok());
1187 }
1188
1189 #[tokio::test]
1190 async fn future_is_send() {
1191 let endpoint = &Endpoint::devnet();
1192 let client = GrpcClient::new(endpoint, 10000);
1193 let client: Box<GrpcClient> = client.into();
1194 tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
1195 }
1196
1197 #[tokio::test]
1198 async fn set_genesis_commitment_sets_the_commitment_when_its_not_already_set() {
1199 let endpoint = &Endpoint::devnet();
1200 let client = GrpcClient::new(endpoint, 10000);
1201
1202 assert!(client.genesis_commitment.read().is_none());
1203
1204 let commitment = Word::default();
1205 client.set_genesis_commitment(commitment).await.unwrap();
1206
1207 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1208 }
1209
1210 #[tokio::test]
1211 async fn set_genesis_commitment_does_nothing_if_the_commitment_is_already_set() {
1212 let endpoint = &Endpoint::devnet();
1213 let client = GrpcClient::new(endpoint, 10000);
1214
1215 let initial_commitment = Word::default();
1216 client.set_genesis_commitment(initial_commitment).await.unwrap();
1217
1218 let new_commitment = Word::from([1u32, 2, 3, 4]);
1219 client.set_genesis_commitment(new_commitment).await.unwrap();
1220
1221 assert_eq!(client.genesis_commitment.read().unwrap(), initial_commitment);
1222 }
1223
1224 #[tokio::test]
1225 async fn set_genesis_commitment_updates_the_client_if_already_connected() {
1226 let endpoint = &Endpoint::devnet();
1227 let client = GrpcClient::new(endpoint, 10000);
1228
1229 client.connect().await.unwrap();
1231
1232 let commitment = Word::default();
1233 client.set_genesis_commitment(commitment).await.unwrap();
1234
1235 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1236 assert!(client.client.read().as_ref().is_some());
1237 }
1238
1239 #[test]
1240 fn with_bearer_auth_stores_token() {
1241 let endpoint = &Endpoint::devnet();
1242 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token-one".to_string());
1243
1244 assert_eq!(client.bearer_token.as_deref(), Some("token-one"));
1245 }
1246
1247 #[test]
1248 fn with_bearer_auth_overwrites_on_repeat_call() {
1249 let endpoint = &Endpoint::devnet();
1250 let client = GrpcClient::new(endpoint, 10000)
1251 .with_bearer_auth("token-one".to_string())
1252 .with_bearer_auth("token-two".to_string());
1253
1254 assert_eq!(client.bearer_token.as_deref(), Some("token-two"));
1256 }
1257
1258 #[tokio::test]
1259 async fn with_bearer_auth_surfaces_invalid_ascii_value_at_connect_time() {
1260 let endpoint = &Endpoint::devnet();
1264 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("bad\nvalue".to_string());
1265
1266 let err = client.connect().await.expect_err("expected invalid token to fail connect");
1267 assert!(
1268 matches!(err, RpcError::ConnectionError(_)),
1269 "expected ConnectionError, got {err:?}",
1270 );
1271 }
1272
1273 #[tokio::test]
1274 async fn with_bearer_auth_is_preserved_across_set_genesis_commitment() {
1275 let endpoint = &Endpoint::devnet();
1276 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token".to_string());
1277 client.connect().await.unwrap();
1278
1279 client.set_genesis_commitment(Word::default()).await.unwrap();
1280
1281 assert_eq!(client.bearer_token.as_deref(), Some("token"));
1283 assert!(client.client.read().as_ref().is_some());
1284 }
1285
1286 #[test]
1287 fn with_max_decoding_message_size_overrides_default() {
1288 let endpoint = &Endpoint::devnet();
1289
1290 let default_client = GrpcClient::new(endpoint, 10_000);
1292 assert_eq!(default_client.max_decoding_message_size, DEFAULT_MAX_RESPONSE_SIZE_BYTES);
1293
1294 let custom =
1296 GrpcClient::new(endpoint, 10_000).with_max_decoding_message_size(8 * 1024 * 1024);
1297 assert_eq!(custom.max_decoding_message_size, 8 * 1024 * 1024);
1298 }
1299
1300 #[tokio::test]
1312 #[ignore = "requires network access to public testnet"]
1313 async fn with_bearer_auth_does_not_break_real_rpc_against_testnet() {
1314 let endpoint = &Endpoint::testnet();
1315 let client = GrpcClient::new(endpoint, 10_000).with_bearer_auth("smoke-test".to_string());
1316
1317 let status = client
1318 .get_status_unversioned()
1319 .await
1320 .expect("testnet status with caller auth header must succeed");
1321 assert!(!status.version.is_empty(), "status must include a server version");
1322 }
1323
1324 fn nullifier_with_prefix(prefix: u16) -> Nullifier {
1325 Nullifier::from_raw(Word::new([
1326 Felt::ZERO,
1327 Felt::ZERO,
1328 Felt::ZERO,
1329 Felt::new_unchecked(u64::from(prefix) << 48),
1330 ]))
1331 }
1332
1333 #[test]
1334 fn verify_requested_nullifiers_rejects_unrequested_prefix() {
1335 let requested = NullifierUpdate {
1336 nullifier: nullifier_with_prefix(0x1234),
1337 block_num: 1u32.into(),
1338 };
1339 let unrequested = NullifierUpdate {
1340 nullifier: nullifier_with_prefix(0xabcd),
1341 block_num: 2u32.into(),
1342 };
1343
1344 let requested_prefixes: BTreeSet<u16> = BTreeSet::from([0x1234]);
1345
1346 ensure_requested_nullifiers(&requested_prefixes, slice::from_ref(&requested))
1347 .expect("requested prefix must be accepted");
1348
1349 let err = ensure_requested_nullifiers(&requested_prefixes, &[requested, unrequested])
1350 .expect_err("unrequested prefix must be rejected");
1351 assert!(matches!(err, RpcError::InvalidResponse(_)));
1352 }
1353
1354 #[test]
1355 fn ensure_requested_tags_rejects_unrequested() {
1356 let requested = NoteTag::new(1);
1357 let other = NoteTag::new(2);
1358 let requested_set = BTreeSet::from([requested]);
1359
1360 ensure_requested_tags(&requested_set, [requested]).expect("requested tag must be accepted");
1361
1362 let err = ensure_requested_tags(&requested_set, [other])
1363 .expect_err("unrequested tag must be rejected");
1364 assert!(matches!(err, RpcError::InvalidResponse(_)));
1365 }
1366
1367 fn note_id(n: u32) -> NoteId {
1368 NoteId::from_raw(Word::from([n, 0, 0, 0]))
1369 }
1370
1371 #[test]
1372 fn ensure_requested_note_ids_rejects_unrequested() {
1373 let requested = note_id(1);
1374 let other = note_id(2);
1375 let requested_set = BTreeSet::from([requested]);
1376
1377 ensure_requested_note_ids(&requested_set, [requested])
1378 .expect("requested note id must be accepted");
1379
1380 let err = ensure_requested_note_ids(&requested_set, [other])
1381 .expect_err("unrequested note id must be rejected");
1382 assert!(matches!(err, RpcError::InvalidResponse(_)));
1383 }
1384}