Skip to main content

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