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::{AccountCode, AccountId};
14use miden_protocol::address::NetworkId;
15use miden_protocol::batch::{ProposedBatch, ProvenBatch};
16use miden_protocol::block::account_tree::AccountWitness;
17use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
18use miden_protocol::crypto::merkle::MerklePath;
19use miden_protocol::crypto::merkle::mmr::{Forest, MmrPath, MmrProof};
20use miden_protocol::note::{NoteId, NoteScript, NoteTag};
21use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};
22use miden_protocol::utils::serde::Deserializable;
23use miden_protocol::{EMPTY_WORD, Word};
24use miden_tx::utils::serde::Serializable;
25use miden_tx::utils::sync::RwLock;
26use tonic::Status;
27use tracing::info;
28
29use super::domain::account::{
30 AccountProof,
31 AccountStorageRequirements,
32 GetAccountRequest,
33 StorageMapFetch,
34};
35use super::domain::note::{FetchedNote, NoteSyncBlock};
36use super::domain::nullifier::NullifierUpdate;
37use super::generated::rpc::AccountRequest;
38use super::generated::rpc::account_request::AccountDetailRequest;
39use super::{Endpoint, NodeRpcClient, RpcEndpoint, RpcError, RpcStatusInfo};
40use crate::rpc::domain::account_vault::{AccountVaultInfo, AccountVaultUpdate};
41use crate::rpc::domain::limits::RpcLimits;
42use crate::rpc::domain::status::NetworkNoteStatusInfo;
43use crate::rpc::domain::storage_map::{StorageMapInfo, StorageMapUpdate};
44use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
45use crate::rpc::domain::transaction::TransactionRecord;
46use crate::rpc::errors::node::parse_node_error;
47use crate::rpc::errors::{AcceptHeaderContext, AcceptHeaderError, GrpcError, RpcConversionError};
48use crate::rpc::generated::rpc::BlockRange;
49use crate::rpc::{AccountStateAt, generated as proto};
50
51mod api_client;
52mod retry;
53
54use api_client::api_client_wrapper::ApiClient;
55
56struct BlockPagination {
58 current_block_from: BlockNumber,
59 block_to: BlockNumber,
60 iterations: u32,
61}
62
63enum PaginationResult {
64 Continue,
65 Done {
66 chain_tip: BlockNumber,
67 block_num: BlockNumber,
68 },
69}
70
71impl BlockPagination {
72 const MAX_ITERATIONS: u32 = 1000;
77
78 fn new(block_from: BlockNumber, block_to: BlockNumber) -> Self {
79 Self {
80 current_block_from: block_from,
81 block_to,
82 iterations: 0,
83 }
84 }
85
86 fn current_block_from(&self) -> BlockNumber {
87 self.current_block_from
88 }
89
90 fn block_to(&self) -> BlockNumber {
91 self.block_to
92 }
93
94 fn advance(
95 &mut self,
96 block_num: BlockNumber,
97 chain_tip: BlockNumber,
98 ) -> Result<PaginationResult, RpcError> {
99 if self.iterations >= Self::MAX_ITERATIONS {
100 return Err(RpcError::PaginationError(
101 "too many pagination iterations, possible infinite loop".to_owned(),
102 ));
103 }
104 self.iterations += 1;
105
106 if block_num < self.current_block_from {
107 return Err(RpcError::PaginationError(
108 "invalid pagination: block_num went backwards".to_owned(),
109 ));
110 }
111
112 let target_block = self.block_to.min(chain_tip);
113
114 if block_num >= target_block {
115 return Ok(PaginationResult::Done { chain_tip, block_num });
116 }
117
118 self.current_block_from = BlockNumber::from(block_num.as_u32().saturating_add(1));
119
120 Ok(PaginationResult::Continue)
121 }
122}
123
124const DEFAULT_MAX_RESPONSE_SIZE_BYTES: usize = 4 * 1024 * 1024 * 115 / 100;
130
131pub struct GrpcClient {
142 client: RwLock<Option<ApiClient>>,
144 endpoint: String,
146 timeout_ms: u64,
148 genesis_commitment: RwLock<Option<Word>>,
150 limits: RwLock<Option<RpcLimits>>,
152 max_retries: u32,
154 retry_interval_ms: u64,
156 bearer_token: Option<String>,
160 max_decoding_message_size: usize,
163}
164
165impl GrpcClient {
166 pub fn new(endpoint: &Endpoint, timeout_ms: u64) -> GrpcClient {
169 GrpcClient {
170 client: RwLock::new(None),
171 endpoint: endpoint.to_string(),
172 timeout_ms,
173 genesis_commitment: RwLock::new(None),
174 limits: RwLock::new(None),
175 max_retries: retry::DEFAULT_MAX_RETRIES,
176 retry_interval_ms: retry::DEFAULT_RETRY_INTERVAL_MS,
177 bearer_token: None,
178 max_decoding_message_size: DEFAULT_MAX_RESPONSE_SIZE_BYTES,
179 }
180 }
181
182 #[must_use]
185 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
186 self.max_retries = max_retries;
187 self
188 }
189
190 #[must_use]
193 pub fn with_retry_interval_ms(mut self, retry_interval_ms: u64) -> Self {
194 self.retry_interval_ms = retry_interval_ms;
195 self
196 }
197
198 #[must_use]
205 pub fn with_max_decoding_message_size(mut self, max_decoding_message_size: usize) -> Self {
206 self.max_decoding_message_size = max_decoding_message_size;
207 self
208 }
209
210 #[must_use]
235 pub fn with_bearer_auth(mut self, token: String) -> Self {
236 self.bearer_token = Some(token);
237 self
238 }
239
240 async fn ensure_connected(&self) -> Result<ApiClient, RpcError> {
243 if self.client.read().is_none() {
244 self.connect().await?;
245 }
246
247 Ok(self.client.read().as_ref().expect("rpc_api should be initialized").clone())
248 }
249
250 async fn connect(&self) -> Result<(), RpcError> {
253 let genesis_commitment = *self.genesis_commitment.read();
254 let new_client = ApiClient::new_client(
255 self.endpoint.clone(),
256 self.timeout_ms,
257 genesis_commitment,
258 self.bearer_token.clone(),
259 self.max_decoding_message_size,
260 )
261 .await?;
262 let mut client = self.client.write();
263 client.replace(new_client);
264
265 Ok(())
266 }
267
268 fn rpc_error_from_status(&self, endpoint: RpcEndpoint, status: Status) -> RpcError {
269 let genesis_commitment = self
270 .genesis_commitment
271 .read()
272 .as_ref()
273 .map_or_else(|| "none".to_string(), Word::to_hex);
274 let context = AcceptHeaderContext {
275 client_version: env!("CARGO_PKG_VERSION").to_string(),
276 genesis_commitment,
277 };
278 RpcError::from_grpc_error_with_context(endpoint, status, context)
279 }
280
281 async fn call_with_retry<T: Send + 'static>(
292 &self,
293 endpoint: RpcEndpoint,
294 mut call: impl FnMut(ApiClient) -> RpcFuture<Result<tonic::Response<T>, Status>>,
295 ) -> Result<tonic::Response<T>, RpcError> {
296 let mut retry_state = retry::RetryState::new(self.max_retries, self.retry_interval_ms);
297
298 loop {
299 let rpc_api = self.ensure_connected().await?;
300
301 match call(rpc_api).await {
302 Ok(response) => return Ok(response),
303 Err(status) if retry_state.should_retry(&status).await => {},
304 Err(status) => return Err(self.rpc_error_from_status(endpoint, status)),
305 }
306 }
307 }
308
309 pub async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
316 let mut rpc_api = ApiClient::new_client_without_accept_header(
317 self.endpoint.clone(),
318 self.timeout_ms,
319 self.bearer_token.clone(),
320 self.max_decoding_message_size,
321 )
322 .await?;
323 rpc_api
324 .status(())
325 .await
326 .map_err(|status| self.rpc_error_from_status(RpcEndpoint::Status, status))
327 .map(tonic::Response::into_inner)
328 .and_then(RpcStatusInfo::try_from)
329 }
330}
331
332#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
333#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
334impl NodeRpcClient for GrpcClient {
335 fn has_genesis_commitment(&self) -> Option<Word> {
340 *self.genesis_commitment.read()
341 }
342
343 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError> {
344 if self.genesis_commitment.read().is_some() {
346 return Ok(());
348 }
349
350 self.genesis_commitment.write().replace(commitment);
352
353 let mut client_guard = self.client.write();
356 if let Some(client) = client_guard.as_mut() {
357 client.set_genesis_commitment(commitment);
358 }
359
360 Ok(())
361 }
362
363 async fn submit_proven_transaction(
364 &self,
365 proven_transaction: ProvenTransaction,
366 transaction_inputs: TransactionInputs,
367 ) -> Result<BlockNumber, RpcError> {
368 let request = proto::transaction::ProvenTransaction {
369 transaction: proven_transaction.to_bytes(),
370 transaction_inputs: Some(transaction_inputs.to_bytes()),
371 };
372
373 let api_response = self
374 .call_with_retry(RpcEndpoint::SubmitProvenTx, |mut rpc_api| {
375 let request = request.clone();
376 Box::pin(async move { rpc_api.submit_proven_tx(request).await })
377 })
378 .await?;
379
380 Ok(BlockNumber::from(api_response.into_inner().block_num))
381 }
382
383 async fn submit_proven_batch(
384 &self,
385 proven_batch: ProvenBatch,
386 proposed_batch: ProposedBatch,
387 transaction_inputs: Vec<TransactionInputs>,
388 ) -> Result<BlockNumber, RpcError> {
389 let request = proto::transaction::TransactionBatch {
390 batch_proof: proven_batch.to_bytes(),
391 proposed_batch: Some(proposed_batch.to_bytes()),
392 transaction_inputs: transaction_inputs.iter().map(Serializable::to_bytes).collect(),
393 };
394
395 let api_response = self
396 .call_with_retry(RpcEndpoint::SubmitProvenBatch, |mut rpc_api| {
397 let request = request.clone();
398 Box::pin(async move { rpc_api.submit_proven_tx_batch(request).await })
399 })
400 .await?;
401
402 Ok(BlockNumber::from(api_response.into_inner().block_num))
403 }
404
405 async fn get_block_header_by_number(
406 &self,
407 block_num: Option<BlockNumber>,
408 include_mmr_proof: bool,
409 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
410 let request = proto::rpc::BlockHeaderByNumberRequest {
411 block_num: block_num.as_ref().map(BlockNumber::as_u32),
412 include_mmr_proof: Some(include_mmr_proof),
413 };
414
415 info!("Calling GetBlockHeaderByNumber: {:?}", request);
416
417 let api_response = self
418 .call_with_retry(RpcEndpoint::GetBlockHeaderByNumber, |mut rpc_api| {
419 Box::pin(async move { rpc_api.get_block_header_by_number(request).await })
420 })
421 .await?;
422
423 let response = api_response.into_inner();
424
425 let block_header: BlockHeader = response
426 .block_header
427 .ok_or(RpcError::ExpectedDataMissing("BlockHeader".into()))?
428 .try_into()?;
429
430 let mmr_proof = if include_mmr_proof {
431 let forest = response
432 .chain_length
433 .ok_or(RpcError::ExpectedDataMissing("ChainLength".into()))?;
434 let merkle_path: MerklePath = response
435 .mmr_path
436 .ok_or(RpcError::ExpectedDataMissing("MmrPath".into()))?
437 .try_into()?;
438
439 let forest_size = usize::try_from(forest).expect("u64 should fit in usize");
440 let forest = Forest::new(forest_size).map_err(|_| {
441 RpcError::InvalidResponse(format!("invalid forest size: {forest_size}"))
442 })?;
443 Some(MmrProof::new(
444 MmrPath::new(forest, block_header.block_num().as_usize(), merkle_path),
445 block_header.commitment(),
446 ))
447 } else {
448 None
449 };
450
451 Ok((block_header, mmr_proof))
452 }
453
454 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
455 let limits = self.get_rpc_limits().await?;
456 let mut notes = Vec::with_capacity(note_ids.len());
457 for chunk in note_ids.chunks(limits.note_ids_limit as usize) {
458 let request = proto::note::NoteIdList {
459 ids: chunk.iter().map(|id| (*id).into()).collect(),
460 };
461
462 let api_response = self
463 .call_with_retry(RpcEndpoint::GetNotesById, |mut rpc_api| {
464 let request = request.clone();
465 Box::pin(async move { rpc_api.get_notes_by_id(request).await })
466 })
467 .await?;
468
469 let response_notes = api_response
470 .into_inner()
471 .notes
472 .into_iter()
473 .map(FetchedNote::try_from)
474 .collect::<Result<Vec<FetchedNote>, RpcConversionError>>()?;
475
476 notes.extend(response_notes);
477 }
478 Ok(notes)
479 }
480
481 async fn sync_chain_mmr(
482 &self,
483 current_block_height: BlockNumber,
484 upper_bound: SyncTarget,
485 ) -> Result<ChainMmrInfo, RpcError> {
486 let finality_level: proto::rpc::FinalityLevel = upper_bound.into();
487
488 let request = proto::rpc::SyncChainMmrRequest {
489 current_client_block_height: current_block_height.as_u32(),
490 finality_level: finality_level.into(),
491 };
492
493 let response = self
494 .call_with_retry(RpcEndpoint::SyncChainMmr, |mut rpc_api| {
495 Box::pin(async move { rpc_api.sync_chain_mmr(request).await })
496 })
497 .await?;
498
499 response.into_inner().try_into()
500 }
501
502 async fn get_account(
514 &self,
515 account_id: AccountId,
516 request: GetAccountRequest,
517 ) -> Result<(BlockNumber, AccountProof), RpcError> {
518 let GetAccountRequest { storage, at, known_code, vault } = request;
519
520 let known_code_commitment = known_code.as_ref().map_or(EMPTY_WORD, AccountCode::commitment);
521 let mut known_codes_by_commitment: BTreeMap<Word, AccountCode> = BTreeMap::new();
522 if let Some(account_code) = known_code {
523 known_codes_by_commitment.insert(account_code.commitment(), account_code);
524 }
525
526 let requirements = match storage.clone() {
528 StorageMapFetch::Slots(reqs) => reqs,
529 StorageMapFetch::Skip | StorageMapFetch::All => AccountStorageRequirements::default(),
530 };
531
532 let account_details = if account_id.is_public() {
535 Some(AccountDetailRequest {
536 code_commitment: Some(known_code_commitment.into()),
537 asset_vault_commitment: vault.into(),
538 storage_request: storage.into(),
539 })
540 } else {
541 None
542 };
543
544 let block_num = match at {
545 AccountStateAt::Block(number) => Some(number.into()),
546 AccountStateAt::ChainTip => None,
547 };
548
549 let proto_request = AccountRequest {
550 account_id: Some(account_id.into()),
551 block_num,
552 details: account_details,
553 };
554
555 let response = self
556 .call_with_retry(RpcEndpoint::GetAccount, |mut rpc_api| {
557 let request = proto_request.clone();
558 Box::pin(async move { rpc_api.get_account(request).await })
559 })
560 .await?
561 .into_inner();
562
563 let account_witness: AccountWitness = response
564 .witness
565 .ok_or(RpcError::ExpectedDataMissing("AccountWitness".to_string()))?
566 .try_into()?;
567
568 let block_num: BlockNumber = response
569 .block_num
570 .ok_or(RpcError::ExpectedDataMissing("response block num".to_string()))?
571 .block_num
572 .into();
573
574 let headers = if account_witness.id().is_public() {
576 let details = response
577 .details
578 .ok_or(RpcError::ExpectedDataMissing("Account.Details".to_string()))?
579 .into_domain(&known_codes_by_commitment, &requirements)?;
580
581 Some(details)
582 } else {
583 None
584 };
585
586 let proof = AccountProof::new(account_witness, headers)
587 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
588
589 Ok((block_num, proof))
590 }
591
592 async fn sync_notes(
598 &self,
599 block_from: BlockNumber,
600 block_to: BlockNumber,
601 note_tags: &BTreeSet<NoteTag>,
602 ) -> Result<Vec<NoteSyncBlock>, RpcError> {
603 if note_tags.is_empty() {
604 return Ok(Vec::new());
605 }
606
607 let limits = self.get_rpc_limits().await?;
608 let tags: Vec<NoteTag> = note_tags.iter().copied().collect();
609
610 let mut merged_blocks: BTreeMap<BlockNumber, NoteSyncBlock> = BTreeMap::new();
613
614 for chunk in tags.chunks(limits.note_tags_limit as usize) {
615 let proto_tags: Vec<u32> = chunk.iter().map(|&t| t.into()).collect();
616 let mut pagination = BlockPagination::new(block_from, block_to);
617
618 loop {
619 let request = proto::rpc::SyncNotesRequest {
620 block_range: Some(BlockRange {
621 block_from: pagination.current_block_from().as_u32(),
622 block_to: block_to.as_u32(),
623 }),
624 note_tags: proto_tags.clone(),
625 };
626
627 let response = self
628 .call_with_retry(RpcEndpoint::SyncNotes, |mut rpc_api| {
629 let request = request.clone();
630 Box::pin(async move { rpc_api.sync_notes(request).await })
631 })
632 .await?
633 .into_inner();
634
635 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
636 "SyncNotesResponse.pagination_info".to_owned(),
637 ))?;
638 let page_chain_tip = BlockNumber::from(page.chain_tip);
639 let page_block_to = BlockNumber::from(page.block_num);
640
641 for proto_block in response.blocks {
642 let block: NoteSyncBlock = proto_block.try_into()?;
643 let bn = block.block_header.block_num();
644 if let Some(existing) = merged_blocks.get_mut(&bn) {
645 for (id, note) in block.notes {
646 existing.notes.entry(id).or_insert(note);
647 }
648 } else {
649 merged_blocks.insert(bn, block);
650 }
651 }
652
653 match pagination.advance(page_block_to, page_chain_tip)? {
654 PaginationResult::Continue => {},
655 PaginationResult::Done { .. } => break,
656 }
657 }
658 }
659
660 Ok(merged_blocks.into_values().collect())
661 }
662
663 async fn sync_nullifiers(
664 &self,
665 prefixes: &[u16],
666 block_from: BlockNumber,
667 block_to: BlockNumber,
668 ) -> Result<Vec<NullifierUpdate>, RpcError> {
669 let limits = self.get_rpc_limits().await?;
670 let mut all_nullifiers = BTreeSet::new();
671
672 for chunk in prefixes.chunks(limits.nullifiers_limit as usize) {
675 let proto_prefixes: Vec<u32> = chunk.iter().map(|&x| u32::from(x)).collect();
676 let mut pagination = BlockPagination::new(block_from, block_to);
677
678 loop {
679 let request = proto::rpc::SyncNullifiersRequest {
680 nullifiers: proto_prefixes.clone(),
681 prefix_len: 16,
682 block_range: Some(BlockRange {
683 block_from: pagination.current_block_from().as_u32(),
684 block_to: pagination.block_to().as_u32(),
685 }),
686 };
687
688 let response = self
689 .call_with_retry(RpcEndpoint::SyncNullifiers, |mut rpc_api| {
690 let request = request.clone();
691 Box::pin(async move { rpc_api.sync_nullifiers(request).await })
692 })
693 .await?
694 .into_inner();
695
696 let batch_nullifiers = response
697 .nullifiers
698 .iter()
699 .map(TryFrom::try_from)
700 .collect::<Result<Vec<NullifierUpdate>, _>>()
701 .map_err(|err| RpcError::InvalidResponse(err.to_string()))?;
702
703 all_nullifiers.extend(batch_nullifiers);
704
705 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
706 "SyncNullifiersResponse.pagination_info".to_owned(),
707 ))?;
708
709 match pagination.advance(page.block_num.into(), page.chain_tip.into())? {
710 PaginationResult::Continue => {},
711 PaginationResult::Done { .. } => break,
712 }
713 }
714 }
715 Ok(all_nullifiers.into_iter().collect::<Vec<_>>())
716 }
717
718 async fn get_block_by_number(
719 &self,
720 block_num: BlockNumber,
721 include_proof: bool,
722 ) -> Result<ProvenBlock, RpcError> {
723 let request = proto::blockchain::BlockRequest {
724 block_num: block_num.as_u32(),
725 include_proof: Some(include_proof),
726 };
727
728 let response = self
729 .call_with_retry(RpcEndpoint::GetBlockByNumber, |mut rpc_api| {
730 Box::pin(async move { rpc_api.get_block_by_number(request).await })
731 })
732 .await?;
733
734 let response = response.into_inner();
735 let block =
736 ProvenBlock::read_from_bytes(&response.block.ok_or(RpcError::ExpectedDataMissing(
737 "GetBlockByNumberResponse.block".to_string(),
738 ))?)?;
739
740 Ok(block)
741 }
742
743 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
744 let request = proto::note::NoteScriptRoot { root: Some(root.into()) };
745
746 let response = self
747 .call_with_retry(RpcEndpoint::GetNoteScriptByRoot, |mut rpc_api| {
748 Box::pin(async move { rpc_api.get_note_script_by_root(request).await })
749 })
750 .await?;
751
752 let Some(script) = response.into_inner().script else {
754 return Ok(None);
755 };
756 let note_script = NoteScript::try_from(script)?;
757
758 let fetched_root = note_script.root();
759 if Word::from(fetched_root) != root {
760 return Err(RpcError::InvalidResponse(format!(
761 "node returned note script with root {fetched_root} for requested root {root}",
762 )));
763 }
764
765 Ok(Some(note_script))
766 }
767
768 async fn sync_storage_maps(
769 &self,
770 block_from: BlockNumber,
771 block_to: BlockNumber,
772 account_id: AccountId,
773 ) -> Result<StorageMapInfo, RpcError> {
774 let mut pagination = BlockPagination::new(block_from, block_to);
775 let mut updates = Vec::new();
776
777 let (chain_tip, block_number) = loop {
778 let request = proto::rpc::SyncAccountStorageMapsRequest {
779 block_range: Some(BlockRange {
780 block_from: pagination.current_block_from().as_u32(),
781 block_to: block_to.as_u32(),
782 }),
783 account_id: Some(account_id.into()),
784 };
785 let response = self
786 .call_with_retry(RpcEndpoint::SyncStorageMaps, |mut rpc_api| {
787 let request = request.clone();
788 Box::pin(async move { rpc_api.sync_account_storage_maps(request).await })
789 })
790 .await?;
791 let response = response.into_inner();
792 let page = response
793 .pagination_info
794 .ok_or(RpcError::ExpectedDataMissing("pagination_info".to_owned()))?;
795 let page_block_num = BlockNumber::from(page.block_num);
796 let page_chain_tip = BlockNumber::from(page.chain_tip);
797 let batch = response
798 .updates
799 .into_iter()
800 .map(TryInto::try_into)
801 .collect::<Result<Vec<StorageMapUpdate>, _>>()?;
802 updates.extend(batch);
803
804 match pagination.advance(page_block_num, page_chain_tip)? {
805 PaginationResult::Continue => {},
806 PaginationResult::Done {
807 chain_tip: final_chain_tip,
808 block_num: final_block_num,
809 } => break (final_chain_tip, final_block_num),
810 }
811 };
812
813 Ok(StorageMapInfo { chain_tip, block_number, updates })
814 }
815
816 async fn sync_account_vault(
817 &self,
818 block_from: BlockNumber,
819 block_to: BlockNumber,
820 account_id: AccountId,
821 ) -> Result<AccountVaultInfo, RpcError> {
822 let mut pagination = BlockPagination::new(block_from, block_to);
823 let mut updates = Vec::new();
824
825 let (chain_tip, block_number) = loop {
826 let request = proto::rpc::SyncAccountVaultRequest {
827 block_range: Some(BlockRange {
828 block_from: pagination.current_block_from().as_u32(),
829 block_to: block_to.as_u32(),
830 }),
831 account_id: Some(account_id.into()),
832 };
833 let response = self
834 .call_with_retry(RpcEndpoint::SyncAccountVault, |mut rpc_api| {
835 let request = request.clone();
836 Box::pin(async move { rpc_api.sync_account_vault(request).await })
837 })
838 .await?;
839 let response = response.into_inner();
840 let page = response
841 .pagination_info
842 .ok_or(RpcError::ExpectedDataMissing("pagination_info".to_owned()))?;
843 let page_block_num = BlockNumber::from(page.block_num);
844 let page_chain_tip = BlockNumber::from(page.chain_tip);
845 let batch = response
846 .updates
847 .iter()
848 .map(|u| (*u).try_into())
849 .collect::<Result<Vec<AccountVaultUpdate>, _>>()?;
850 updates.extend(batch);
851
852 match pagination.advance(page_block_num, page_chain_tip)? {
853 PaginationResult::Continue => {},
854 PaginationResult::Done {
855 chain_tip: final_chain_tip,
856 block_num: final_block_num,
857 } => break (final_chain_tip, final_block_num),
858 }
859 };
860
861 Ok(AccountVaultInfo { chain_tip, block_number, updates })
862 }
863
864 async fn sync_transactions(
870 &self,
871 block_from: BlockNumber,
872 block_to: BlockNumber,
873 account_ids: Vec<AccountId>,
874 ) -> Result<Vec<TransactionRecord>, RpcError> {
875 if account_ids.is_empty() {
876 return Ok(Vec::new());
877 }
878
879 let limits = self.get_rpc_limits().await?;
880 let mut transactions: Vec<TransactionRecord> = Vec::new();
881
882 for chunk in account_ids.chunks(limits.account_ids_limit as usize) {
883 let proto_account_ids: Vec<_> = chunk.iter().map(|acc_id| (*acc_id).into()).collect();
884 let mut pagination = BlockPagination::new(block_from, block_to);
885
886 loop {
887 let request = proto::rpc::SyncTransactionsRequest {
888 block_range: Some(BlockRange {
889 block_from: pagination.current_block_from().as_u32(),
890 block_to: block_to.as_u32(),
891 }),
892 account_ids: proto_account_ids.clone(),
893 };
894
895 let response = self
896 .call_with_retry(RpcEndpoint::SyncTransactions, |mut rpc_api| {
897 let request = request.clone();
898 Box::pin(async move { rpc_api.sync_transactions(request).await })
899 })
900 .await?
901 .into_inner();
902
903 let page = response.pagination_info.ok_or(RpcError::ExpectedDataMissing(
904 "SyncTransactionsResponse.pagination_info".to_owned(),
905 ))?;
906 let page_chain_tip = BlockNumber::from(page.chain_tip);
907 let page_block_to = BlockNumber::from(page.block_num);
908
909 for proto_tx in response.transactions {
910 transactions.push(TransactionRecord::try_from(proto_tx)?);
911 }
912
913 match pagination.advance(page_block_to, page_chain_tip)? {
914 PaginationResult::Continue => {},
915 PaginationResult::Done { .. } => break,
916 }
917 }
918 }
919
920 Ok(transactions)
921 }
922
923 async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
924 let endpoint: Endpoint =
925 Endpoint::try_from(self.endpoint.as_str()).map_err(RpcError::InvalidNodeEndpoint)?;
926 Ok(endpoint.to_network_id())
927 }
928
929 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError> {
930 if let Some(limits) = *self.limits.read() {
932 return Ok(limits);
933 }
934
935 let response = self
937 .call_with_retry(RpcEndpoint::GetLimits, |mut rpc_api| {
938 Box::pin(async move { rpc_api.get_limits(()).await })
939 })
940 .await?;
941 let limits = RpcLimits::try_from(response.into_inner()).map_err(RpcError::from)?;
942
943 self.limits.write().replace(limits);
945 Ok(limits)
946 }
947
948 fn has_rpc_limits(&self) -> Option<RpcLimits> {
949 *self.limits.read()
950 }
951
952 async fn set_rpc_limits(&self, limits: RpcLimits) {
953 self.limits.write().replace(limits);
954 }
955
956 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
957 GrpcClient::get_status_unversioned(self).await
958 }
959
960 async fn get_network_note_status(
961 &self,
962 note_id: NoteId,
963 ) -> Result<NetworkNoteStatusInfo, RpcError> {
964 let request = proto::note::NoteId { id: Some(note_id.into()) };
965
966 let response = self
967 .call_with_retry(RpcEndpoint::GetNetworkNoteStatus, |mut rpc_api| {
968 Box::pin(async move { rpc_api.get_network_note_status(request).await })
969 })
970 .await?;
971
972 response.into_inner().try_into()
973 }
974}
975
976impl RpcError {
980 pub fn from_grpc_error_with_context(
981 endpoint: RpcEndpoint,
982 status: Status,
983 context: AcceptHeaderContext,
984 ) -> Self {
985 if let Some(accept_error) =
986 AcceptHeaderError::try_from_message_with_context(status.message(), context)
987 {
988 return Self::AcceptHeaderError(accept_error);
989 }
990
991 let endpoint_error = parse_node_error(&endpoint, status.details(), status.message());
993
994 let error_kind = GrpcError::from(&status);
995 let source = Box::new(status) as Box<dyn Error + Send + Sync + 'static>;
996
997 Self::RequestError {
998 endpoint,
999 error_kind,
1000 endpoint_error,
1001 source: Some(source),
1002 }
1003 }
1004}
1005
1006impl From<&Status> for GrpcError {
1007 fn from(status: &Status) -> Self {
1008 GrpcError::from_code(status.code() as i32, Some(status.message().to_string()))
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use std::boxed::Box;
1015
1016 use miden_protocol::Word;
1017 use miden_protocol::block::BlockNumber;
1018
1019 use super::{BlockPagination, DEFAULT_MAX_RESPONSE_SIZE_BYTES, GrpcClient, PaginationResult};
1020 use crate::alloc::string::ToString;
1021 use crate::rpc::{Endpoint, NodeRpcClient, RpcError};
1022
1023 fn assert_send_sync<T: Send + Sync>() {}
1024
1025 #[test]
1026 fn is_send_sync() {
1027 assert_send_sync::<GrpcClient>();
1028 assert_send_sync::<Box<dyn NodeRpcClient>>();
1029 }
1030
1031 #[test]
1032 fn block_pagination_errors_when_block_num_goes_backwards() {
1033 let mut pagination = BlockPagination::new(10_u32.into(), 20_u32.into());
1034
1035 let res = pagination.advance(9_u32.into(), 20_u32.into());
1036 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1037 }
1038
1039 #[test]
1040 fn block_pagination_errors_after_max_iterations() {
1041 let mut pagination = BlockPagination::new(0_u32.into(), 10_000_u32.into());
1042 let chain_tip: BlockNumber = 10_000_u32.into();
1043
1044 for _ in 0..BlockPagination::MAX_ITERATIONS {
1045 let current = pagination.current_block_from();
1046 let res = pagination
1047 .advance(current, chain_tip)
1048 .expect("expected pagination to continue within iteration limit");
1049 assert!(matches!(res, PaginationResult::Continue));
1050 }
1051
1052 let res = pagination.advance(pagination.current_block_from(), chain_tip);
1053 assert!(matches!(res, Err(RpcError::PaginationError(_))));
1054 }
1055
1056 #[test]
1057 fn block_pagination_stops_at_min_of_block_to_and_chain_tip() {
1058 let mut pagination = BlockPagination::new(0_u32.into(), 50_u32.into());
1060
1061 let res = pagination
1062 .advance(30_u32.into(), 30_u32.into())
1063 .expect("expected pagination to succeed");
1064
1065 assert!(matches!(
1066 res,
1067 PaginationResult::Done {
1068 chain_tip,
1069 block_num
1070 } if chain_tip.as_u32() == 30 && block_num.as_u32() == 30
1071 ));
1072 }
1073
1074 #[test]
1075 fn block_pagination_advances_cursor_by_one() {
1076 let mut pagination = BlockPagination::new(5_u32.into(), 100_u32.into());
1077
1078 let res = pagination
1079 .advance(5_u32.into(), 100_u32.into())
1080 .expect("expected pagination to succeed");
1081 assert!(matches!(res, PaginationResult::Continue));
1082 assert_eq!(pagination.current_block_from().as_u32(), 6);
1083 }
1084
1085 async fn dyn_trait_send_fut(client: Box<dyn NodeRpcClient>) {
1087 let res = client.get_block_header_by_number(None, false).await;
1089 assert!(res.is_ok());
1090 }
1091
1092 #[tokio::test]
1093 async fn future_is_send() {
1094 let endpoint = &Endpoint::devnet();
1095 let client = GrpcClient::new(endpoint, 10000);
1096 let client: Box<GrpcClient> = client.into();
1097 tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
1098 }
1099
1100 #[tokio::test]
1101 async fn set_genesis_commitment_sets_the_commitment_when_its_not_already_set() {
1102 let endpoint = &Endpoint::devnet();
1103 let client = GrpcClient::new(endpoint, 10000);
1104
1105 assert!(client.genesis_commitment.read().is_none());
1106
1107 let commitment = Word::default();
1108 client.set_genesis_commitment(commitment).await.unwrap();
1109
1110 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1111 }
1112
1113 #[tokio::test]
1114 async fn set_genesis_commitment_does_nothing_if_the_commitment_is_already_set() {
1115 let endpoint = &Endpoint::devnet();
1116 let client = GrpcClient::new(endpoint, 10000);
1117
1118 let initial_commitment = Word::default();
1119 client.set_genesis_commitment(initial_commitment).await.unwrap();
1120
1121 let new_commitment = Word::from([1u32, 2, 3, 4]);
1122 client.set_genesis_commitment(new_commitment).await.unwrap();
1123
1124 assert_eq!(client.genesis_commitment.read().unwrap(), initial_commitment);
1125 }
1126
1127 #[tokio::test]
1128 async fn set_genesis_commitment_updates_the_client_if_already_connected() {
1129 let endpoint = &Endpoint::devnet();
1130 let client = GrpcClient::new(endpoint, 10000);
1131
1132 client.connect().await.unwrap();
1134
1135 let commitment = Word::default();
1136 client.set_genesis_commitment(commitment).await.unwrap();
1137
1138 assert_eq!(client.genesis_commitment.read().unwrap(), commitment);
1139 assert!(client.client.read().as_ref().is_some());
1140 }
1141
1142 #[test]
1143 fn with_bearer_auth_stores_token() {
1144 let endpoint = &Endpoint::devnet();
1145 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token-one".to_string());
1146
1147 assert_eq!(client.bearer_token.as_deref(), Some("token-one"));
1148 }
1149
1150 #[test]
1151 fn with_bearer_auth_overwrites_on_repeat_call() {
1152 let endpoint = &Endpoint::devnet();
1153 let client = GrpcClient::new(endpoint, 10000)
1154 .with_bearer_auth("token-one".to_string())
1155 .with_bearer_auth("token-two".to_string());
1156
1157 assert_eq!(client.bearer_token.as_deref(), Some("token-two"));
1159 }
1160
1161 #[tokio::test]
1162 async fn with_bearer_auth_surfaces_invalid_ascii_value_at_connect_time() {
1163 let endpoint = &Endpoint::devnet();
1167 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("bad\nvalue".to_string());
1168
1169 let err = client.connect().await.expect_err("expected invalid token to fail connect");
1170 assert!(
1171 matches!(err, RpcError::ConnectionError(_)),
1172 "expected ConnectionError, got {err:?}",
1173 );
1174 }
1175
1176 #[tokio::test]
1177 async fn with_bearer_auth_is_preserved_across_set_genesis_commitment() {
1178 let endpoint = &Endpoint::devnet();
1179 let client = GrpcClient::new(endpoint, 10000).with_bearer_auth("token".to_string());
1180 client.connect().await.unwrap();
1181
1182 client.set_genesis_commitment(Word::default()).await.unwrap();
1183
1184 assert_eq!(client.bearer_token.as_deref(), Some("token"));
1186 assert!(client.client.read().as_ref().is_some());
1187 }
1188
1189 #[test]
1190 fn with_max_decoding_message_size_overrides_default() {
1191 let endpoint = &Endpoint::devnet();
1192
1193 let default_client = GrpcClient::new(endpoint, 10_000);
1195 assert_eq!(default_client.max_decoding_message_size, DEFAULT_MAX_RESPONSE_SIZE_BYTES);
1196
1197 let custom =
1199 GrpcClient::new(endpoint, 10_000).with_max_decoding_message_size(8 * 1024 * 1024);
1200 assert_eq!(custom.max_decoding_message_size, 8 * 1024 * 1024);
1201 }
1202
1203 #[tokio::test]
1215 #[ignore = "requires network access to public testnet"]
1216 async fn with_bearer_auth_does_not_break_real_rpc_against_testnet() {
1217 let endpoint = &Endpoint::testnet();
1218 let client = GrpcClient::new(endpoint, 10_000).with_bearer_auth("smoke-test".to_string());
1219
1220 let status = client
1221 .get_status_unversioned()
1222 .await
1223 .expect("testnet status with caller auth header must succeed");
1224 assert!(!status.version.is_empty(), "status must include a server version");
1225 }
1226}