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};
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, 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 /// Fetches the nullifiers corresponding to a list of prefixes using the
184 /// `/SyncNullifiers` RPC endpoint.
185 ///
186 /// - `prefix` is a list of nullifiers prefixes to search for.
187 /// - `block_num` is the block number to start the search from. Nullifiers created in this block
188 /// or the following blocks will be included.
189 /// - `block_to` is the optional block number to stop the search at. If not provided, syncs up
190 /// to the network chain tip.
191 async fn sync_nullifiers(
192 &self,
193 prefix: &[u16],
194 block_num: BlockNumber,
195 block_to: Option<BlockNumber>,
196 ) -> Result<Vec<NullifierUpdate>, RpcError>;
197
198 /// Fetches the nullifier proofs corresponding to a list of nullifiers using the
199 /// `/CheckNullifiers` RPC endpoint.
200 async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError>;
201
202 /// Fetches the account proof and optionally its details from the node, using the
203 /// `GetAccountProof` endpoint.
204 ///
205 /// The `account_state` parameter specifies the block number from which to retrieve
206 /// the account proof from (the state of the account at that block).
207 ///
208 /// The `storage_requirements` parameter specifies which storage slots and map keys
209 /// should be included in the response for public accounts.
210 ///
211 /// The `known_account_code` parameter is the known code commitment
212 /// to prevent unnecessary data fetching. Returns the block number and the account proof. If
213 /// the account is not found in the node, the method will return an error.
214 async fn get_account_proof(
215 &self,
216 account_id: AccountId,
217 storage_requirements: AccountStorageRequirements,
218 account_state: AccountStateAt,
219 known_account_code: Option<AccountCode>,
220 ) -> Result<(BlockNumber, AccountProof), RpcError>;
221
222 /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
223 /// then `None` is returned.
224 /// The `block_num` parameter is the block number to start the search from.
225 ///
226 /// The default implementation of this method uses
227 /// [`NodeRpcClient::sync_nullifiers`].
228 async fn get_nullifier_commit_heights(
229 &self,
230 requested_nullifiers: BTreeSet<Nullifier>,
231 block_from: BlockNumber,
232 ) -> Result<BTreeMap<Nullifier, Option<BlockNumber>>, RpcError> {
233 let prefixes: Vec<u16> =
234 requested_nullifiers.iter().map(crate::note::Nullifier::prefix).collect();
235 let retrieved_nullifiers = self.sync_nullifiers(&prefixes, block_from, None).await?;
236
237 let mut nullifiers_height = BTreeMap::new();
238 for nullifier in requested_nullifiers {
239 if let Some(update) =
240 retrieved_nullifiers.iter().find(|update| update.nullifier == nullifier)
241 {
242 nullifiers_height.insert(nullifier, Some(update.block_num));
243 } else {
244 nullifiers_height.insert(nullifier, None);
245 }
246 }
247
248 Ok(nullifiers_height)
249 }
250
251 /// Fetches public note-related data for a list of [`NoteId`] and builds [`InputNoteRecord`]s
252 /// with it. If a note is not found or it's private, it is ignored and will not be included
253 /// in the returned list.
254 ///
255 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
256 async fn get_public_note_records(
257 &self,
258 note_ids: &[NoteId],
259 current_timestamp: Option<u64>,
260 ) -> Result<Vec<InputNoteRecord>, RpcError> {
261 if note_ids.is_empty() {
262 return Ok(vec![]);
263 }
264
265 let mut public_notes = Vec::with_capacity(note_ids.len());
266 let note_details = self.get_notes_by_id(note_ids).await?;
267
268 for detail in note_details {
269 if let FetchedNote::Public(note, inclusion_proof) = detail {
270 let state = UnverifiedNoteState {
271 metadata: note.metadata().clone(),
272 inclusion_proof,
273 }
274 .into();
275 let note = InputNoteRecord::new(note.into(), current_timestamp, state);
276
277 public_notes.push(note);
278 }
279 }
280
281 Ok(public_notes)
282 }
283
284 /// Fetches the public accounts that have been updated since the last known state of the
285 /// accounts.
286 ///
287 /// The `local_accounts` parameter is a list of account headers that the client has
288 /// stored locally and that it wants to check for updates. If an account is private or didn't
289 /// change, it is ignored and will not be included in the returned list.
290 /// The default implementation of this method uses [`NodeRpcClient::get_account_details`].
291 async fn get_updated_public_accounts(
292 &self,
293 local_accounts: &[&AccountHeader],
294 ) -> Result<Vec<Account>, RpcError> {
295 let mut public_accounts = vec![];
296
297 for local_account in local_accounts {
298 let response = self.get_account_details(local_account.id()).await?;
299
300 if let FetchedAccount::Public(account, _) = response {
301 let account = *account;
302 // We should only return an account if it's newer, otherwise we ignore it
303 if account.nonce().as_int() > local_account.nonce().as_int() {
304 public_accounts.push(account);
305 }
306 }
307 }
308
309 Ok(public_accounts)
310 }
311
312 /// Given a block number, fetches the block header corresponding to that height from the node
313 /// along with the MMR proof.
314 ///
315 /// The default implementation of this method uses
316 /// [`NodeRpcClient::get_block_header_by_number`].
317 async fn get_block_header_with_proof(
318 &self,
319 block_num: BlockNumber,
320 ) -> Result<(BlockHeader, MmrProof), RpcError> {
321 let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
322 Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
323 }
324
325 /// Fetches the note with the specified ID.
326 ///
327 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
328 ///
329 /// Errors:
330 /// - [`RpcError::NoteNotFound`] if the note with the specified ID is not found.
331 async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
332 let notes = self.get_notes_by_id(&[note_id]).await?;
333 notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
334 }
335
336 /// Fetches the note script with the specified root.
337 ///
338 /// Errors:
339 /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
340 async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError>;
341
342 /// Fetches storage map updates for specified account and storage slots within a block range,
343 /// using the `/SyncStorageMaps` RPC endpoint.
344 ///
345 /// - `block_from`: The starting block number for the range.
346 /// - `block_to`: The ending block number for the range.
347 /// - `account_id`: The account ID for which to fetch storage map updates.
348 async fn sync_storage_maps(
349 &self,
350 block_from: BlockNumber,
351 block_to: Option<BlockNumber>,
352 account_id: AccountId,
353 ) -> Result<StorageMapInfo, RpcError>;
354
355 /// Fetches account vault updates for specified account within a block range,
356 /// using the `/SyncAccountVault` RPC endpoint.
357 ///
358 /// - `block_from`: The starting block number for the range.
359 /// - `block_to`: The ending block number for the range.
360 /// - `account_id`: The account ID for which to fetch storage map updates.
361 async fn sync_account_vault(
362 &self,
363 block_from: BlockNumber,
364 block_to: Option<BlockNumber>,
365 account_id: AccountId,
366 ) -> Result<AccountVaultInfo, RpcError>;
367
368 /// Fetches transactions records for specific accounts within a block range.
369 /// Using the `/SyncTransactions` RPC endpoint.
370 ///
371 /// - `block_from`: The starting block number for the range.
372 /// - `block_to`: The ending block number for the range.
373 /// - `account_ids`: The account IDs for which to fetch storage map updates.
374 async fn sync_transactions(
375 &self,
376 block_from: BlockNumber,
377 block_to: Option<BlockNumber>,
378 account_ids: Vec<AccountId>,
379 ) -> Result<TransactionsInfo, RpcError>;
380
381 /// Fetches the network ID of the node.
382 /// Errors:
383 /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
384 async fn get_network_id(&self) -> Result<NetworkId, RpcError>;
385
386 /// Fetches the RPC limits configured on the node.
387 ///
388 /// Implementations may cache the result internally to avoid repeated network calls.
389 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError>;
390
391 /// Returns the RPC limits if they have been set, without fetching from the node.
392 fn has_rpc_limits(&self) -> Option<RpcLimits>;
393
394 /// Sets the RPC limits internally to be used by the client.
395 async fn set_rpc_limits(&self, limits: RpcLimits);
396
397 /// Fetches the RPC status without requiring Accept header validation.
398 ///
399 /// This is useful for diagnostics when version negotiation fails, as it allows
400 /// retrieving node information even when there's a version mismatch.
401 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError>;
402}
403
404// RPC API ENDPOINT
405// ================================================================================================
406//
407/// RPC methods for the Miden protocol.
408#[derive(Debug, Clone, Copy)]
409pub enum RpcEndpoint {
410 Status,
411 CheckNullifiers,
412 SyncNullifiers,
413 GetAccount,
414 GetBlockByNumber,
415 GetBlockHeaderByNumber,
416 GetNotesById,
417 SyncChainMmr,
418 SubmitProvenTx,
419 SyncNotes,
420 GetNoteScriptByRoot,
421 SyncStorageMaps,
422 SyncAccountVault,
423 SyncTransactions,
424 GetLimits,
425}
426
427impl RpcEndpoint {
428 /// Returns the endpoint name as used in the RPC service definition.
429 pub fn proto_name(&self) -> &'static str {
430 match self {
431 RpcEndpoint::Status => "Status",
432 RpcEndpoint::CheckNullifiers => "CheckNullifiers",
433 RpcEndpoint::SyncNullifiers => "SyncNullifiers",
434 RpcEndpoint::GetAccount => "GetAccount",
435 RpcEndpoint::GetBlockByNumber => "GetBlockByNumber",
436 RpcEndpoint::GetBlockHeaderByNumber => "GetBlockHeaderByNumber",
437 RpcEndpoint::GetNotesById => "GetNotesById",
438 RpcEndpoint::SyncChainMmr => "SyncChainMmr",
439 RpcEndpoint::SubmitProvenTx => "SubmitProvenTransaction",
440 RpcEndpoint::SyncNotes => "SyncNotes",
441 RpcEndpoint::GetNoteScriptByRoot => "GetNoteScriptByRoot",
442 RpcEndpoint::SyncStorageMaps => "SyncStorageMaps",
443 RpcEndpoint::SyncAccountVault => "SyncAccountVault",
444 RpcEndpoint::SyncTransactions => "SyncTransactions",
445 RpcEndpoint::GetLimits => "GetLimits",
446 }
447 }
448}
449
450impl fmt::Display for RpcEndpoint {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 match self {
453 RpcEndpoint::Status => write!(f, "status"),
454 RpcEndpoint::CheckNullifiers => write!(f, "check_nullifiers"),
455 RpcEndpoint::SyncNullifiers => {
456 write!(f, "sync_nullifiers")
457 },
458 RpcEndpoint::GetAccount => write!(f, "get_account_proof"),
459 RpcEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
460 RpcEndpoint::GetBlockHeaderByNumber => {
461 write!(f, "get_block_header_by_number")
462 },
463 RpcEndpoint::GetNotesById => write!(f, "get_notes_by_id"),
464 RpcEndpoint::SyncChainMmr => write!(f, "sync_chain_mmr"),
465 RpcEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
466 RpcEndpoint::SyncNotes => write!(f, "sync_notes"),
467 RpcEndpoint::GetNoteScriptByRoot => write!(f, "get_note_script_by_root"),
468 RpcEndpoint::SyncStorageMaps => write!(f, "sync_storage_maps"),
469 RpcEndpoint::SyncAccountVault => write!(f, "sync_account_vault"),
470 RpcEndpoint::SyncTransactions => write!(f, "sync_transactions"),
471 RpcEndpoint::GetLimits => write!(f, "get_limits"),
472 }
473 }
474}