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