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