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>(
308 &self,
309 endpoint: RpcEndpoint,
310 mut call: impl FnMut(ApiClient) -> RpcFuture<Result<tonic::Response<T>, Status>>,
311 ) -> Result<tonic::Response<T>, RpcError> {
312 let mut retry_state = retry::RetryState::new(self.max_retries, self.retry_interval_ms);
313
314 loop {
315 let rpc_api = self.ensure_connected().await?;
316
317 match call(rpc_api).await {
318 Ok(response) => return Ok(response),
319 Err(status) if retry_state.should_retry(&status).await => {},
320 Err(status) => return Err(self.rpc_error_from_status(endpoint, status)),
321 }
322 }
323 }
324
325 pub async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
332 let mut rpc_api = ApiClient::new_client_without_accept_header(
333 self.endpoint.clone(),
334 self.timeout_ms,
335 self.bearer_token.clone(),
336 self.max_decoding_message_size,
337 )
338 .await?;
339 rpc_api
340 .status(())
341 .await
342 .map_err(|status| self.rpc_error_from_status(RpcEndpoint::Status, status))
343 .map(tonic::Response::into_inner)
344 .and_then(RpcStatusInfo::try_from)
345 }
346}
347
348#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
349#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
350impl NodeRpcClient for GrpcClient {
351 fn has_genesis_commitment(&self) -> Option<Word> {
356 *self.genesis_commitment.read()
357 }
358
359 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
360 if self.genesis_commitment.read().is_some() {
362 return Ok(());
364 }
365
366 self.genesis_commitment.write().replace(commitment);
368
369 let mut client_guard = self.client.write();
372 if let Some(client) = client_guard.as_mut() {
373 client.set_genesis_commitment(commitment);
374 }
375
376 Ok(())
377 }
378
379 async fn get_transaction_encryption_key(
380 &self,
381 ) -> Result<AttestedTransactionEncryptionKey, RpcError> {
382 let api_response = self
383 .call_with_retry(RpcEndpoint::GetTransactionEncryptionKey, |mut rpc_api| {
384 Box::pin(async move { rpc_api.get_transaction_encryption_key(()).await })
385 })
386 .await?;
387 let response = api_response.into_inner();
388
389 let attestations = response
393 .attestations
394 .into_iter()
395 .filter_map(|attestation| {
396 let decoded =
397 ValidatorPublicKey::read_from_bytes(&attestation.validator_public_key)
398 .ok()
399 .zip(ValidatorSignature::read_from_bytes(&attestation.signature).ok())
400 .map(|(validator_key, signature)| ValidatorAttestation {
401 validator_key,
402 signature,
403 });
404 if decoded.is_none() {
405 warn!(
406 "skipping a transaction encryption key attestation that failed to decode"
407 );
408 }
409 decoded
410 })
411 .collect::<Vec<_>>();
412
413 let wire_scheme = |scheme: i32| {
416 u32::try_from(scheme)
417 .map_err(|_| RpcError::InvalidResponse(format!("negative IES scheme '{scheme}'")))
418 };
419
420 let next_key = response
421 .next_key
422 .map(|next| {
423 Ok::<_, RpcError>(NextTransactionEncryptionKey {
424 scheme: wire_scheme(next.scheme)?,
425 key_id: next.key_id,
426 public_key: next.public_key,
427 rotation_block_num: next.rotation_block_num.into(),
428 })
429 })
430 .transpose()?;
431
432 Ok(AttestedTransactionEncryptionKey {
433 scheme: wire_scheme(response.scheme)?,
434 key_id: response.key_id,
435 public_key: response.public_key,
436 attestations,
437 next_key,
438 })
439 }
440
441 async fn submit_proven_transaction(
442 &self,
443 proven_transaction: ProvenTransaction,
444 sealed_transaction_inputs: SealedTransactionInputs,
445 ) -> Result<BlockNumber, RpcError> {
446 let request = proto::transaction::ProvenTransaction {
447 transaction: proven_transaction.to_bytes(),
448 sealed_transaction_inputs: Some(sealed_transaction_inputs.into()),
449 };
450
451 let api_response = self
452 .call_with_retry(RpcEndpoint::SubmitProvenTx, |mut rpc_api| {
453 let request = request.clone();
454 Box::pin(async move { rpc_api.submit_proven_tx(request).await })
455 })
456 .await?;
457
458 Ok(BlockNumber::from(api_response.into_inner().block_num))
459 }
460
461 async fn submit_proven_batch(
462 &self,
463 proven_batch: ProvenBatch,
464 proposed_batch: ProposedBatch,
465 sealed_transaction_inputs: Vec<SealedTransactionInputs>,
466 ) -> Result<BlockNumber, RpcError> {
467 let request = proto::transaction::TransactionBatch {
468 batch_proof: proven_batch.to_bytes(),
469 proposed_batch: Some(proposed_batch.to_bytes()),
470 sealed_transaction_inputs: sealed_transaction_inputs
471 .into_iter()
472 .map(Into::into)
473 .collect(),
474 };
475
476 let api_response = self
477 .call_with_retry(RpcEndpoint::SubmitProvenBatch, |mut rpc_api| {
478 let request = request.clone();
479 Box::pin(async move { rpc_api.submit_proven_tx_batch(request).await })
480 })
481 .await?;
482
483 Ok(BlockNumber::from(api_response.into_inner().block_num))
484 }
485
486 async fn get_block_header_by_number(
487 &self,
488 block_num: Option<BlockNumber>,
489 include_mmr_proof: bool,
490 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
491 let request = proto::rpc::BlockHeaderByNumberRequest {
492 block_num: block_num.as_ref().map(BlockNumber::as_u32),
493 include_mmr_proof: Some(include_mmr_proof),
494 };
495
496 info!("Calling GetBlockHeaderByNumber: {:?}", request);
497
498 let api_response = self
499 .call_with_retry(RpcEndpoint::GetBlockHeaderByNumber, |mut rpc_api| {
500 Box::pin(async move { rpc_api.get_block_header_by_number(request).await })
501 })
502 .await?;
503
504 let response = api_response.into_inner();
505
506 let block_header: BlockHeader = response
507 .block_header
508 .ok_or(RpcError::ExpectedDataMissing("BlockHeader".into()))?
509 .try_into()?;
510
511 let mmr_proof = if include_mmr_proof {
512 let forest = response
513 .chain_length
514 .ok_or(RpcError::ExpectedDataMissing("ChainLength".into()))?;
515 let merkle_path: MerklePath = response
516 .mmr_path
517 .ok_or(RpcError::ExpectedDataMissing("MmrPath".into()))?
518 .try_into()?;
519
520 let forest_size = usize::try_from(forest).expect("u64 should fit in usize");
521 let forest = Forest::new(forest_size).map_err(|_| {
522 RpcError::InvalidResponse(format!("invalid forest size: {forest_size}"))
523 })?;
524 Some(MmrProof::new(
525 MmrPath::new(forest, block_header.block_num().as_usize(), merkle_path),
526 block_header.commitment(),
527 ))
528 } else {
529 None
530 };
531
532 Ok((block_header, mmr_proof))
533 }
534
535 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
536 let limits = self.get_rpc_limits().await?;
537 let mut notes = Vec::with_capacity(note_ids.len());
538 for chunk in note_ids.chunks(limits.note_ids_limit as usize) {
539 let request = proto::note::NoteIdList {
540 ids: chunk.iter().map(|id| (*id).into()).collect(),
541 };
542
543 let api_response = self
544 .call_with_retry(RpcEndpoint::GetNotesById, |mut rpc_api| {
545 let request = request.clone();
546 Box::pin(async move { rpc_api.get_notes_by_id(request).await })
547 })
548 .await?;
549
550 let response_notes = api_response
551 .into_inner()
552 .notes
553 .into_iter()
554 .map(FetchedNote::try_from)
555 .collect::<Result<Vec<FetchedNote>, RpcConversionError>>()?;
556
557 notes.extend(response_notes);
558 }
559 Ok(notes)
560 }
561
562 async fn sync_chain_mmr(
563 &self,
564 current_block_height: BlockNumber,
565 upper_bound: SyncTarget,
566 ) -> Result<ChainMmrInfo, RpcError> {
567 let finality_level: proto::rpc::FinalityLevel = upper_bound.into();
568
569 let request = proto::rpc::SyncChainMmrRequest {
570 current_client_block_height: current_block_height.as_u32(),
571 finality_level: finality_level.into(),
572 };
573
574 let response = self
575 .call_with_retry(RpcEndpoint::SyncChainMmr, |mut rpc_api| {
576 Box::pin(async move { rpc_api.sync_chain_mmr(request).await })
577 })
578 .await?;
579
580 response.into_inner().try_into()
581 }
582
583 async fn get_account(
595 &self,
596 account_id: AccountId,
597 request: GetAccountRequest,
598 ) -> Result<(BlockNumber, AccountProof), RpcError> {
599 let GetAccountRequest { storage, at, known_code, vault } = request;
600
601 let known_code_commitment = known_code.as_ref().map_or(EMPTY_WORD, AccountCode::commitment);
602 let mut known_codes_by_commitment: BTreeMap<Word, AccountCode> = BTreeMap::new();
603 if let Some(account_code) = known_code {
604 known_codes_by_commitment.insert(account_code.commitment(), account_code);
605 }
606
607 let requirements = match storage.clone() {
609 StorageMapFetch::Slots(reqs) => reqs,
610 StorageMapFetch::Skip | StorageMapFetch::All => AccountStorageRequirements::default(),
611 };
612
613 let account_details = if account_id.is_public() {
616 Some(AccountDetailRequest {
617 code_commitment: Some(known_code_commitment.into()),
618 asset_vault_commitment: vault.into(),
619 storage_request: storage.into(),
620 })
621 } else {
622 None
623 };
624
625 let block_num = match at {
626 AccountStateAt::Block(number) => Some(number.into()),
627 AccountStateAt::ChainTip => None,
628 };
629
630 let proto_request = AccountRequest {
631 account_id: Some(account_id.into()),
632 block_num,
633 details: account_details,
634 };
635
636 let response = self
637 .call_with_retry(RpcEndpoint::GetAccount, |mut rpc_api| {
638 let request = proto_request.clone();
639 Box::pin(async move { rpc_api.get_account(request).await })
640 })
641 .await?
642 .into_inner();
643
644 let account_witness: AccountWitness = response
645 .witness
646 .ok_or(RpcError::ExpectedDataMissing("AccountWitness".to_string()))?
647 .try_into()?;
648
649 let response_block_num: BlockNumber = response
650 .block_num
651 .ok_or(RpcError::ExpectedDataMissing("response block num".to_string()))?
652 .block_num
653 .into();
654
655 let headers = if account_witness.id().is_public() {
657 let details = response
658 .details
659 .ok_or(RpcError::ExpectedDataMissing("Account.Details".to_string()))?
660 .into_domain(&known_codes_by_commitment, &requirements)?;
661
662 Some(details)
663 } else {
664 None
665 };
666
667 let proof = AccountProof::new(account_witness, headers)
668 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
669
670 Ok((response_block_num, proof))
671 }
672
673 async fn sync_notes(
679 &self,
680 block_from: BlockNumber,
681 block_to: BlockNumber,
682 note_tags: &BTreeSet<NoteTag>,
683 ) -> Result<Vec<SyncNotesBlock>, RpcError> {
684 if note_tags.is_empty() {
685 return Ok(Vec::new());
686 }
687
688 let limits = self.get_rpc_limits().await?;
689 let tags: Vec<NoteTag> = note_tags.iter().copied().collect();
690
691 let mut merged_blocks: BTreeMap<BlockNumber, SyncNotesBlock> = BTreeMap::new();
694
695 for chunk in tags.chunks(limits.note_tags_limit as usize) {
696 let proto_tags: Vec<u32> = chunk.iter().map(|&t| t.into()).collect();
697 let mut pagination = BlockPagination::new(block_from, block_to);
698
699 loop {
700 let request = proto::rpc::SyncNotesRequest {
701 block_range: Some(BlockRange {
702 block_from: pagination.current_block_from().as_u32(),
703 block_to: block_to.as_u32(),
704 }),
705 note_tags: proto_tags.clone(),
706 };
707
708 let response = self
709 .call_with_retry(RpcEndpoint::SyncNotes, |mut rpc_api| {
710 let request = request.clone();
711 Box::pin(async move { rpc_api.sync_notes(request).await })
712 })
713 .await?
714 .into_inner();
715
716 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
717 "SyncNotesResponse.pagination_info".to_owned(),
718 ))?;
719 let page_chain_tip = BlockNumber::from(page.chain_tip);
720 let page_block_to = BlockNumber::from(page.block_num);
721
722 for proto_block in response.blocks {
723 let block: SyncNotesBlock = proto_block.try_into()?;
724 let bn = block.block_header.block_num();
725 if let Some(existing) = merged_blocks.get_mut(&bn) {
726 for (id, note) in block.notes {
727 existing.notes.entry(id).or_insert(note);
728 }
729 } else {
730 merged_blocks.insert(bn, block);
731 }
732 }
733
734 match pagination.advance(page_block_to, page_chain_tip)? {
735 PaginationResult::Continue => {},
736 PaginationResult::Done { .. } => break,
737 }
738 }
739 }
740
741 Ok(merged_blocks.into_values().collect())
742 }
743
744 async fn sync_nullifiers(
745 &self,
746 prefixes: &[u16],
747 block_from: BlockNumber,
748 block_to: BlockNumber,
749 ) -> Result<Vec<NullifierUpdate>, RpcError> {
750 let limits = self.get_rpc_limits().await?;
751 let mut all_nullifiers = BTreeSet::new();
752
753 for chunk in prefixes.chunks(limits.nullifiers_limit as usize) {
756 let proto_prefixes: Vec<u32> = chunk.iter().map(|&x| u32::from(x)).collect();
757 let mut pagination = BlockPagination::new(block_from, block_to);
758
759 loop {
760 let request = proto::rpc::SyncNullifiersRequest {
761 nullifiers: proto_prefixes.clone(),
762 prefix_len: 16,
763 block_range: Some(BlockRange {
764 block_from: pagination.current_block_from().as_u32(),
765 block_to: pagination.block_to().as_u32(),
766 }),
767 };
768
769 let response = self
770 .call_with_retry(RpcEndpoint::SyncNullifiers, |mut rpc_api| {
771 let request = request.clone();
772 Box::pin(async move { rpc_api.sync_nullifiers(request).await })
773 })
774 .await?
775 .into_inner();
776
777 let batch_nullifiers = response
778 .nullifiers
779 .iter()
780 .map(TryFrom::try_from)
781 .collect::<Result<Vec<NullifierUpdate>, _>>()
782 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
783
784 all_nullifiers.extend(batch_nullifiers);
785
786 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
787 "SyncNullifiersResponse.pagination_info".to_owned(),
788 ))?;
789
790 match pagination.advance(page.block_num.into(), page.chain_tip.into())? {
791 PaginationResult::Continue => {},
792 PaginationResult::Done { .. } => break,
793 }
794 }
795 }
796 Ok(all_nullifiers.into_iter().collect::<Vec<_>>())
797 }
798
799 async fn get_block_by_number(
800 &self,
801 block_num: BlockNumber,
802 include_proof: bool,
803 ) -> Result<ProvenBlock, RpcError> {
804 let request = proto::blockchain::BlockRequest {
805 block_num: block_num.as_u32(),
806 include_proof: Some(include_proof),
807 };
808
809 let response = self
810 .call_with_retry(RpcEndpoint::GetBlockByNumber, |mut rpc_api| {
811 Box::pin(async move { rpc_api.get_block_by_number(request).await })
812 })
813 .await?;
814
815 let response = response.into_inner();
816 let block =
817 ProvenBlock::read_from_bytes(&response.block.ok_or(RpcError::ExpectedDataMissing(
818 "GetBlockByNumberResponse.block".to_string(),
819 ))?)?;
820
821 Ok(block)
822 }
823
824 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
825 let request = proto::note::NoteScriptRoot { root: Some(root.into()) };
826
827 let response = self
828 .call_with_retry(RpcEndpoint::GetNoteScriptByRoot, |mut rpc_api| {
829 Box::pin(async move { rpc_api.get_note_script_by_root(request).await })
830 })
831 .await?;
832
833 let Some(script) = response.into_inner().script else {
835 return Ok(None);
836 };
837 let note_script = NoteScript::try_from(script)?;
838
839 Ok(Some(note_script))
840 }
841
842 async fn sync_storage_maps(
843 &self,
844 block_from: BlockNumber,
845 block_to: BlockNumber,
846 account_id: AccountId,
847 ) -> Result<StorageMapInfo, RpcError> {
848 let mut pagination = BlockPagination::new(block_from, block_to);
849 let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
850
851 let (chain_tip, block_number) = loop {
852 let request = proto::rpc::SyncAccountStorageMapsRequest {
853 block_range: Some(BlockRange {
854 block_from: pagination.current_block_from().as_u32(),
855 block_to: block_to.as_u32(),
856 }),
857 account_id: Some(account_id.into()),
858 };
859 let response = self
860 .call_with_retry(RpcEndpoint::SyncStorageMaps, |mut rpc_api| {
861 let request = request.clone();
862 Box::pin(async move { rpc_api.sync_account_storage_maps(request).await })
863 })
864 .await?;
865 let page = StorageMapInfo::try_from(response.into_inner())?;
866
867 for (slot_name, entries) in page.map_entries {
868 map_entries
869 .entry(slot_name)
870 .or_default()
871 .as_map_mut()
872 .extend(entries.into_map());
873 }
874
875 match pagination.advance(page.block_number, page.chain_tip)? {
876 PaginationResult::Continue => {},
877 PaginationResult::Done {
878 chain_tip: final_chain_tip,
879 block_num: final_block_num,
880 } => break (final_chain_tip, final_block_num),
881 }
882 };
883
884 Ok(StorageMapInfo { chain_tip, block_number, map_entries })
885 }
886
887 async fn sync_account_vault(
888 &self,
889 block_from: BlockNumber,
890 block_to: BlockNumber,
891 account_id: AccountId,
892 ) -> Result<AccountVaultInfo, RpcError> {
893 let mut pagination = BlockPagination::new(block_from, block_to);
894 let mut vault_patch = AccountVaultPatch::default();
895
896 let (chain_tip, block_number) = loop {
897 let request = proto::rpc::SyncAccountVaultRequest {
898 block_range: Some(BlockRange {
899 block_from: pagination.current_block_from().as_u32(),
900 block_to: block_to.as_u32(),
901 }),
902 account_id: Some(account_id.into()),
903 };
904 let response = self
905 .call_with_retry(RpcEndpoint::SyncAccountVault, |mut rpc_api| {
906 let request = request.clone();
907 Box::pin(async move { rpc_api.sync_account_vault(request).await })
908 })
909 .await?;
910 let page = AccountVaultInfo::try_from(response.into_inner())?;
911
912 vault_patch.merge(page.vault_patch);
913
914 match pagination.advance(page.block_number, page.chain_tip)? {
915 PaginationResult::Continue => {},
916 PaginationResult::Done {
917 chain_tip: final_chain_tip,
918 block_num: final_block_num,
919 } => break (final_chain_tip, final_block_num),
920 }
921 };
922
923 Ok(AccountVaultInfo { chain_tip, block_number, vault_patch })
924 }
925
926 async fn sync_transactions(
932 &self,
933 block_from: BlockNumber,
934 block_to: BlockNumber,
935 account_ids: Vec<AccountId>,
936 ) -> Result<Vec<TransactionRecord>, RpcError> {
937 if account_ids.is_empty() {
938 return Ok(Vec::new());
939 }
940
941 let limits = self.get_rpc_limits().await?;
942 let mut transactions: Vec<TransactionRecord> = Vec::new();
943
944 for chunk in account_ids.chunks(limits.account_ids_limit as usize) {
945 let proto_account_ids: Vec<_> = chunk.iter().map(|acc_id| (*acc_id).into()).collect();
946 let mut pagination = BlockPagination::new(block_from, block_to);
947
948 loop {
949 let request = proto::rpc::SyncTransactionsRequest {
950 block_range: Some(BlockRange {
951 block_from: pagination.current_block_from().as_u32(),
952 block_to: block_to.as_u32(),
953 }),
954 account_ids: proto_account_ids.clone(),
955 };
956
957 let response = self
958 .call_with_retry(RpcEndpoint::SyncTransactions, |mut rpc_api| {
959 let request = request.clone();
960 Box::pin(async move { rpc_api.sync_transactions(request).await })
961 })
962 .await?
963 .into_inner();
964
965 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
966 "SyncTransactionsResponse.pagination_info".to_owned(),
967 ))?;
968 let page_chain_tip = BlockNumber::from(page.chain_tip);
969 let page_block_to = BlockNumber::from(page.block_num);
970
971 for proto_tx in response.transactions {
972 transactions.push(TransactionRecord::try_from(proto_tx)?);
973 }
974
975 match pagination.advance(page_block_to, page_chain_tip)? {
976 PaginationResult::Continue => {},
977 PaginationResult::Done { .. } => break,
978 }
979 }
980 }
981
982 Ok(transactions)
983 }
984
985 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
986 let endpoint: Endpoint =
987 Endpoint::try_from(self.endpoint.as_str()).map_err(RpcError::InvalidNodeEndpoint)?;
988 Ok(endpoint.to_network_id())
989 }
990
991 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
992 if let Some(limits) = *self.limits.read() {
994 return Ok(limits);
995 }
996
997 let response = self
999 .call_with_retry(RpcEndpoint::GetLimits, |mut rpc_api| {
1000 Box::pin(async move { rpc_api.get_limits(()).await })
1001 })
1002 .await?;
1003 let limits = RpcLimits::try_from(response.into_inner()).map_err(RpcError::from)?;
1004
1005 self.limits.write().replace(limits);
1007 Ok(limits)
1008 }
1009
1010 fn has_rpc_limits(&self) -> Option<RpcLimits> {
1011 *self.limits.read()
1012 }
1013
1014 async fn set_rpc_limits(&self, limits: RpcLimits) {
1015 self.limits.write().replace(limits);
1016 }
1017
1018 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
1019 GrpcClient::get_status_unversioned(self).await
1020 }
1021
1022 async fn get_network_note_status(
1023 &self,
1024 note_id: NoteId,
1025 ) -> Result<NetworkNoteStatusInfo, RpcError> {
1026 let request = proto::note::NoteId { id: Some(note_id.into()) };
1027
1028 let response = self
1029 .call_with_retry(RpcEndpoint::GetNetworkNoteStatus, |mut rpc_api| {
1030 Box::pin(async move { rpc_api.get_network_note_status(request).await })
1031 })
1032 .await?;
1033
1034 response.into_inner().try_into()
1035 }
1036}
1037
1038impl RpcError {
1042 pub fn from_grpc_error_with_context(
1043 endpoint: RpcEndpoint,
1044 status: Status,
1045 context: AcceptHeaderContext,
1046 ) -> Self {
1047 if let Some(accept_error) =
1048 AcceptHeaderError::try_from_message_with_context(status.message(), context)
1049 {
1050 return Self::AcceptHeaderError(accept_error);
1051 }
1052
1053 let endpoint_error = parse_node_error(&endpoint, status.details(), status.message());
1055
1056 let error_kind = GrpcError::from(&status);
1057 let source = Box::new(status) as Box<dyn Error + Send + Sync + 'static>;
1058
1059 Self::RequestError {
1060 endpoint,
1061 error_kind,
1062 endpoint_error,
1063 source: Some(source),
1064 }
1065 }
1066}
1067
1068impl From<&Status> for GrpcError {
1069 fn from(status: &Status) -> Self {
1070 GrpcError::from_code(status.code() as i32, Some(status.message().to_string()))
1071 }
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076 use std::boxed::Box;
1077
1078 use miden_protocol::Word;
1079 use miden_protocol::block::BlockNumber;
1080
1081 use super::{BlockPagination, DEFAULT_MAX_RESPONSE_SIZE_BYTES, GrpcClient, PaginationResult};
1082 use crate::alloc::string::ToString;
1083 use crate::rpc::{Endpoint, NodeRpcClient, RpcError};
1084
1085 fn assert_send_sync<T: Send + Sync>() {}
1086
1087 #[test]
1088 fn is_send_sync() {
1089 assert_send_sync::<GrpcClient>();
1090 assert_send_sync::<Box<dyn NodeRpcClient>>();
1091 }
1092
1093 #[test]
1094 fn block_pagination_errors_when_block_num_goes_backwards() {
1095 let mut pagination = BlockPagination::new(10_u32.into(), 20_u32.into());
1096
1097 let res = pagination.advance(9_u32.into(), 20_u32.into());
1098 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1099 }
1100
1101 #[test]
1102 fn block_pagination_errors_after_max_iterations() {
1103 let mut pagination = BlockPagination::new(0_u32.into(), 10_000_u32.into());
1104 let chain_tip: BlockNumber = 10_000_u32.into();
1105
1106 for _ in 0..BlockPagination::MAX_ITERATIONS {
1107 let current = pagination.current_block_from();
1108 let res = pagination
1109 .advance(current, chain_tip)
1110 .expect("expected pagination to continue within iteration limit");
1111 assert!(matches!(res, PaginationResult::Continue));
1112 }
1113
1114 let res = pagination.advance(pagination.current_block_from(), chain_tip);
1115 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1116 }
1117
1118 #[test]
1119 fn block_pagination_stops_at_min_of_block_to_and_chain_tip() {
1120 let mut pagination = BlockPagination::new(0_u32.into(), 50_u32.into());
1122
1123 let res = pagination
1124 .advance(30_u32.into(), 30_u32.into())
1125 .expect("expected pagination to succeed");
1126
1127 assert!(matches!(
1128 res,
1129 PaginationResult::Done {
1130 chain_tip,
1131 block_num
1132 } if chain_tip.as_u32() == 30 && block_num.as_u32() == 30
1133 ));
1134 }
1135
1136 #[test]
1137 fn block_pagination_advances_cursor_by_one() {
1138 let mut pagination = BlockPagination::new(5_u32.into(), 100_u32.into());
1139
1140 let res = pagination
1141 .advance(5_u32.into(), 100_u32.into())
1142 .expect("expected pagination to succeed");
1143 assert!(matches!(res, PaginationResult::Continue));
1144 assert_eq!(pagination.current_block_from().as_u32(), 6);
1145 }
1146
1147 async fn dyn_trait_send_fut(client: Box<dyn NodeRpcClient>) {
1149 let res = client.get_block_header_by_number(None, false).await;
1151 assert!(res.is_ok());
1152 }
1153
1154 #[tokio::test]
1155 async fn future_is_send() {
1156 let endpoint = &Endpoint::devnet();
1157 let client = GrpcClient::new(endpoint, 10000);
1158 let client: Box<GrpcClient> = client.into();
1159 tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
1160 }
1161
1162 #[tokio::test]
1163 async fn set_genesis_commitment_sets_the_commitment_when_its_not_already_set() {
1164 let endpoint = &Endpoint::devnet();
1165 let client = GrpcClient::new(endpoint, 10000);
1166
1167 assert!(client.genesis_commitment.read().is_none());
1168
1169 let commitment = Word::default();
1170 client.set_genesis_commitment(commitment).await.unwrap();
1171
1172 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1173 }
1174
1175 #[tokio::test]
1176 async fn set_genesis_commitment_does_nothing_if_the_commitment_is_already_set() {
1177 let endpoint = &Endpoint::devnet();
1178 let client = GrpcClient::new(endpoint, 10000);
1179
1180 let initial_commitment = Word::default();
1181 client.set_genesis_commitment(initial_commitment).await.unwrap();
1182
1183 let new_commitment = Word::from([1u32, 2, 3, 4]);
1184 client.set_genesis_commitment(new_commitment).await.unwrap();
1185
1186 assert_eq!(client.genesis_commitment.read().unwrap(), initial_commitment);
1187 }
1188
1189 #[tokio::test]
1190 async fn set_genesis_commitment_updates_the_client_if_already_connected() {
1191 let endpoint = &Endpoint::devnet();
1192 let client = GrpcClient::new(endpoint, 10000);
1193
1194 client.connect().await.unwrap();
1196
1197 let commitment = Word::default();
1198 client.set_genesis_commitment(commitment).await.unwrap();
1199
1200 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1201 assert!(client.client.read().as_ref().is_some());
1202 }
1203
1204 #[test]
1205 fn with_bearer_auth_stores_token() {
1206 let endpoint = &Endpoint::devnet();
1207 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token-one".to_string());
1208
1209 assert_eq!(client.bearer_token.as_deref(), Some("token-one"));
1210 }
1211
1212 #[test]
1213 fn with_bearer_auth_overwrites_on_repeat_call() {
1214 let endpoint = &Endpoint::devnet();
1215 let client = GrpcClient::new(endpoint, 10000)
1216 .with_bearer_auth("token-one".to_string())
1217 .with_bearer_auth("token-two".to_string());
1218
1219 assert_eq!(client.bearer_token.as_deref(), Some("token-two"));
1221 }
1222
1223 #[tokio::test]
1224 async fn with_bearer_auth_surfaces_invalid_ascii_value_at_connect_time() {
1225 let endpoint = &Endpoint::devnet();
1229 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("bad\nvalue".to_string());
1230
1231 let err = client.connect().await.expect_err("expected invalid token to fail connect");
1232 assert!(
1233 matches!(err, RpcError::ConnectionError(_)),
1234 "expected ConnectionError, got {err:?}",
1235 );
1236 }
1237
1238 #[tokio::test]
1239 async fn with_bearer_auth_is_preserved_across_set_genesis_commitment() {
1240 let endpoint = &Endpoint::devnet();
1241 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token".to_string());
1242 client.connect().await.unwrap();
1243
1244 client.set_genesis_commitment(Word::default()).await.unwrap();
1245
1246 assert_eq!(client.bearer_token.as_deref(), Some("token"));
1248 assert!(client.client.read().as_ref().is_some());
1249 }
1250
1251 #[test]
1252 fn with_max_decoding_message_size_overrides_default() {
1253 let endpoint = &Endpoint::devnet();
1254
1255 let default_client = GrpcClient::new(endpoint, 10_000);
1257 assert_eq!(default_client.max_decoding_message_size, DEFAULT_MAX_RESPONSE_SIZE_BYTES);
1258
1259 let custom =
1261 GrpcClient::new(endpoint, 10_000).with_max_decoding_message_size(8 * 1024 * 1024);
1262 assert_eq!(custom.max_decoding_message_size, 8 * 1024 * 1024);
1263 }
1264
1265 #[tokio::test]
1277 #[ignore = "requires network access to public testnet"]
1278 async fn with_bearer_auth_does_not_break_real_rpc_against_testnet() {
1279 let endpoint = &Endpoint::testnet();
1280 let client = GrpcClient::new(endpoint, 10_000).with_bearer_auth("smoke-test".to_string());
1281
1282 let status = client
1283 .get_status_unversioned()
1284 .await
1285 .expect("testnet status with caller auth header must succeed");
1286 assert!(!status.version.is_empty(), "status must include a server version");
1287 }
1288}