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_objects::DecodeMessageExt;
14use miden_protocol::account::{
15 AccountCode,
16 AccountId,
17 AccountVaultPatch,
18 StorageMapPatchEntries,
19 StorageSlotName,
20};
21use miden_protocol::address::NetworkId;
22use miden_protocol::batch::{ProposedBatch, ProvenBatch};
23use miden_protocol::block::account_tree::AccountWitness;
24use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock};
25use miden_protocol::crypto::merkle::MerklePath;
26use miden_protocol::crypto::merkle::mmr::{Forest, MmrPath, MmrProof};
27use miden_protocol::note::{NoteId, NoteScript, NoteTag};
28use miden_protocol::transaction::ProvenTransaction;
29use miden_protocol::vm::ExecutionProof;
30use miden_protocol::{EMPTY_WORD, Word};
31use miden_tx::utils::sync::RwLock;
32use tonic::Status;
33use tracing::{info, warn};
34
35use super::domain::account::{
36 AccountProof,
37 AccountStorageRequirements,
38 GetAccountRequest,
39 StorageMapFetch,
40};
41use super::domain::note::{FetchedNote, SyncNotesBlock};
42use super::domain::nullifier::NullifierUpdate;
43use super::encryption::{
44 AttestedTransactionEncryptionKey,
45 NextTransactionEncryptionKey,
46 SealedTransactionInputs,
47 ValidatorAttestation,
48};
49use super::generated::rpc::AccountRequest;
50use super::generated::rpc::account_request::AccountDetailRequest;
51use super::{Endpoint, NodeRpcClient, RpcEndpoint, RpcError, RpcStatusInfo};
52use crate::rpc::domain::account_vault::AccountVaultInfo;
53use crate::rpc::domain::limits::RpcLimits;
54use crate::rpc::domain::status::NetworkNoteStatusInfo;
55use crate::rpc::domain::storage_map::StorageMapInfo;
56use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
57use crate::rpc::domain::transaction::TransactionRecord;
58use crate::rpc::errors::node::parse_node_error;
59use crate::rpc::errors::{AcceptHeaderContext, AcceptHeaderError, GrpcError, RpcConversionError};
60use crate::rpc::generated::rpc::BlockRange;
61use crate::rpc::{AccountStateAt, generated as proto};
62
63mod api_client;
64mod retry;
65
66use api_client::api_client_wrapper::ApiClient;
67
68struct BlockPagination {
70 current_block_from: BlockNumber,
71 block_to: BlockNumber,
72 iterations: u32,
73}
74
75enum PaginationResult {
76 Continue,
77 Done {
78 chain_tip: BlockNumber,
79 block_num: BlockNumber,
80 },
81}
82
83impl BlockPagination {
84 const MAX_ITERATIONS: u32 = 1000;
89
90 fn new(block_from: BlockNumber, block_to: BlockNumber) -> Self {
91 Self {
92 current_block_from: block_from,
93 block_to,
94 iterations: 0,
95 }
96 }
97
98 fn current_block_from(&self) -> BlockNumber {
99 self.current_block_from
100 }
101
102 fn block_to(&self) -> BlockNumber {
103 self.block_to
104 }
105
106 fn advance(
107 &mut self,
108 block_num: BlockNumber,
109 chain_tip: BlockNumber,
110 ) -> Result<PaginationResult, RpcError> {
111 if self.iterations >= Self::MAX_ITERATIONS {
112 return Err(RpcError::PaginationError(
113 "too many pagination iterations, possible infinite loop".to_owned(),
114 ));
115 }
116 self.iterations += 1;
117
118 if block_num < self.current_block_from {
119 return Err(RpcError::PaginationError(
120 "invalid pagination: block_num went backwards".to_owned(),
121 ));
122 }
123
124 let target_block = self.block_to.min(chain_tip);
125
126 if block_num >= target_block {
127 return Ok(PaginationResult::Done { chain_tip, block_num });
128 }
129
130 self.current_block_from = BlockNumber::from(block_num.as_u32().saturating_add(1));
131
132 Ok(PaginationResult::Continue)
133 }
134}
135
136const DEFAULT_MAX_RESPONSE_SIZE_BYTES: usize = 4 * 1024 * 1024 * 115 / 100;
142
143pub struct GrpcClient {
153 client: RwLock<Option<ApiClient>>,
155 endpoint: String,
157 timeout_ms: u64,
159 genesis_commitment: RwLock<Option<Word>>,
161 limits: RwLock<Option<RpcLimits>>,
163 max_retries: u32,
165 retry_interval_ms: u64,
167 bearer_token: Option<String>,
171 max_decoding_message_size: usize,
174}
175
176impl GrpcClient {
177 pub fn new(endpoint: &Endpoint, timeout_ms: u64) -> GrpcClient {
180 GrpcClient {
181 client: RwLock::new(None),
182 endpoint: endpoint.to_string(),
183 timeout_ms,
184 genesis_commitment: RwLock::new(None),
185 limits: RwLock::new(None),
186 max_retries: retry::DEFAULT_MAX_RETRIES,
187 retry_interval_ms: retry::DEFAULT_RETRY_INTERVAL_MS,
188 bearer_token: None,
189 max_decoding_message_size: DEFAULT_MAX_RESPONSE_SIZE_BYTES,
190 }
191 }
192
193 #[must_use]
196 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
197 self.max_retries = max_retries;
198 self
199 }
200
201 #[must_use]
204 pub fn with_retry_interval_ms(mut self, retry_interval_ms: u64) -> Self {
205 self.retry_interval_ms = retry_interval_ms;
206 self
207 }
208
209 #[must_use]
216 pub fn with_max_decoding_message_size(mut self, max_decoding_message_size: usize) -> Self {
217 self.max_decoding_message_size = max_decoding_message_size;
218 self
219 }
220
221 #[must_use]
245 pub fn with_bearer_auth(mut self, token: String) -> Self {
246 self.bearer_token = Some(token);
247 self
248 }
249
250 async fn ensure_connected(&self) -> Result<ApiClient, RpcError> {
253 if self.client.read().is_none() {
254 self.connect().await?;
255 }
256
257 Ok(self.client.read().as_ref().expect("rpc_api should be initialized").clone())
258 }
259
260 async fn connect(&self) -> Result<(), RpcError> {
263 let genesis_commitment = *self.genesis_commitment.read();
264 let new_client = ApiClient::new_client(
265 self.endpoint.clone(),
266 self.timeout_ms,
267 genesis_commitment,
268 self.bearer_token.clone(),
269 self.max_decoding_message_size,
270 )
271 .await?;
272 let mut client = self.client.write();
273 client.replace(new_client);
274
275 Ok(())
276 }
277
278 fn rpc_error_from_status(&self, endpoint: RpcEndpoint, status: Status) -> RpcError {
279 let genesis_commitment = self
280 .genesis_commitment
281 .read()
282 .as_ref()
283 .map_or_else(|| "none".to_string(), Word::to_hex);
284 let context = AcceptHeaderContext {
285 client_version: env!("CARGO_PKG_VERSION").to_string(),
286 genesis_commitment,
287 };
288 RpcError::from_grpc_error_with_context(endpoint, status, context)
289 }
290
291 async fn call_with_retry<T: Send + 'static>(
304 &self,
305 endpoint: RpcEndpoint,
306 mut call: impl FnMut(ApiClient) -> RpcFuture<Result<tonic::Response<T>, Status>>,
307 ) -> Result<tonic::Response<T>, RpcError> {
308 let mut retry_state =
309 retry::RetryState::new(endpoint, self.max_retries, self.retry_interval_ms);
310
311 loop {
312 let rpc_api = self.ensure_connected().await?;
313
314 match call(rpc_api).await {
315 Ok(response) => return Ok(response),
316 Err(status) if retry_state.should_retry(&status).await => {},
317 Err(status) => return Err(self.rpc_error_from_status(endpoint, status)),
318 }
319 }
320 }
321
322 pub async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
328 let mut rpc_api = ApiClient::new_client_without_accept_header(
329 self.endpoint.clone(),
330 self.timeout_ms,
331 self.bearer_token.clone(),
332 self.max_decoding_message_size,
333 )
334 .await?;
335 rpc_api
336 .status(())
337 .await
338 .map_err(|status| self.rpc_error_from_status(RpcEndpoint::Status, status))
339 .map(tonic::Response::into_inner)
340 .and_then(RpcStatusInfo::try_from)
341 }
342}
343
344#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
345#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
346impl NodeRpcClient for GrpcClient {
347 fn has_genesis_commitment(&self) -> Option<Word> {
352 *self.genesis_commitment.read()
353 }
354
355 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
356 if self.genesis_commitment.read().is_some() {
358 return Ok(());
360 }
361
362 self.genesis_commitment.write().replace(commitment);
364
365 let mut client_guard = self.client.write();
368 if let Some(client) = client_guard.as_mut() {
369 client.set_genesis_commitment(commitment);
370 }
371
372 Ok(())
373 }
374
375 async fn get_transaction_encryption_key(
376 &self,
377 ) -> Result<AttestedTransactionEncryptionKey, RpcError> {
378 let api_response = self
379 .call_with_retry(RpcEndpoint::GetTransactionEncryptionKey, |mut rpc_api| {
380 Box::pin(async move { rpc_api.get_transaction_encryption_key(()).await })
381 })
382 .await?;
383 let response = api_response.into_inner();
384
385 let attestations = response
389 .attestations
390 .into_iter()
391 .filter_map(|attestation| {
392 let validator_key =
393 attestation.validator_public_key.and_then(|key| key.decode_and_verify().ok());
394 let signature =
395 attestation.signature.and_then(|signature| signature.decode_and_verify().ok());
396 let decoded = validator_key.zip(signature).map(|(validator_key, signature)| {
397 ValidatorAttestation { validator_key, signature }
398 });
399 if decoded.is_none() {
400 warn!(
401 "skipping a transaction encryption key attestation that failed to decode"
402 );
403 }
404 decoded
405 })
406 .collect::<Vec<_>>();
407
408 let wire_scheme = |scheme: i32| {
411 u32::try_from(scheme)
412 .map_err(|_| RpcError::InvalidResponse(format!("negative IES scheme '{scheme}'")))
413 };
414
415 let next_key = response
416 .next_key
417 .map(|next| {
418 Ok::<_, RpcError>(NextTransactionEncryptionKey {
419 scheme: wire_scheme(next.scheme)?,
420 key_id: next.key_id,
421 public_key: next.public_key,
422 rotation_block_num: next.rotation_block_num.into(),
423 })
424 })
425 .transpose()?;
426
427 Ok(AttestedTransactionEncryptionKey {
428 scheme: wire_scheme(response.scheme)?,
429 key_id: response.key_id,
430 public_key: response.public_key,
431 attestations,
432 next_key,
433 })
434 }
435
436 async fn submit_proven_transaction(
437 &self,
438 proven_transaction: ProvenTransaction,
439 sealed_transaction_inputs: SealedTransactionInputs,
440 ) -> Result<BlockNumber, RpcError> {
441 let request = proto::submission::ProvenTransactionSubmission {
442 transaction: Some((&proven_transaction).into()),
443 sealed_transaction_inputs: Some(sealed_transaction_inputs.into()),
444 };
445
446 let api_response = self
447 .call_with_retry(RpcEndpoint::SubmitProvenTx, |mut rpc_api| {
448 let request = request.clone();
449 Box::pin(async move { rpc_api.submit_proven_tx(request).await })
450 })
451 .await?;
452
453 Ok(BlockNumber::from(api_response.into_inner().block_num))
454 }
455
456 async fn submit_proven_batch(
457 &self,
458 proven_batch: ProvenBatch,
459 proposed_batch: ProposedBatch,
460 sealed_transaction_inputs: Vec<SealedTransactionInputs>,
461 ) -> Result<BlockNumber, RpcError> {
462 let request = proto::submission::TransactionBatch {
463 batch: Some((&proven_batch).into()),
464 proposed_batch: Some((&proposed_batch).into()),
465 sealed_transaction_inputs: sealed_transaction_inputs
466 .into_iter()
467 .map(Into::into)
468 .collect(),
469 };
470
471 let api_response = self
472 .call_with_retry(RpcEndpoint::SubmitProvenBatch, |mut rpc_api| {
473 let request = request.clone();
474 Box::pin(async move { rpc_api.submit_proven_tx_batch(request).await })
475 })
476 .await?;
477
478 Ok(BlockNumber::from(api_response.into_inner().block_num))
479 }
480
481 async fn get_block_header_by_number(
482 &self,
483 block_num: Option<BlockNumber>,
484 include_mmr_proof: bool,
485 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
486 let request = proto::rpc::BlockHeaderByNumberRequest {
487 block_num: block_num.as_ref().map(BlockNumber::as_u32),
488 include_mmr_proof: Some(include_mmr_proof),
489 include_protocol_config: None,
490 };
491
492 info!("Calling GetBlockHeaderByNumber: {:?}", request);
493
494 let api_response = self
495 .call_with_retry(RpcEndpoint::GetBlockHeaderByNumber, |mut rpc_api| {
496 Box::pin(async move { rpc_api.get_block_header_by_number(request).await })
497 })
498 .await?;
499
500 let response = api_response.into_inner();
501
502 let block_header: BlockHeader = response
503 .block_header
504 .ok_or(RpcError::ExpectedDataMissing("BlockHeader".into()))?
505 .decode_and_build_unchecked()?;
506
507 let mmr_proof = if include_mmr_proof {
508 let forest = response
509 .chain_length
510 .ok_or(RpcError::ExpectedDataMissing("ChainLength".into()))?;
511 let merkle_path: MerklePath = response
512 .mmr_path
513 .ok_or(RpcError::ExpectedDataMissing("MmrPath".into()))?
514 .decode_and_verify()?;
515
516 let forest_size = usize::try_from(forest).expect("u64 should fit in usize");
517 let forest = Forest::new(forest_size).map_err(|_| {
518 RpcError::InvalidResponse(format!("invalid forest size: {forest_size}"))
519 })?;
520 Some(MmrProof::new(
521 MmrPath::new(forest, block_header.block_num().as_usize(), merkle_path),
522 block_header.commitment(),
523 ))
524 } else {
525 None
526 };
527
528 Ok((block_header, mmr_proof))
529 }
530
531 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
532 let limits = self.get_rpc_limits().await?;
533 let mut notes = Vec::with_capacity(note_ids.len());
534 for chunk in note_ids.chunks(limits.note_ids_limit as usize) {
535 let request = proto::rpc::NotesByIdRequest {
536 note_ids: chunk.iter().map(proto::note::NoteId::from).collect(),
537 };
538
539 let api_response = self
540 .call_with_retry(RpcEndpoint::GetNotesById, |mut rpc_api| {
541 let request = request.clone();
542 Box::pin(async move { rpc_api.get_notes_by_id(request).await })
543 })
544 .await?;
545
546 let response_notes = api_response
547 .into_inner()
548 .notes
549 .into_iter()
550 .map(FetchedNote::try_from)
551 .collect::<Result<Vec<FetchedNote>, RpcConversionError>>()?;
552
553 notes.extend(response_notes);
554 }
555 Ok(notes)
556 }
557
558 async fn sync_chain_mmr(
559 &self,
560 current_block_height: BlockNumber,
561 upper_bound: SyncTarget,
562 ) -> Result<ChainMmrInfo, RpcError> {
563 let finality_level: proto::rpc::FinalityLevel = upper_bound.into();
564
565 let request = proto::rpc::SyncChainMmrRequest {
566 current_client_block_height: current_block_height.as_u32(),
567 finality_level: finality_level.into(),
568 };
569
570 let response = self
571 .call_with_retry(RpcEndpoint::SyncChainMmr, |mut rpc_api| {
572 Box::pin(async move { rpc_api.sync_chain_mmr(request).await })
573 })
574 .await?;
575
576 response.into_inner().try_into()
577 }
578
579 async fn get_account(
591 &self,
592 account_id: AccountId,
593 request: GetAccountRequest,
594 ) -> Result<(BlockNumber, AccountProof), RpcError> {
595 let GetAccountRequest { storage, at, known_code, vault } = request;
596
597 let known_code_commitment = known_code.as_ref().map_or(EMPTY_WORD, AccountCode::commitment);
598 let mut known_codes_by_commitment: BTreeMap<Word, AccountCode> = BTreeMap::new();
599 if let Some(account_code) = known_code {
600 known_codes_by_commitment.insert(account_code.commitment(), account_code);
601 }
602
603 let requirements = match storage.clone() {
605 StorageMapFetch::Slots(reqs) => reqs,
606 StorageMapFetch::Skip | StorageMapFetch::All => AccountStorageRequirements::default(),
607 };
608
609 let account_details = if account_id.is_public() {
612 Some(AccountDetailRequest {
613 code_commitment: Some(known_code_commitment.into()),
614 asset_vault_commitment: vault.into(),
615 storage_request: storage.into(),
616 })
617 } else {
618 None
619 };
620
621 let block_num = match at {
622 AccountStateAt::Block(number) => Some(number.into()),
623 AccountStateAt::ChainTip => None,
624 };
625
626 let proto_request = AccountRequest {
627 account_id: Some(account_id.into()),
628 block_num,
629 details: account_details,
630 };
631
632 let response = self
633 .call_with_retry(RpcEndpoint::GetAccount, |mut rpc_api| {
634 let request = proto_request.clone();
635 Box::pin(async move { rpc_api.get_account(request).await })
636 })
637 .await?
638 .into_inner();
639
640 let account_witness: AccountWitness = response
641 .witness
642 .ok_or(RpcError::ExpectedDataMissing("AccountWitness".to_string()))?
643 .decode_and_verify()?;
644
645 let response_block_num: BlockNumber = response
646 .block_num
647 .ok_or(RpcError::ExpectedDataMissing("response block num".to_string()))?
648 .block_num
649 .into();
650
651 let headers = if account_witness.id().is_public() {
653 let details = response
654 .details
655 .ok_or(RpcError::ExpectedDataMissing("Account.Details".to_string()))?
656 .into_domain(&known_codes_by_commitment, &requirements)?;
657
658 Some(details)
659 } else {
660 None
661 };
662
663 let proof = AccountProof::new(account_witness, headers)
664 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
665
666 Ok((response_block_num, proof))
667 }
668
669 async fn sync_notes(
675 &self,
676 block_from: BlockNumber,
677 block_to: BlockNumber,
678 note_tags: &BTreeSet<NoteTag>,
679 ) -> Result<Vec<SyncNotesBlock>, RpcError> {
680 if note_tags.is_empty() {
681 return Ok(Vec::new());
682 }
683
684 let limits = self.get_rpc_limits().await?;
685 let tags: Vec<NoteTag> = note_tags.iter().copied().collect();
686
687 let mut merged_blocks: BTreeMap<BlockNumber, SyncNotesBlock> = BTreeMap::new();
690
691 for chunk in tags.chunks(limits.note_tags_limit as usize) {
692 let proto_tags: Vec<u32> = chunk.iter().map(|&t| t.into()).collect();
693 let mut pagination = BlockPagination::new(block_from, block_to);
694
695 loop {
696 let request = proto::rpc::SyncNotesRequest {
697 block_range: Some(BlockRange {
698 block_from: pagination.current_block_from().as_u32(),
699 block_to: block_to.as_u32(),
700 }),
701 note_tags: proto_tags.clone(),
702 };
703
704 let response = self
705 .call_with_retry(RpcEndpoint::SyncNotes, |mut rpc_api| {
706 let request = request.clone();
707 Box::pin(async move { rpc_api.sync_notes(request).await })
708 })
709 .await?
710 .into_inner();
711
712 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
713 "SyncNotesResponse.pagination_info".to_owned(),
714 ))?;
715 let page_chain_tip = BlockNumber::from(page.chain_tip);
716 let page_block_to = BlockNumber::from(page.block_num);
717
718 for proto_block in response.blocks {
719 let block: SyncNotesBlock = proto_block.try_into()?;
720 let bn = block.block_header.block_num();
721 if let Some(existing) = merged_blocks.get_mut(&bn) {
722 for (id, note) in block.notes {
723 existing.notes.entry(id).or_insert(note);
724 }
725 } else {
726 merged_blocks.insert(bn, block);
727 }
728 }
729
730 match pagination.advance(page_block_to, page_chain_tip)? {
731 PaginationResult::Continue => {},
732 PaginationResult::Done { .. } => break,
733 }
734 }
735 }
736
737 Ok(merged_blocks.into_values().collect())
738 }
739
740 async fn sync_nullifiers(
741 &self,
742 prefixes: &[u16],
743 block_from: BlockNumber,
744 block_to: BlockNumber,
745 ) -> Result<Vec<NullifierUpdate>, RpcError> {
746 let limits = self.get_rpc_limits().await?;
747 let mut all_nullifiers = BTreeSet::new();
748
749 for chunk in prefixes.chunks(limits.nullifiers_limit as usize) {
752 let proto_prefixes: Vec<u32> = chunk.iter().map(|&x| u32::from(x)).collect();
753 let mut pagination = BlockPagination::new(block_from, block_to);
754
755 loop {
756 let request = proto::rpc::SyncNullifiersRequest {
757 nullifiers: proto_prefixes.clone(),
758 prefix_len: 16,
759 block_range: Some(BlockRange {
760 block_from: pagination.current_block_from().as_u32(),
761 block_to: pagination.block_to().as_u32(),
762 }),
763 };
764
765 let response = self
766 .call_with_retry(RpcEndpoint::SyncNullifiers, |mut rpc_api| {
767 let request = request.clone();
768 Box::pin(async move { rpc_api.sync_nullifiers(request).await })
769 })
770 .await?
771 .into_inner();
772
773 let batch_nullifiers = response
774 .nullifiers
775 .iter()
776 .map(TryFrom::try_from)
777 .collect::<Result<Vec<NullifierUpdate>, _>>()
778 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
779
780 all_nullifiers.extend(batch_nullifiers);
781
782 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
783 "SyncNullifiersResponse.pagination_info".to_owned(),
784 ))?;
785
786 match pagination.advance(page.block_num.into(), page.chain_tip.into())? {
787 PaginationResult::Continue => {},
788 PaginationResult::Done { .. } => break,
789 }
790 }
791 }
792 Ok(all_nullifiers.into_iter().collect::<Vec<_>>())
793 }
794
795 async fn get_block_by_number(
796 &self,
797 block_num: BlockNumber,
798 include_proof: bool,
799 ) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError> {
800 let request = proto::rpc::BlockRequest {
801 block_num: block_num.as_u32(),
802 include_proof: Some(include_proof),
803 };
804
805 let response = self
806 .call_with_retry(RpcEndpoint::GetBlockByNumber, |mut rpc_api| {
807 Box::pin(async move { rpc_api.get_block_by_number(request).await })
808 })
809 .await?;
810
811 decode_block_response(response.into_inner())
812 }
813
814 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
815 let request = proto::rpc::NoteScriptByRootRequest { root: Some(root.into()) };
816
817 let response = self
818 .call_with_retry(RpcEndpoint::GetNoteScriptByRoot, |mut rpc_api| {
819 let request = request.clone();
820 Box::pin(async move { rpc_api.get_note_script_by_root(request).await })
821 })
822 .await?;
823
824 let Some(script) = response.into_inner().script else {
826 return Ok(None);
827 };
828 let note_script: NoteScript = script.decode_and_verify()?;
829
830 Ok(Some(note_script))
831 }
832
833 async fn sync_storage_maps(
834 &self,
835 block_from: BlockNumber,
836 block_to: BlockNumber,
837 account_id: AccountId,
838 ) -> Result<StorageMapInfo, RpcError> {
839 let mut pagination = BlockPagination::new(block_from, block_to);
840 let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
841
842 let (chain_tip, block_number) = loop {
843 let request = proto::rpc::SyncAccountStorageMapsRequest {
844 block_range: Some(BlockRange {
845 block_from: pagination.current_block_from().as_u32(),
846 block_to: block_to.as_u32(),
847 }),
848 account_id: Some(account_id.into()),
849 };
850 let response = self
851 .call_with_retry(RpcEndpoint::SyncStorageMaps, |mut rpc_api| {
852 Box::pin(async move { rpc_api.sync_account_storage_maps(request).await })
853 })
854 .await?;
855 let page = StorageMapInfo::try_from(response.into_inner())?;
856
857 for (slot_name, entries) in page.map_entries {
858 map_entries
859 .entry(slot_name)
860 .or_default()
861 .as_map_mut()
862 .extend(entries.into_map());
863 }
864
865 match pagination.advance(page.block_number, page.chain_tip)? {
866 PaginationResult::Continue => {},
867 PaginationResult::Done {
868 chain_tip: final_chain_tip,
869 block_num: final_block_num,
870 } => break (final_chain_tip, final_block_num),
871 }
872 };
873
874 Ok(StorageMapInfo { chain_tip, block_number, map_entries })
875 }
876
877 async fn sync_account_vault(
878 &self,
879 block_from: BlockNumber,
880 block_to: BlockNumber,
881 account_id: AccountId,
882 ) -> Result<AccountVaultInfo, RpcError> {
883 let mut pagination = BlockPagination::new(block_from, block_to);
884 let mut vault_patch = AccountVaultPatch::default();
885
886 let (chain_tip, block_number) = loop {
887 let request = proto::rpc::SyncAccountVaultRequest {
888 block_range: Some(BlockRange {
889 block_from: pagination.current_block_from().as_u32(),
890 block_to: block_to.as_u32(),
891 }),
892 account_id: Some(account_id.into()),
893 };
894 let response = self
895 .call_with_retry(RpcEndpoint::SyncAccountVault, |mut rpc_api| {
896 Box::pin(async move { rpc_api.sync_account_vault(request).await })
897 })
898 .await?;
899 let page = AccountVaultInfo::try_from(response.into_inner())?;
900
901 vault_patch.merge(page.vault_patch);
902
903 match pagination.advance(page.block_number, page.chain_tip)? {
904 PaginationResult::Continue => {},
905 PaginationResult::Done {
906 chain_tip: final_chain_tip,
907 block_num: final_block_num,
908 } => break (final_chain_tip, final_block_num),
909 }
910 };
911
912 Ok(AccountVaultInfo { chain_tip, block_number, vault_patch })
913 }
914
915 async fn sync_transactions(
921 &self,
922 block_from: BlockNumber,
923 block_to: BlockNumber,
924 account_ids: Vec<AccountId>,
925 ) -> Result<Vec<TransactionRecord>, RpcError> {
926 if account_ids.is_empty() {
927 return Ok(Vec::new());
928 }
929
930 let limits = self.get_rpc_limits().await?;
931 let mut transactions: Vec<TransactionRecord> = Vec::new();
932
933 for chunk in account_ids.chunks(limits.account_ids_limit as usize) {
934 let proto_account_ids: Vec<_> = chunk.iter().map(|acc_id| (*acc_id).into()).collect();
935 let mut pagination = BlockPagination::new(block_from, block_to);
936
937 loop {
938 let request = proto::rpc::SyncTransactionsRequest {
939 block_range: Some(BlockRange {
940 block_from: pagination.current_block_from().as_u32(),
941 block_to: block_to.as_u32(),
942 }),
943 account_ids: proto_account_ids.clone(),
944 };
945
946 let response = self
947 .call_with_retry(RpcEndpoint::SyncTransactions, |mut rpc_api| {
948 let request = request.clone();
949 Box::pin(async move { rpc_api.sync_transactions(request).await })
950 })
951 .await?
952 .into_inner();
953
954 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
955 "SyncTransactionsResponse.pagination_info".to_owned(),
956 ))?;
957 let page_chain_tip = BlockNumber::from(page.chain_tip);
958 let page_block_to = BlockNumber::from(page.block_num);
959
960 for proto_tx in response.transactions {
961 transactions.push(TransactionRecord::try_from(proto_tx)?);
962 }
963
964 match pagination.advance(page_block_to, page_chain_tip)? {
965 PaginationResult::Continue => {},
966 PaginationResult::Done { .. } => break,
967 }
968 }
969 }
970
971 Ok(transactions)
972 }
973
974 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
975 let endpoint: Endpoint =
976 Endpoint::try_from(self.endpoint.as_str()).map_err(RpcError::InvalidNodeEndpoint)?;
977 Ok(endpoint.to_network_id())
978 }
979
980 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
981 if let Some(limits) = *self.limits.read() {
982 return Ok(limits);
983 }
984
985 let response = self
986 .call_with_retry(RpcEndpoint::GetLimits, |mut rpc_api| {
987 Box::pin(async move { rpc_api.get_limits(()).await })
988 })
989 .await?;
990 let limits = RpcLimits::try_from(response.into_inner()).map_err(RpcError::from)?;
991
992 self.limits.write().replace(limits);
994 Ok(limits)
995 }
996
997 fn has_rpc_limits(&self) -> Option<RpcLimits> {
998 *self.limits.read()
999 }
1000
1001 async fn set_rpc_limits(&self, limits: RpcLimits) {
1002 self.limits.write().replace(limits);
1003 }
1004
1005 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
1006 GrpcClient::get_status_unversioned(self).await
1007 }
1008
1009 async fn get_network_note_status(
1010 &self,
1011 note_id: NoteId,
1012 ) -> Result<NetworkNoteStatusInfo, RpcError> {
1013 let request = proto::note::NoteId::from(¬e_id);
1014
1015 let response = self
1016 .call_with_retry(RpcEndpoint::GetNetworkNoteStatus, |mut rpc_api| {
1017 let request = request.clone();
1018 Box::pin(async move { rpc_api.get_network_note_status(request).await })
1019 })
1020 .await?;
1021
1022 response.into_inner().try_into()
1023 }
1024}
1025
1026impl RpcError {
1030 pub fn from_grpc_error_with_context(
1031 endpoint: RpcEndpoint,
1032 status: Status,
1033 context: AcceptHeaderContext,
1034 ) -> Self {
1035 if let Some(accept_error) =
1036 AcceptHeaderError::try_from_message_with_context(status.message(), context)
1037 {
1038 return Self::AcceptHeaderError(accept_error);
1039 }
1040
1041 let endpoint_error = parse_node_error(&endpoint, status.details(), status.message());
1043
1044 let error_kind = GrpcError::from(&status);
1045 let source = Box::new(status) as Box<dyn Error + Send + Sync + 'static>;
1046
1047 Self::RequestError {
1048 endpoint,
1049 error_kind,
1050 endpoint_error,
1051 source: Some(source),
1052 }
1053 }
1054}
1055
1056impl From<&Status> for GrpcError {
1057 fn from(status: &Status) -> Self {
1058 GrpcError::from_code(status.code() as i32, Some(status.message().to_string()))
1059 }
1060}
1061
1062fn decode_block_response(
1071 response: proto::rpc::MaybeBlock,
1072) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError> {
1073 let block: SignedBlock = response
1076 .block
1077 .ok_or(RpcError::ExpectedDataMissing("GetBlockByNumberResponse.block".to_string()))?
1078 .decode_and_build_unchecked()?;
1079
1080 let proof = response.proof.map(ExecutionProof::try_from).transpose()?;
1083
1084 Ok((block, proof))
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089 use std::boxed::Box;
1090 use std::vec;
1091
1092 use miden_protocol::Word;
1093 use miden_protocol::block::{BlockNumber, SignedBlock};
1094 use miden_testing::MockChain;
1095
1096 use super::{
1097 BlockPagination,
1098 DEFAULT_MAX_RESPONSE_SIZE_BYTES,
1099 GrpcClient,
1100 PaginationResult,
1101 decode_block_response,
1102 proto,
1103 };
1104 use crate::alloc::string::ToString;
1105 use crate::rpc::{Endpoint, NodeRpcClient, RpcError};
1106
1107 fn assert_send_sync<T: Send + Sync>() {}
1108
1109 fn genesis_block_messages()
1111 -> (proto::blockchain::SignedBlock, proto::primitives::ExecutionProof) {
1112 let chain = MockChain::new();
1113 let block = chain.proven_blocks().first().expect("the chain has a genesis block").clone();
1114 let (header, body, signatures, proof) = block.into_parts();
1115
1116 (SignedBlock::new_unchecked(header, body, signatures).into(), proof.into())
1117 }
1118
1119 #[test]
1120 fn decode_block_response_reads_a_requested_proof() {
1121 let (block, proof_message) = genesis_block_messages();
1122 let response = proto::rpc::MaybeBlock {
1123 block: Some(block),
1124 proof: Some(proof_message.clone()),
1125 };
1126
1127 let (_block, proof) = decode_block_response(response).unwrap();
1128
1129 let decoded: proto::primitives::ExecutionProof =
1130 proof.expect("the response carries a proof").into();
1131 assert_eq!(decoded, proof_message);
1132 }
1133
1134 #[test]
1135 fn decode_block_response_omits_an_absent_proof() {
1136 let (block, _) = genesis_block_messages();
1137 let response = proto::rpc::MaybeBlock { block: Some(block), proof: None };
1138
1139 let (_block, proof) = decode_block_response(response).unwrap();
1140
1141 assert!(proof.is_none());
1142 }
1143
1144 #[test]
1145 fn decode_block_response_rejects_malformed_proof_bytes() {
1146 let (block, _) = genesis_block_messages();
1147 let response = proto::rpc::MaybeBlock {
1148 block: Some(block),
1149 proof: Some(proto::primitives::ExecutionProof { encoded: vec![0xff; 32] }),
1150 };
1151
1152 let res = decode_block_response(response);
1153
1154 assert!(matches!(res, Err(RpcError::DeserializationError(_))));
1155 }
1156
1157 #[test]
1158 fn decode_block_response_rejects_an_absent_block() {
1159 let response = proto::rpc::MaybeBlock { block: None, proof: None };
1160
1161 let res = decode_block_response(response);
1162
1163 assert!(matches!(res, Err(RpcError::ExpectedDataMissing(_))));
1164 }
1165
1166 #[test]
1167 fn is_send_sync() {
1168 assert_send_sync::<GrpcClient>();
1169 assert_send_sync::<Box<dyn NodeRpcClient>>();
1170 }
1171
1172 #[test]
1173 fn block_pagination_errors_when_block_num_goes_backwards() {
1174 let mut pagination = BlockPagination::new(10_u32.into(), 20_u32.into());
1175
1176 let res = pagination.advance(9_u32.into(), 20_u32.into());
1177 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1178 }
1179
1180 #[test]
1181 fn block_pagination_errors_after_max_iterations() {
1182 let mut pagination = BlockPagination::new(0_u32.into(), 10_000_u32.into());
1183 let chain_tip: BlockNumber = 10_000_u32.into();
1184
1185 for _ in 0..BlockPagination::MAX_ITERATIONS {
1186 let current = pagination.current_block_from();
1187 let res = pagination
1188 .advance(current, chain_tip)
1189 .expect("expected pagination to continue within iteration limit");
1190 assert!(matches!(res, PaginationResult::Continue));
1191 }
1192
1193 let res = pagination.advance(pagination.current_block_from(), chain_tip);
1194 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1195 }
1196
1197 #[test]
1198 fn block_pagination_stops_at_min_of_block_to_and_chain_tip() {
1199 let mut pagination = BlockPagination::new(0_u32.into(), 50_u32.into());
1201
1202 let res = pagination
1203 .advance(30_u32.into(), 30_u32.into())
1204 .expect("expected pagination to succeed");
1205
1206 assert!(matches!(
1207 res,
1208 PaginationResult::Done {
1209 chain_tip,
1210 block_num
1211 } if chain_tip.as_u32() == 30 && block_num.as_u32() == 30
1212 ));
1213 }
1214
1215 #[test]
1216 fn block_pagination_advances_cursor_by_one() {
1217 let mut pagination = BlockPagination::new(5_u32.into(), 100_u32.into());
1218
1219 let res = pagination
1220 .advance(5_u32.into(), 100_u32.into())
1221 .expect("expected pagination to succeed");
1222 assert!(matches!(res, PaginationResult::Continue));
1223 assert_eq!(pagination.current_block_from().as_u32(), 6);
1224 }
1225
1226 async fn dyn_trait_send_fut(client: Box<dyn NodeRpcClient>) {
1228 let res = client.get_block_header_by_number(None, false).await;
1230 assert!(res.is_ok());
1231 }
1232
1233 #[tokio::test]
1234 async fn future_is_send() {
1235 let endpoint = &Endpoint::devnet();
1236 let client = GrpcClient::new(endpoint, 10000);
1237 let client: Box<GrpcClient> = client.into();
1238 tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
1239 }
1240
1241 #[tokio::test]
1242 async fn set_genesis_commitment_sets_the_commitment_when_its_not_already_set() {
1243 let endpoint = &Endpoint::devnet();
1244 let client = GrpcClient::new(endpoint, 10000);
1245
1246 assert!(client.genesis_commitment.read().is_none());
1247
1248 let commitment = Word::default();
1249 client.set_genesis_commitment(commitment).await.unwrap();
1250
1251 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1252 }
1253
1254 #[tokio::test]
1255 async fn set_genesis_commitment_does_nothing_if_the_commitment_is_already_set() {
1256 let endpoint = &Endpoint::devnet();
1257 let client = GrpcClient::new(endpoint, 10000);
1258
1259 let initial_commitment = Word::default();
1260 client.set_genesis_commitment(initial_commitment).await.unwrap();
1261
1262 let new_commitment = Word::from([1u32, 2, 3, 4]);
1263 client.set_genesis_commitment(new_commitment).await.unwrap();
1264
1265 assert_eq!(client.genesis_commitment.read().unwrap(), initial_commitment);
1266 }
1267
1268 #[tokio::test]
1269 async fn set_genesis_commitment_updates_the_client_if_already_connected() {
1270 let endpoint = &Endpoint::devnet();
1271 let client = GrpcClient::new(endpoint, 10000);
1272
1273 client.connect().await.unwrap();
1275
1276 let commitment = Word::default();
1277 client.set_genesis_commitment(commitment).await.unwrap();
1278
1279 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1280 assert!(client.client.read().as_ref().is_some());
1281 }
1282
1283 #[test]
1284 fn with_bearer_auth_stores_token() {
1285 let endpoint = &Endpoint::devnet();
1286 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token-one".to_string());
1287
1288 assert_eq!(client.bearer_token.as_deref(), Some("token-one"));
1289 }
1290
1291 #[test]
1292 fn with_bearer_auth_overwrites_on_repeat_call() {
1293 let endpoint = &Endpoint::devnet();
1294 let client = GrpcClient::new(endpoint, 10000)
1295 .with_bearer_auth("token-one".to_string())
1296 .with_bearer_auth("token-two".to_string());
1297
1298 assert_eq!(client.bearer_token.as_deref(), Some("token-two"));
1300 }
1301
1302 #[tokio::test]
1303 async fn with_bearer_auth_surfaces_invalid_ascii_value_at_connect_time() {
1304 let endpoint = &Endpoint::devnet();
1308 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("bad\nvalue".to_string());
1309
1310 let err = client.connect().await.expect_err("expected invalid token to fail connect");
1311 assert!(
1312 matches!(err, RpcError::ConnectionError(_)),
1313 "expected ConnectionError, got {err:?}",
1314 );
1315 }
1316
1317 #[tokio::test]
1318 async fn with_bearer_auth_is_preserved_across_set_genesis_commitment() {
1319 let endpoint = &Endpoint::devnet();
1320 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token".to_string());
1321 client.connect().await.unwrap();
1322
1323 client.set_genesis_commitment(Word::default()).await.unwrap();
1324
1325 assert_eq!(client.bearer_token.as_deref(), Some("token"));
1327 assert!(client.client.read().as_ref().is_some());
1328 }
1329
1330 #[test]
1331 fn with_max_decoding_message_size_overrides_default() {
1332 let endpoint = &Endpoint::devnet();
1333
1334 let default_client = GrpcClient::new(endpoint, 10_000);
1336 assert_eq!(default_client.max_decoding_message_size, DEFAULT_MAX_RESPONSE_SIZE_BYTES);
1337
1338 let custom =
1340 GrpcClient::new(endpoint, 10_000).with_max_decoding_message_size(8 * 1024 * 1024);
1341 assert_eq!(custom.max_decoding_message_size, 8 * 1024 * 1024);
1342 }
1343
1344 #[tokio::test]
1355 #[ignore = "requires network access to public testnet"]
1356 async fn with_bearer_auth_does_not_break_real_rpc_against_testnet() {
1357 let endpoint = &Endpoint::testnet();
1358 let client = GrpcClient::new(endpoint, 10_000).with_bearer_auth("smoke-test".to_string());
1359
1360 let status = client
1361 .get_status_unversioned()
1362 .await
1363 .expect("testnet status with caller auth header must succeed");
1364 assert!(!status.version.is_empty(), "status must include a server version");
1365 }
1366}