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