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(¬e_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(¬e_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}