Skip to main content

miden_client/rpc/
mod.rs

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