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//! - Retrieve block headers (optionally with MMR proofs).
9//! - Sync state updates (including notes, nullifiers, and account updates).
10//! - Fetch details for specific notes and accounts.
11//!
12//! The client implementation adapts to the target environment automatically:
13//! - Native targets use `tonic` transport with TLS.
14//! - `wasm32` targets use `tonic-web-wasm-client` transport.
15//!
16//! ## Example
17//!
18//! ```no_run
19//! # use miden_client::rpc::{Endpoint, NodeRpcClient, GrpcClient};
20//! # use miden_protocol::block::BlockNumber;
21//! # #[tokio::main]
22//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! // Create a gRPC client instance (assumes default endpoint configuration).
24//! let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
25//! let mut rpc_client = GrpcClient::new(&endpoint, 1000);
26//!
27//! // Fetch the latest block header (by passing None).
28//! let (block_header, mmr_proof) = rpc_client.get_block_header_by_number(None, true).await?;
29//!
30//! println!("Latest block number: {}", block_header.block_num());
31//! if let Some(proof) = mmr_proof {
32//!     println!("MMR proof received accordingly");
33//! }
34//!
35//! #    Ok(())
36//! # }
37//! ```
38//! The client also makes use of this component in order to communicate with the node.
39//!
40//! For further details and examples, see the documentation for the individual methods in the
41//! [`NodeRpcClient`] trait.
42
43use alloc::boxed::Box;
44use alloc::collections::{BTreeMap, BTreeSet};
45use alloc::string::String;
46use alloc::vec::Vec;
47use core::fmt;
48
49use domain::account::{AccountProof, FetchedAccount};
50use domain::note::{FetchedNote, NoteSyncInfo, SyncNotesResult};
51use domain::nullifier::NullifierUpdate;
52use domain::sync::ChainMmrInfo;
53use miden_protocol::Word;
54use miden_protocol::account::{AccountCode, AccountId};
55use miden_protocol::address::NetworkId;
56use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
57use miden_protocol::crypto::merkle::mmr::MmrProof;
58use miden_protocol::crypto::merkle::smt::SmtProof;
59use miden_protocol::note::{NoteId, NoteScript, NoteTag, NoteType, Nullifier};
60use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};
61
62use crate::rpc::domain::storage_map::StorageMapInfo;
63
64/// Contains domain types related to RPC requests and responses, as well as utility functions
65/// for dealing with them.
66pub mod domain;
67
68mod errors;
69pub use errors::*;
70
71mod endpoint;
72pub(crate) use domain::limits::RPC_LIMITS_STORE_SETTING;
73pub use domain::limits::RpcLimits;
74pub use domain::status::RpcStatusInfo;
75pub use endpoint::Endpoint;
76
77#[cfg(not(feature = "testing"))]
78mod generated;
79#[cfg(feature = "testing")]
80pub mod generated;
81
82#[cfg(feature = "tonic")]
83mod tonic_client;
84#[cfg(feature = "tonic")]
85pub use tonic_client::GrpcClient;
86
87use crate::rpc::domain::account::AccountStorageRequirements;
88use crate::rpc::domain::account_vault::AccountVaultInfo;
89use crate::rpc::domain::transaction::TransactionsInfo;
90use crate::store::InputNoteRecord;
91use crate::store::input_note_states::UnverifiedNoteState;
92
93/// Represents the state that we want to retrieve from the network
94pub enum AccountStateAt {
95    /// Gets the latest state, for the current chain tip
96    ChainTip,
97    /// Gets the state at a specific block number
98    Block(BlockNumber),
99}
100
101// NODE RPC CLIENT TRAIT
102// ================================================================================================
103
104/// Defines the interface for communicating with the Miden node.
105///
106/// The implementers are responsible for connecting to the Miden node, handling endpoint
107/// requests/responses, and translating responses into domain objects relevant for each of the
108/// endpoints.
109#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
110#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
111pub trait NodeRpcClient: Send + Sync {
112    /// Sets the genesis commitment for the client and reconnects to the node providing the
113    /// genesis commitment in the request headers. If the genesis commitment is already set,
114    /// this method does nothing.
115    async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError>;
116
117    /// Returns the genesis commitment if it has been set, without fetching from the node.
118    fn has_genesis_commitment(&self) -> Option<Word>;
119
120    /// Given a Proven Transaction, send it to the node for it to be included in a future block
121    /// using the `/SubmitProvenTransaction` RPC endpoint.
122    async fn submit_proven_transaction(
123        &self,
124        proven_transaction: ProvenTransaction,
125        transaction_inputs: TransactionInputs,
126    ) -> Result<BlockNumber, RpcError>;
127
128    /// Given a block number, fetches the block header corresponding to that height from the node
129    /// using the `/GetBlockHeaderByNumber` endpoint.
130    /// If `include_mmr_proof` is set to true and the function returns an `Ok`, the second value
131    /// of the return tuple should always be Some(MmrProof).
132    ///
133    /// When `None` is provided, returns info regarding the latest block.
134    async fn get_block_header_by_number(
135        &self,
136        block_num: Option<BlockNumber>,
137        include_mmr_proof: bool,
138    ) -> Result<(BlockHeader, Option<MmrProof>), RpcError>;
139
140    /// Given a block number, fetches the block corresponding to that height from the node using
141    /// the `/GetBlockByNumber` RPC endpoint.
142    async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError>;
143
144    /// Fetches note-related data for a list of [`NoteId`] using the `/GetNotesById`
145    /// RPC endpoint.
146    ///
147    /// For [`miden_protocol::note::NoteType::Private`] notes, the response includes only the
148    /// [`miden_protocol::note::NoteMetadata`].
149    ///
150    /// For [`miden_protocol::note::NoteType::Public`] notes, the response includes all note details
151    /// (recipient, assets, script, etc.).
152    ///
153    /// In both cases, a [`miden_protocol::note::NoteInclusionProof`] is returned so the caller can
154    /// verify that each note is part of the block's note tree.
155    async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError>;
156
157    /// Fetches the MMR delta for a given block range using the `/SyncChainMmr` RPC endpoint.
158    ///
159    /// - `block_from` is the last block number already present in the caller's MMR.
160    /// - `block_to` is the optional upper bound of the range. If `None`, syncs up to the chain tip.
161    async fn sync_chain_mmr(
162        &self,
163        block_from: BlockNumber,
164        block_to: Option<BlockNumber>,
165    ) -> Result<ChainMmrInfo, RpcError>;
166
167    /// Fetches the current state of an account from the node using the `/GetAccountDetails` RPC
168    /// endpoint.
169    ///
170    /// - `account_id` is the ID of the wanted account.
171    async fn get_account_details(&self, account_id: AccountId) -> Result<FetchedAccount, RpcError>;
172
173    /// Fetches the notes related to the specified tags using the `/SyncNotes` RPC endpoint.
174    ///
175    /// - `block_num` is the last block number known by the client.
176    /// - `note_tags` is a list of tags used to filter the notes the client is interested in.
177    async fn sync_notes(
178        &self,
179        block_num: BlockNumber,
180        block_to: Option<BlockNumber>,
181        note_tags: &BTreeSet<NoteTag>,
182    ) -> Result<NoteSyncInfo, RpcError>;
183
184    /// Paginates [`NodeRpcClient::sync_notes`] over the full block range, then makes a single
185    /// [`NodeRpcClient::get_notes_by_id`] call to:
186    /// - Fill metadata for notes with attachments (whose sync response only had header fields).
187    /// - Fetch full note bodies for public notes (scripts, assets, recipient).
188    ///
189    /// All notes that are public or have missing metadata are fetched (not just the ones the
190    /// client tracks) to avoid revealing which specific notes the client is interested in.
191    ///
192    /// Returns the chain tip, the fully-resolved note blocks, and the fetched note details.
193    async fn sync_notes_with_details(
194        &self,
195        block_from: BlockNumber,
196        block_to: Option<BlockNumber>,
197        note_tags: &BTreeSet<NoteTag>,
198    ) -> Result<SyncNotesResult, RpcError> {
199        let mut all_blocks = Vec::new();
200        let mut cursor = block_from;
201        let mut chain_tip;
202
203        loop {
204            let note_sync = self.sync_notes(cursor, block_to, note_tags).await?;
205
206            chain_tip = note_sync.chain_tip;
207            cursor = note_sync.block_to + 1;
208            let range_end = block_to.unwrap_or(chain_tip);
209            let done = note_sync.blocks.is_empty() || cursor >= range_end;
210            all_blocks.extend(note_sync.blocks);
211
212            if done {
213                break;
214            }
215        }
216
217        // Single get_notes_by_id call for all notes that are public or missing metadata.
218        let note_ids: Vec<NoteId> = all_blocks
219            .iter()
220            .flat_map(|b| b.notes.values())
221            .filter(|n| n.metadata().is_none() || n.note_type() != NoteType::Private)
222            .map(|n| *n.note_id())
223            .collect();
224
225        let mut public_notes = BTreeMap::new();
226
227        if !note_ids.is_empty() {
228            let fetched = self.get_notes_by_id(&note_ids).await?;
229
230            for fetched_note in fetched {
231                // Fill metadata on committed notes that were missing it.
232                let note_id = fetched_note.id();
233                for block in &mut all_blocks {
234                    if let Some(note) = block.notes.get_mut(&note_id)
235                        && note.metadata().is_none()
236                    {
237                        note.set_metadata(fetched_note.metadata().clone());
238                    }
239                }
240
241                // Collect full note bodies for public notes.
242                if let FetchedNote::Public(note, _) = fetched_note {
243                    public_notes.insert(note.id(), note);
244                }
245            }
246        }
247
248        Ok(SyncNotesResult { blocks: all_blocks, public_notes })
249    }
250
251    /// Fetches the nullifiers corresponding to a list of prefixes using the
252    /// `/SyncNullifiers` RPC endpoint.
253    ///
254    /// - `prefix` is a list of nullifiers prefixes to search for.
255    /// - `block_num` is the block number to start the search from. Nullifiers created in this block
256    ///   or the following blocks will be included.
257    /// - `block_to` is the optional block number to stop the search at. If not provided, syncs up
258    ///   to the network chain tip.
259    async fn sync_nullifiers(
260        &self,
261        prefix: &[u16],
262        block_num: BlockNumber,
263        block_to: Option<BlockNumber>,
264    ) -> Result<Vec<NullifierUpdate>, RpcError>;
265
266    /// Fetches the nullifier proofs corresponding to a list of nullifiers using the
267    /// `/CheckNullifiers` RPC endpoint.
268    async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError>;
269
270    /// Fetches the account proof and optionally its details from the node, using the
271    /// `GetAccountProof` endpoint.
272    ///
273    /// The `account_state` parameter specifies the block number from which to retrieve
274    /// the account proof from (the state of the account at that block).
275    ///
276    /// The `storage_requirements` parameter specifies which storage slots and map keys
277    /// should be included in the response for public accounts.
278    ///
279    /// The `known_account_code` parameter is the known code commitment
280    /// to prevent unnecessary data fetching.
281    ///
282    /// The `known_vault_commitment` parameter controls vault data retrieval:
283    /// - `None`: vault data is not requested.
284    /// - `Some(commitment)`: vault data is returned only if the account's current vault root
285    ///   differs from the provided commitment. Use `EMPTY_WORD` to always fetch.
286    ///
287    /// Returns the block number and the account proof. If the account is not found in
288    /// the node, the method will return an error.
289    async fn get_account_proof(
290        &self,
291        account_id: AccountId,
292        storage_requirements: AccountStorageRequirements,
293        account_state: AccountStateAt,
294        known_account_code: Option<AccountCode>,
295        known_vault_commitment: Option<Word>,
296    ) -> Result<(BlockNumber, AccountProof), RpcError>;
297
298    /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
299    /// then `None` is returned.
300    /// The `block_num` parameter is the block number to start the search from.
301    ///
302    /// The default implementation of this method uses
303    /// [`NodeRpcClient::sync_nullifiers`].
304    async fn get_nullifier_commit_heights(
305        &self,
306        requested_nullifiers: BTreeSet<Nullifier>,
307        block_from: BlockNumber,
308    ) -> Result<BTreeMap<Nullifier, Option<BlockNumber>>, RpcError> {
309        let prefixes: Vec<u16> =
310            requested_nullifiers.iter().map(crate::note::Nullifier::prefix).collect();
311        let retrieved_nullifiers = self.sync_nullifiers(&prefixes, block_from, None).await?;
312
313        let mut nullifiers_height = BTreeMap::new();
314        for nullifier in requested_nullifiers {
315            if let Some(update) =
316                retrieved_nullifiers.iter().find(|update| update.nullifier == nullifier)
317            {
318                nullifiers_height.insert(nullifier, Some(update.block_num));
319            } else {
320                nullifiers_height.insert(nullifier, None);
321            }
322        }
323
324        Ok(nullifiers_height)
325    }
326
327    /// Fetches public note-related data for a list of [`NoteId`] and builds [`InputNoteRecord`]s
328    /// with it. If a note is not found or it's private, it is ignored and will not be included
329    /// in the returned list.
330    ///
331    /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
332    async fn get_public_note_records(
333        &self,
334        note_ids: &[NoteId],
335        current_timestamp: Option<u64>,
336    ) -> Result<Vec<InputNoteRecord>, RpcError> {
337        if note_ids.is_empty() {
338            return Ok(vec![]);
339        }
340
341        let mut public_notes = Vec::with_capacity(note_ids.len());
342        let note_details = self.get_notes_by_id(note_ids).await?;
343
344        for detail in note_details {
345            if let FetchedNote::Public(note, inclusion_proof) = detail {
346                let state = UnverifiedNoteState {
347                    metadata: note.metadata().clone(),
348                    inclusion_proof,
349                }
350                .into();
351                let note = InputNoteRecord::new(note.into(), current_timestamp, state);
352
353                public_notes.push(note);
354            }
355        }
356
357        Ok(public_notes)
358    }
359
360    /// Given a block number, fetches the block header corresponding to that height from the node
361    /// along with the MMR proof.
362    ///
363    /// The default implementation of this method uses
364    /// [`NodeRpcClient::get_block_header_by_number`].
365    async fn get_block_header_with_proof(
366        &self,
367        block_num: BlockNumber,
368    ) -> Result<(BlockHeader, MmrProof), RpcError> {
369        let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
370        Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
371    }
372
373    /// Fetches the note with the specified ID.
374    ///
375    /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
376    ///
377    /// Errors:
378    /// - [`RpcError::NoteNotFound`] if the note with the specified ID is not found.
379    async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
380        let notes = self.get_notes_by_id(&[note_id]).await?;
381        notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
382    }
383
384    /// Fetches the note script with the specified root.
385    ///
386    /// Errors:
387    /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
388    async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError>;
389
390    /// Fetches storage map updates for specified account and storage slots within a block range,
391    /// using the `/SyncStorageMaps` RPC endpoint.
392    ///
393    /// - `block_from`: The starting block number for the range.
394    /// - `block_to`: The ending block number for the range.
395    /// - `account_id`: The account ID for which to fetch storage map updates.
396    async fn sync_storage_maps(
397        &self,
398        block_from: BlockNumber,
399        block_to: Option<BlockNumber>,
400        account_id: AccountId,
401    ) -> Result<StorageMapInfo, RpcError>;
402
403    /// Fetches account vault updates for specified account within a block range,
404    /// using the `/SyncAccountVault` RPC endpoint.
405    ///
406    /// - `block_from`: The starting block number for the range.
407    /// - `block_to`: The ending block number for the range.
408    /// - `account_id`: The account ID for which to fetch storage map updates.
409    async fn sync_account_vault(
410        &self,
411        block_from: BlockNumber,
412        block_to: Option<BlockNumber>,
413        account_id: AccountId,
414    ) -> Result<AccountVaultInfo, RpcError>;
415
416    /// Fetches transactions records for specific accounts within a block range.
417    /// Using the `/SyncTransactions` RPC endpoint.
418    ///
419    /// - `block_from`: The starting block number for the range.
420    /// - `block_to`: The ending block number for the range.
421    /// - `account_ids`: The account IDs for which to fetch storage map updates.
422    async fn sync_transactions(
423        &self,
424        block_from: BlockNumber,
425        block_to: Option<BlockNumber>,
426        account_ids: Vec<AccountId>,
427    ) -> Result<TransactionsInfo, RpcError>;
428
429    /// Fetches the network ID of the node.
430    /// Errors:
431    /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
432    async fn get_network_id(&self) -> Result<NetworkId, RpcError>;
433
434    /// Fetches the RPC limits configured on the node.
435    ///
436    /// Implementations may cache the result internally to avoid repeated network calls.
437    async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError>;
438
439    /// Returns the RPC limits if they have been set, without fetching from the node.
440    fn has_rpc_limits(&self) -> Option<RpcLimits>;
441
442    /// Sets the RPC limits internally to be used by the client.
443    async fn set_rpc_limits(&self, limits: RpcLimits);
444
445    /// Fetches the RPC status without requiring Accept header validation.
446    ///
447    /// This is useful for diagnostics when version negotiation fails, as it allows
448    /// retrieving node information even when there's a version mismatch.
449    async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError>;
450}
451
452// RPC API ENDPOINT
453// ================================================================================================
454//
455/// RPC methods for the Miden protocol.
456#[derive(Debug, Clone, Copy)]
457pub enum RpcEndpoint {
458    Status,
459    CheckNullifiers,
460    SyncNullifiers,
461    GetAccount,
462    GetBlockByNumber,
463    GetBlockHeaderByNumber,
464    GetNotesById,
465    SyncChainMmr,
466    SubmitProvenTx,
467    SyncNotes,
468    GetNoteScriptByRoot,
469    SyncStorageMaps,
470    SyncAccountVault,
471    SyncTransactions,
472    GetLimits,
473}
474
475impl RpcEndpoint {
476    /// Returns the endpoint name as used in the RPC service definition.
477    pub fn proto_name(&self) -> &'static str {
478        match self {
479            RpcEndpoint::Status => "Status",
480            RpcEndpoint::CheckNullifiers => "CheckNullifiers",
481            RpcEndpoint::SyncNullifiers => "SyncNullifiers",
482            RpcEndpoint::GetAccount => "GetAccount",
483            RpcEndpoint::GetBlockByNumber => "GetBlockByNumber",
484            RpcEndpoint::GetBlockHeaderByNumber => "GetBlockHeaderByNumber",
485            RpcEndpoint::GetNotesById => "GetNotesById",
486            RpcEndpoint::SyncChainMmr => "SyncChainMmr",
487            RpcEndpoint::SubmitProvenTx => "SubmitProvenTransaction",
488            RpcEndpoint::SyncNotes => "SyncNotes",
489            RpcEndpoint::GetNoteScriptByRoot => "GetNoteScriptByRoot",
490            RpcEndpoint::SyncStorageMaps => "SyncStorageMaps",
491            RpcEndpoint::SyncAccountVault => "SyncAccountVault",
492            RpcEndpoint::SyncTransactions => "SyncTransactions",
493            RpcEndpoint::GetLimits => "GetLimits",
494        }
495    }
496}
497
498impl fmt::Display for RpcEndpoint {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        match self {
501            RpcEndpoint::Status => write!(f, "status"),
502            RpcEndpoint::CheckNullifiers => write!(f, "check_nullifiers"),
503            RpcEndpoint::SyncNullifiers => {
504                write!(f, "sync_nullifiers")
505            },
506            RpcEndpoint::GetAccount => write!(f, "get_account_proof"),
507            RpcEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
508            RpcEndpoint::GetBlockHeaderByNumber => {
509                write!(f, "get_block_header_by_number")
510            },
511            RpcEndpoint::GetNotesById => write!(f, "get_notes_by_id"),
512            RpcEndpoint::SyncChainMmr => write!(f, "sync_chain_mmr"),
513            RpcEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
514            RpcEndpoint::SyncNotes => write!(f, "sync_notes"),
515            RpcEndpoint::GetNoteScriptByRoot => write!(f, "get_note_script_by_root"),
516            RpcEndpoint::SyncStorageMaps => write!(f, "sync_storage_maps"),
517            RpcEndpoint::SyncAccountVault => write!(f, "sync_account_vault"),
518            RpcEndpoint::SyncTransactions => write!(f, "sync_transactions"),
519            RpcEndpoint::GetLimits => write!(f, "get_limits"),
520        }
521    }
522}