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    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(&note_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(&note_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}