miden_client/rpc/mod.rs
1//! Provides an interface for the client to communicate with a Miden node using
2//! Remote Procedure Calls (RPC).
3//!
4//! This module defines the [`NodeRpcClient`] trait which abstracts calls to the RPC protocol used
5//! to:
6//!
7//! - Submit proven transactions.
8//! - Submit proven batches.
9//! - Retrieve block headers (optionally with MMR proofs).
10//! - Sync state updates (including notes, nullifiers, and account updates).
11//! - Fetch details for specific notes and accounts.
12//!
13//! The client implementation adapts to the target environment automatically:
14//! - Native targets use `tonic` transport with TLS.
15//! - `wasm32` targets use `tonic-web-wasm-client` transport.
16//!
17//! ## Example
18//!
19//! ```no_run
20//! # use miden_client::rpc::{Endpoint, NodeRpcClient, GrpcClient, VerifyingRpcClient};
21//! # use miden_protocol::block::BlockNumber;
22//! # #[tokio::main]
23//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! // Create a gRPC client instance (assumes default endpoint configuration), wrapped so that
25//! // node responses are verified against the requests.
26//! let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
27//! let rpc_client = VerifyingRpcClient::new(GrpcClient::new(&endpoint, 1000));
28//!
29//! // Fetch the latest block header (by passing None).
30//! let (block_header, mmr_proof) = rpc_client.get_block_header_by_number(None, true).await?;
31//!
32//! println!("Latest block number: {}", block_header.block_num());
33//! if let Some(proof) = mmr_proof {
34//! println!("MMR proof received accordingly");
35//! }
36//!
37//! # Ok(())
38//! # }
39//! ```
40//! The client also makes use of this component in order to communicate with the node.
41//!
42//! For further details and examples, see the documentation for the individual methods in the
43//! [`NodeRpcClient`] trait.
44
45use alloc::boxed::Box;
46use alloc::collections::{BTreeMap, BTreeSet};
47use alloc::string::String;
48use alloc::vec::Vec;
49use core::fmt;
50
51use domain::account::{
52 AccountDetails,
53 AccountProof,
54 AccountStorageMapDetails,
55 GetAccountRequest,
56 StorageMapEntries,
57 StorageMapEntry,
58 StorageMapFetch,
59 VaultFetch,
60};
61use domain::note::{FetchedNote, ResolvedSyncNotesBlock, SyncNotesBlock, SyncedNote};
62use domain::nullifier::NullifierUpdate;
63use domain::sync::{ChainMmrInfo, SyncTarget};
64use encryption::{AttestedTransactionEncryptionKey, SealedTransactionInputs};
65use miden_protocol::Word;
66use miden_protocol::account::{Account, AccountId};
67use miden_protocol::address::NetworkId;
68use miden_protocol::batch::{ProposedBatch, ProvenBatch};
69use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
70use miden_protocol::crypto::merkle::mmr::MmrProof;
71use miden_protocol::note::{
72 NoteAttachments,
73 NoteDetails,
74 NoteId,
75 NoteScript,
76 NoteTag,
77 NoteType,
78 Nullifier,
79};
80use miden_protocol::transaction::ProvenTransaction;
81
82use crate::rpc::domain::storage_map::StorageMapInfo;
83
84/// Contains domain types related to RPC requests and responses, as well as utility functions
85/// for dealing with them.
86pub mod domain;
87pub mod encryption;
88
89mod errors;
90pub use errors::*;
91
92mod endpoint;
93pub(crate) use domain::limits::RPC_LIMITS_STORE_SETTING;
94pub use domain::limits::RpcLimits;
95pub use domain::status::{NetworkNoteStatus, NetworkNoteStatusInfo, RpcStatusInfo};
96pub use endpoint::Endpoint;
97
98#[cfg(not(feature = "testing"))]
99mod generated;
100#[cfg(feature = "testing")]
101pub mod generated;
102
103#[cfg(feature = "tonic")]
104mod tonic_client;
105#[cfg(feature = "tonic")]
106pub use tonic_client::GrpcClient;
107
108mod verifying_client;
109pub use verifying_client::VerifyingRpcClient;
110
111use crate::rpc::domain::account_vault::AccountVaultInfo;
112use crate::rpc::domain::transaction::TransactionRecord;
113use crate::store::InputNoteRecord;
114use crate::store::input_note_states::UnverifiedNoteState;
115
116/// Represents the state that we want to retrieve from the network
117#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
118pub enum AccountStateAt {
119 /// Gets the latest state, for the current chain tip
120 #[default]
121 ChainTip,
122 /// Gets the state at a specific block number
123 Block(BlockNumber),
124}
125
126// NODE RPC CLIENT TRAIT
127// ================================================================================================
128
129/// Defines the interface for communicating with the Miden node.
130///
131/// The implementers are responsible for connecting to the Miden node, handling endpoint
132/// requests/responses, and translating responses into domain objects relevant for each of the
133/// endpoints. Implementations do not check that responses correspond to the method's arguments.
134/// Wrap a client in [`VerifyingRpcClient`] to reject mismatched responses.
135#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
136#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
137pub trait NodeRpcClient: Send + Sync {
138 /// Sets the genesis commitment for the client and reconnects to the node providing the
139 /// genesis commitment in the request headers. If the genesis commitment is already set,
140 /// this method does nothing.
141 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError>;
142
143 /// Returns the genesis commitment if it has been set, without fetching from the node.
144 fn has_genesis_commitment(&self) -> Option<Word>;
145
146 /// Fetches the validator set's transaction encryption key using the
147 /// `/GetTransactionEncryptionKey` endpoint.
148 ///
149 /// The key arrives attested but untrusted: this endpoint is served by the RPC operator, so the
150 /// response must be passed through [`AttestedTransactionEncryptionKey::verify`] before it is
151 /// used to seal anything.
152 async fn get_transaction_encryption_key(
153 &self,
154 ) -> Result<AttestedTransactionEncryptionKey, RpcError>;
155
156 /// Given a Proven Transaction, send it to the node for it to be included in a future block
157 /// using the `/SubmitProvenTransaction` RPC endpoint.
158 ///
159 /// The transaction inputs are passed already sealed, since sealing needs the client's RNG
160 /// for the scheme's ephemeral key material. See [`encryption`] for how they are produced.
161 ///
162 /// Returns the node's chain tip at submission (not the block the transaction is committed in).
163 async fn submit_proven_transaction(
164 &self,
165 proven_transaction: ProvenTransaction,
166 sealed_transaction_inputs: SealedTransactionInputs,
167 ) -> Result<BlockNumber, RpcError>;
168
169 /// Given a Proven Batch together with the corresponding [`ProposedBatch`] and the list of
170 /// [`SealedTransactionInputs`] (one per transaction, matching the ordering of the batch), sends
171 /// the batch to the node for inclusion in a future block using the `/SubmitProvenBatch`
172 /// RPC endpoint. All transactions in the batch must build on the current mempool state
173 /// following normal transaction submission rules.
174 ///
175 /// Each transaction's inputs are sealed independently against its own transaction ID, because
176 /// the node fans the batch out into one validator submission per transaction. See
177 /// [`encryption`] for how the sealed inputs are produced.
178 ///
179 /// Returns the node's chain tip at submission (not the block the batch is committed in).
180 async fn submit_proven_batch(
181 &self,
182 proven_batch: ProvenBatch,
183 proposed_batch: ProposedBatch,
184 transaction_inputs: Vec<SealedTransactionInputs>,
185 ) -> Result<BlockNumber, RpcError>;
186
187 /// Given a block number, fetches the block header corresponding to that height from the node
188 /// using the `/GetBlockHeaderByNumber` endpoint.
189 /// If `include_mmr_proof` is set to true and the function returns an `Ok`, the second value
190 /// of the return tuple should always be Some(MmrProof).
191 ///
192 /// When `None` is provided, returns info regarding the latest block.
193 ///
194 /// The returned header is not verified against the requested `block_num`;
195 /// [`VerifyingRpcClient`] performs that check.
196 async fn get_block_header_by_number(
197 &self,
198 block_num: Option<BlockNumber>,
199 include_mmr_proof: bool,
200 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError>;
201
202 /// Given a block number, fetches the block corresponding to that height from the node using
203 /// the `/GetBlockByNumber` RPC endpoint.
204 ///
205 /// If `include_proof` is set to true, the block proof will be included in the response.
206 ///
207 /// The returned block is not verified against the requested `block_num`;
208 /// [`VerifyingRpcClient`] performs that check.
209 async fn get_block_by_number(
210 &self,
211 block_num: BlockNumber,
212 include_proof: bool,
213 ) -> Result<ProvenBlock, RpcError>;
214
215 /// Fetches note-related data for a list of [`NoteId`] using the `/GetNotesById`
216 /// RPC endpoint.
217 ///
218 /// For [`miden_protocol::note::NoteType::Private`] notes, the response includes only the
219 /// [`miden_protocol::note::NoteMetadata`].
220 ///
221 /// For [`miden_protocol::note::NoteType::Public`] notes, the response includes all note details
222 /// (recipient, assets, script, etc.).
223 ///
224 /// In both cases, a [`miden_protocol::note::NoteInclusionProof`] is returned so the caller can
225 /// verify that each note is part of the block's note tree.
226 ///
227 /// Returned notes are not verified to be among the requested `note_ids`;
228 /// [`VerifyingRpcClient`] performs that check.
229 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError>;
230
231 /// Fetches the MMR delta for a given block range using the `/SyncChainMmr` RPC endpoint.
232 ///
233 /// - `current_block_height` is the last block number already present in the caller's MMR.
234 /// - `upper_bound` determines the upper bound of the sync range. Can be a specific block number
235 /// (`BlockNumber`), or a chain tip finality level: `CommittedChainTip` syncs up to the latest
236 /// committed block (the chain tip), while `ProvenChainTip` syncs up to the latest proven
237 /// block which may be behind the committed tip.
238 async fn sync_chain_mmr(
239 &self,
240 current_block_height: BlockNumber,
241 upper_bound: SyncTarget,
242 ) -> Result<ChainMmrInfo, RpcError>;
243
244 /// Fetches the full state of a public account from the node using the `/GetAccount` endpoint,
245 /// and then resolves oversized vault and storage map entries via the `SyncVault` and
246 /// `SyncStorageMap` endpoints when needed.
247 ///
248 /// - `account_id` is the ID of the wanted account.
249 ///
250 /// Returns `Ok(None)` for accounts without public state.
251 async fn get_account_details(
252 &self,
253 account_id: AccountId,
254 ) -> Result<Option<Account>, RpcError> {
255 // Accounts without public state have no full state to fetch; only a commitment is on-chain.
256 if !account_id.is_public() {
257 return Ok(None);
258 }
259
260 // A single request fetches the full public state: every storage map's entries plus the
261 // vault, with the storage layout discovered server-side.
262 let (block_number, mut proof) = self
263 .get_account(
264 account_id,
265 GetAccountRequest::new()
266 .with_storage(StorageMapFetch::All)
267 .with_vault(VaultFetch::Always),
268 )
269 .await?;
270
271 if let Some(details) = proof.details_mut() {
272 self.resolve_oversize_vault(account_id, block_number, details).await?;
273 self.resolve_oversize_storage_maps(account_id, block_number, details).await?;
274 }
275
276 let details = proof.into_details().ok_or(RpcError::ExpectedDataMissing(
277 "public account returned without details".into(),
278 ))?;
279
280 Ok(Some(Account::try_from(&details)?))
281 }
282
283 /// Fetches notes related to the specified tags using the `/SyncNotes` RPC endpoint,
284 /// paginating over the full block range and returning, in block-number order, every block in
285 /// that range that contains at least one note matching the requested tags.
286 ///
287 /// - `block_from`: The starting block number for the range (inclusive).
288 /// - `block_to`: The ending block number for the range (inclusive).
289 /// - `note_tags` is the set of tags used to filter the notes the client is interested in.
290 ///
291 /// Every returned note carries its full metadata, and its attachment content when the response
292 /// carried it. Use [`NodeRpcClient::sync_notes_with_content`] to resolve the rest.
293 ///
294 /// Returned notes are not verified to carry one of the requested `note_tags`;
295 /// [`VerifyingRpcClient`] performs that check.
296 async fn sync_notes(
297 &self,
298 block_from: BlockNumber,
299 block_to: BlockNumber,
300 note_tags: &BTreeSet<NoteTag>,
301 ) -> Result<Vec<SyncNotesBlock>, RpcError>;
302
303 /// Calls [`NodeRpcClient::sync_notes`] for the requested range, then makes a single
304 /// [`NodeRpcClient::get_notes_by_id`] call to resolve the note content the sync response did
305 /// not already carry, according to `fetch`, folding it into each note.
306 ///
307 /// A note whose attachments the sync response already carried needs no request: only notes
308 /// reporting `needs_attachment_fetch` have theirs fetched.
309 ///
310 /// With [`NoteContentFetch::PublicDetailsAndAttachments`], all public notes in the range are
311 /// additionally fetched so the request does not reveal the client's interest set. Narrowing it
312 /// reveals nothing either, since the omissions follow the node's own sync records.
313 ///
314 /// Returns one [`ResolvedSyncNotesBlock`] per matching block, each note carrying its inclusion
315 /// data alongside its content.
316 ///
317 /// A note whose resolved content contradicts its sync record is dropped with a warning rather
318 /// than failing the call, since anyone can commit a note whose content they never publish.
319 async fn sync_notes_with_content(
320 &self,
321 block_from: BlockNumber,
322 block_to: BlockNumber,
323 note_tags: &BTreeSet<NoteTag>,
324 fetch: NoteContentFetch,
325 ) -> Result<Vec<ResolvedSyncNotesBlock>, RpcError> {
326 let blocks = self.sync_notes(block_from, block_to, note_tags).await?;
327 let note_ids: Vec<NoteId> = blocks
328 .iter()
329 .flat_map(|block| block.notes.values())
330 .filter(|note| match fetch {
331 NoteContentFetch::PublicDetailsAndAttachments => {
332 note.note_type() == NoteType::Public || note.needs_attachment_fetch()
333 },
334 NoteContentFetch::AttachmentsOnly => note.needs_attachment_fetch(),
335 })
336 .map(|note| *note.note_id())
337 .collect();
338
339 let mut fetched_content: BTreeMap<NoteId, (Option<NoteDetails>, Option<NoteAttachments>)> =
340 BTreeMap::new();
341 if !note_ids.is_empty() {
342 for fetched_note in self.get_notes_by_id(¬e_ids).await? {
343 let (note_id, details, attachments) = match fetched_note {
344 FetchedNote::Public(note, _) => {
345 let note_id = note.id();
346 let (assets, _, recipient, attachments) = note.into_parts();
347 (note_id, Some(NoteDetails::new(assets, recipient)), attachments)
348 },
349 FetchedNote::Private(note_id, _, attachments, _) => {
350 (note_id, None, attachments)
351 },
352 };
353
354 // An empty set carries nothing, so it is recorded as absent rather than as
355 // content: keeping it would shadow the attachments the note's own sync record may
356 // already have carried, for a public note as much as for a private one.
357 let attachments = (!attachments.is_empty()).then_some(attachments);
358 fetched_content.insert(note_id, (details, attachments));
359 }
360 }
361
362 // Fold the resolved content into each note, keeping the per-block grouping so the
363 // inclusion data (header + MMR path) is carried once per block. `SyncedNote::new` rejects
364 // content that is inconsistent with its sync record (mismatched or missing attachment
365 // content); such notes are dropped rather than failing the sync, since a tracked record
366 // is never stored incomplete this way (it stays expected and can be retried by
367 // re-importing), while a hard error would wedge every sync scanning this block range.
368 let mut synced_blocks = Vec::with_capacity(blocks.len());
369 for block in blocks {
370 let mut notes = BTreeMap::new();
371 for (note_id, committed) in block.notes {
372 // Fetched attachments win when the response actually carried some: a public note's
373 // attachments are bound to the requested id, which `VerifyingRpcClient` checks.
374 // The sync record is the fallback, and a note reporting neither has none.
375 let (details, fetched_attachments) =
376 fetched_content.remove(¬e_id).unwrap_or_default();
377 let attachments = fetched_attachments
378 .or_else(|| committed.attachments().cloned())
379 .unwrap_or_else(NoteAttachments::empty);
380
381 match SyncedNote::new(committed, details, attachments) {
382 Ok(synced_note) => {
383 notes.insert(note_id, synced_note);
384 },
385 Err(err) => {
386 tracing::warn!(%note_id, %err, "skipping synced note with unusable content");
387 },
388 }
389 }
390 synced_blocks.push(ResolvedSyncNotesBlock {
391 block_header: block.block_header,
392 mmr_path: block.mmr_path,
393 notes,
394 });
395 }
396
397 Ok(synced_blocks)
398 }
399
400 /// Fetches the nullifiers corresponding to a list of prefixes using the
401 /// `/SyncNullifiers` RPC endpoint.
402 ///
403 /// - `prefix` is a list of nullifiers prefixes to search for.
404 /// - `block_from`: The starting block number for the range (inclusive).
405 /// - `block_to`: The ending block number for the range (inclusive).
406 ///
407 /// Returned nullifiers are not verified to carry one of the requested prefixes;
408 /// [`VerifyingRpcClient`] performs that check.
409 async fn sync_nullifiers(
410 &self,
411 prefix: &[u16],
412 block_from: BlockNumber,
413 block_to: BlockNumber,
414 ) -> Result<Vec<NullifierUpdate>, RpcError>;
415
416 /// Fetches the account from the node, using the `/GetAccount` endpoint.
417 ///
418 /// The response carries an
419 /// [`AccountWitness`](miden_protocol::block::account_tree::AccountWitness) and the target
420 /// block. Public accounts additionally get [`AccountDetails`]; for private accounts the
421 /// other `request` fields are ignored.
422 ///
423 /// For a fully oversize-resolved account, use [`NodeRpcClient::get_account_details`].
424 ///
425 /// The response block number is not verified against the requested one;
426 /// [`VerifyingRpcClient`] performs that check.
427 ///
428 /// # Errors
429 ///
430 /// - If the account isn't found in the network
431 async fn get_account(
432 &self,
433 account_id: AccountId,
434 request: GetAccountRequest,
435 ) -> Result<(BlockNumber, AccountProof), RpcError>;
436
437 /// Fills in the asset list when the vault came back flagged `too_many_assets`, by
438 /// querying [`NodeRpcClient::sync_account_vault`] over `[GENESIS, block_to]`. No-op when
439 /// the flag isn't set.
440 async fn resolve_oversize_vault(
441 &self,
442 account_id: AccountId,
443 block_to: BlockNumber,
444 details: &mut AccountDetails,
445 ) -> Result<(), RpcError> {
446 if !details.vault_details.too_many_assets {
447 return Ok(());
448 }
449 let vault_info =
450 self.sync_account_vault(BlockNumber::GENESIS, block_to, account_id).await?;
451 // Syncing from genesis merges the full vault history into an absolute patch, so its
452 // updated (non-removed) assets are the account's current vault contents.
453 details.vault_details.assets = vault_info.vault_patch.updated_assets().collect();
454 details.vault_details.too_many_assets = false;
455 Ok(())
456 }
457
458 /// Fills in the entries of any storage map the node reported as oversize, by querying
459 /// [`NodeRpcClient::sync_storage_maps`] over `[GENESIS, block_to]`. No-op when no map is
460 /// oversize.
461 async fn resolve_oversize_storage_maps(
462 &self,
463 account_id: AccountId,
464 block_to: BlockNumber,
465 details: &mut AccountDetails,
466 ) -> Result<(), RpcError> {
467 if !details
468 .storage_details
469 .map_details
470 .iter()
471 .any(AccountStorageMapDetails::is_limit_exceeded)
472 {
473 return Ok(());
474 }
475 let info = self.sync_storage_maps(BlockNumber::GENESIS, block_to, account_id).await?;
476 for map_details in &mut details.storage_details.map_details {
477 if !map_details.is_limit_exceeded() {
478 continue;
479 }
480 // Syncing from genesis merges the full history of each slot into its absolute
481 // current entries, so the result is the complete map content.
482 let entries: Vec<StorageMapEntry> = info
483 .map_entries
484 .get(&map_details.slot_name)
485 .map(|entries| {
486 entries
487 .as_map()
488 .iter()
489 .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
490 .collect()
491 })
492 .unwrap_or_default();
493 map_details.entries = StorageMapEntries::AllEntries(entries);
494 }
495 Ok(())
496 }
497
498 /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
499 /// then `None` is returned.
500 /// The `block_num` parameter is the block number to start the search from (inclusive).
501 ///
502 /// The default implementation of this method makes two RPC requests: one to
503 /// [`NodeRpcClient::get_block_header_by_number`] to resolve the chain tip, and one to
504 /// [`NodeRpcClient::sync_nullifiers`] to search up to that tip.
505 async fn get_nullifier_commit_heights(
506 &self,
507 requested_nullifiers: BTreeSet<Nullifier>,
508 block_from: BlockNumber,
509 ) -> Result<BTreeMap<Nullifier, Option<BlockNumber>>, RpcError> {
510 let prefixes: Vec<u16> =
511 requested_nullifiers.iter().map(crate::note::Nullifier::prefix).collect();
512 let (chain_tip, _) = self.get_block_header_by_number(None, false).await?;
513 let retrieved_nullifiers =
514 self.sync_nullifiers(&prefixes, block_from, chain_tip.block_num()).await?;
515
516 let mut nullifiers_height = BTreeMap::new();
517 for nullifier in requested_nullifiers {
518 if let Some(update) =
519 retrieved_nullifiers.iter().find(|update| update.nullifier == nullifier)
520 {
521 nullifiers_height.insert(nullifier, Some(update.block_num));
522 } else {
523 nullifiers_height.insert(nullifier, None);
524 }
525 }
526
527 Ok(nullifiers_height)
528 }
529
530 /// Fetches public note-related data for a list of [`NoteId`] and builds [`InputNoteRecord`]s
531 /// with it. If a note is not found or it's private, it is ignored and will not be included
532 /// in the returned list.
533 ///
534 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
535 async fn get_public_note_records(
536 &self,
537 note_ids: &[NoteId],
538 current_timestamp: Option<u64>,
539 ) -> Result<Vec<InputNoteRecord>, RpcError> {
540 if note_ids.is_empty() {
541 return Ok(vec![]);
542 }
543
544 let mut public_notes = Vec::with_capacity(note_ids.len());
545 let note_details = self.get_notes_by_id(note_ids).await?;
546
547 for detail in note_details {
548 if let FetchedNote::Public(note, inclusion_proof) = detail {
549 let state = UnverifiedNoteState {
550 metadata: *note.metadata(),
551 inclusion_proof,
552 }
553 .into();
554 let attachments = note.attachments().clone();
555 let note = InputNoteRecord::new(note.into(), attachments, current_timestamp, state);
556
557 public_notes.push(note);
558 }
559 }
560
561 Ok(public_notes)
562 }
563
564 /// Given a block number, fetches the block header corresponding to that height from the node
565 /// along with the MMR proof.
566 ///
567 /// The default implementation of this method uses
568 /// [`NodeRpcClient::get_block_header_by_number`].
569 async fn get_block_header_with_proof(
570 &self,
571 block_num: BlockNumber,
572 ) -> Result<(BlockHeader, MmrProof), RpcError> {
573 let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
574 Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
575 }
576
577 /// Fetches the note with the specified ID.
578 ///
579 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
580 ///
581 /// Errors:
582 /// - [`RpcError::NoteNotFound`] if the note with the specified ID is not found.
583 async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
584 let notes = self.get_notes_by_id(&[note_id]).await?;
585 notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
586 }
587
588 /// Fetches the note script with the specified root, returning `None` if the node has no script
589 /// registered for that root.
590 ///
591 /// A returned script's root is not verified to match the requested `root`;
592 /// [`VerifyingRpcClient`] performs that check.
593 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError>;
594
595 /// Fetches storage map updates for specified account and storage slots within a block range,
596 /// using the `/SyncStorageMaps` RPC endpoint.
597 ///
598 /// - `block_from`: The starting block number for the range (inclusive).
599 /// - `block_to`: The ending block number for the range (inclusive). The node rejects values
600 /// greater than the chain tip.
601 /// - `account_id`: The account ID for which to fetch storage map updates.
602 async fn sync_storage_maps(
603 &self,
604 block_from: BlockNumber,
605 block_to: BlockNumber,
606 account_id: AccountId,
607 ) -> Result<StorageMapInfo, RpcError>;
608
609 /// Fetches account vault updates for specified account within a block range,
610 /// using the `/SyncAccountVault` RPC endpoint.
611 ///
612 /// - `block_from`: The starting block number for the range (inclusive).
613 /// - `block_to`: The ending block number for the range (inclusive). The node rejects values
614 /// greater than the chain tip.
615 /// - `account_id`: The account ID for which to fetch storage map updates.
616 async fn sync_account_vault(
617 &self,
618 block_from: BlockNumber,
619 block_to: BlockNumber,
620 account_id: AccountId,
621 ) -> Result<AccountVaultInfo, RpcError>;
622
623 /// Fetches transaction records for specific accounts within a block range using the
624 /// `/SyncTransactions` RPC endpoint.
625 ///
626 /// - `block_from`: The starting block number for the range (inclusive).
627 /// - `block_to`: The ending block number for the range (inclusive).
628 /// - `account_ids`: The account IDs for which to fetch transactions.
629 async fn sync_transactions(
630 &self,
631 block_from: BlockNumber,
632 block_to: BlockNumber,
633 account_ids: Vec<AccountId>,
634 ) -> Result<Vec<TransactionRecord>, RpcError>;
635
636 /// Fetches the network ID of the node.
637 /// Errors:
638 /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
639 async fn get_network_id(&self) -> Result<NetworkId, RpcError>;
640
641 /// Fetches the RPC limits configured on the node.
642 ///
643 /// Implementations may cache the result internally to avoid repeated network calls.
644 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError>;
645
646 /// Returns the RPC limits if they have been set, without fetching from the node.
647 fn has_rpc_limits(&self) -> Option<RpcLimits>;
648
649 /// Sets the RPC limits internally to be used by the client.
650 async fn set_rpc_limits(&self, limits: RpcLimits);
651
652 /// Fetches the RPC status without requiring Accept header validation.
653 ///
654 /// This is useful for diagnostics when version negotiation fails, as it allows
655 /// retrieving node information even when there's a version mismatch.
656 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError>;
657
658 /// Fetches the status of a specific network note ID.
659 ///
660 /// This is useful for debugging when a network note fails.
661 async fn get_network_note_status(
662 &self,
663 note_id: NoteId,
664 ) -> Result<NetworkNoteStatusInfo, RpcError>;
665}
666
667/// Selects which note content [`NodeRpcClient::sync_notes_with_content`] resolves via
668/// `GetNotesById` after syncing note inclusions.
669///
670/// This enables the possibility of optimizing the call by not requesting more data than needed.
671/// For example, when a public note's details are already known (but not the attachments),
672/// `AttachmentsOnly` can be used. One example of this is when importing notes through
673/// `NoteDetails`.
674///
675/// Neither policy requests attachment content the sync response already carried in full, so a note
676/// whose attachments all fit in a single word is never fetched for its attachments alone.
677#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678pub enum NoteContentFetch {
679 /// Fetch the full body of every public note in the range, plus any attachment content still
680 /// missing.
681 PublicDetailsAndAttachments,
682 /// Fetch only the attachment content still missing.
683 AttachmentsOnly,
684}
685
686// RPC API ENDPOINT
687// ================================================================================================
688//
689/// RPC methods for the Miden protocol.
690#[derive(Debug, Clone, Copy)]
691pub enum RpcEndpoint {
692 Status,
693 SyncNullifiers,
694 GetAccount,
695 GetBlockByNumber,
696 GetBlockHeaderByNumber,
697 GetNotesById,
698 SyncChainMmr,
699 SubmitProvenTx,
700 SubmitProvenBatch,
701 SyncNotes,
702 GetNoteScriptByRoot,
703 SyncStorageMaps,
704 SyncAccountVault,
705 SyncTransactions,
706 GetLimits,
707 GetNetworkNoteStatus,
708 GetTransactionEncryptionKey,
709}
710
711impl RpcEndpoint {
712 /// Returns the endpoint name as used in the RPC service definition.
713 pub fn proto_name(&self) -> &'static str {
714 match self {
715 RpcEndpoint::Status => "Status",
716 RpcEndpoint::SyncNullifiers => "SyncNullifiers",
717 RpcEndpoint::GetAccount => "GetAccount",
718 RpcEndpoint::GetBlockByNumber => "GetBlockByNumber",
719 RpcEndpoint::GetBlockHeaderByNumber => "GetBlockHeaderByNumber",
720 RpcEndpoint::GetNotesById => "GetNotesById",
721 RpcEndpoint::SyncChainMmr => "SyncChainMmr",
722 RpcEndpoint::GetTransactionEncryptionKey => "GetTransactionEncryptionKey",
723 RpcEndpoint::SubmitProvenTx => "SubmitProvenTransaction",
724 RpcEndpoint::SubmitProvenBatch => "SubmitProvenBatch",
725 RpcEndpoint::SyncNotes => "SyncNotes",
726 RpcEndpoint::GetNoteScriptByRoot => "GetNoteScriptByRoot",
727 RpcEndpoint::SyncStorageMaps => "SyncStorageMaps",
728 RpcEndpoint::SyncAccountVault => "SyncAccountVault",
729 RpcEndpoint::SyncTransactions => "SyncTransactions",
730 RpcEndpoint::GetLimits => "GetLimits",
731 RpcEndpoint::GetNetworkNoteStatus => "GetNetworkNoteStatus",
732 }
733 }
734
735 /// Returns whether repeating the call is safe when the outcome of the previous attempt is
736 /// unknown.
737 ///
738 /// Submissions are not: the node may have accepted the transaction before the response was
739 /// lost, so a repeat hits already-consumed state and comes back as a conflict that cannot be
740 /// told apart from a genuine double spend.
741 ///
742 /// The match is exhaustive on purpose, so a new endpoint has to be classified before it
743 /// compiles.
744 #[cfg(feature = "tonic")]
745 pub(crate) fn is_idempotent(self) -> bool {
746 match self {
747 RpcEndpoint::SubmitProvenTx | RpcEndpoint::SubmitProvenBatch => false,
748 RpcEndpoint::Status
749 | RpcEndpoint::SyncNullifiers
750 | RpcEndpoint::GetAccount
751 | RpcEndpoint::GetBlockByNumber
752 | RpcEndpoint::GetBlockHeaderByNumber
753 | RpcEndpoint::GetNotesById
754 | RpcEndpoint::SyncChainMmr
755 | RpcEndpoint::SyncNotes
756 | RpcEndpoint::GetNoteScriptByRoot
757 | RpcEndpoint::SyncStorageMaps
758 | RpcEndpoint::SyncAccountVault
759 | RpcEndpoint::SyncTransactions
760 | RpcEndpoint::GetLimits
761 | RpcEndpoint::GetNetworkNoteStatus
762 | RpcEndpoint::GetTransactionEncryptionKey => true,
763 }
764 }
765}
766
767impl fmt::Display for RpcEndpoint {
768 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
769 match self {
770 RpcEndpoint::Status => write!(f, "status"),
771 RpcEndpoint::SyncNullifiers => {
772 write!(f, "sync_nullifiers")
773 },
774 RpcEndpoint::GetAccount => write!(f, "get_account"),
775 RpcEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
776 RpcEndpoint::GetBlockHeaderByNumber => {
777 write!(f, "get_block_header_by_number")
778 },
779 RpcEndpoint::GetNotesById => write!(f, "get_notes_by_id"),
780 RpcEndpoint::SyncChainMmr => write!(f, "sync_chain_mmr"),
781 RpcEndpoint::GetTransactionEncryptionKey => {
782 write!(f, "get_transaction_encryption_key")
783 },
784 RpcEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
785 RpcEndpoint::SubmitProvenBatch => write!(f, "submit_proven_batch"),
786 RpcEndpoint::SyncNotes => write!(f, "sync_notes"),
787 RpcEndpoint::GetNoteScriptByRoot => write!(f, "get_note_script_by_root"),
788 RpcEndpoint::SyncStorageMaps => write!(f, "sync_storage_maps"),
789 RpcEndpoint::SyncAccountVault => write!(f, "sync_account_vault"),
790 RpcEndpoint::SyncTransactions => write!(f, "sync_transactions"),
791 RpcEndpoint::GetLimits => write!(f, "get_limits"),
792 RpcEndpoint::GetNetworkNoteStatus => write!(f, "get_network_note_status"),
793 }
794 }
795}