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