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_objects::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::{AccountProofs, FetchedAccount};
50use domain::note::{FetchedNote, NoteSyncInfo};
51use domain::nullifier::NullifierUpdate;
52use domain::sync::StateSyncInfo;
53use miden_objects::Word;
54use miden_objects::account::{Account, AccountCode, AccountHeader, AccountId};
55use miden_objects::address::NetworkId;
56use miden_objects::block::{BlockHeader, BlockNumber, ProvenBlock};
57use miden_objects::crypto::merkle::{MmrProof, SmtProof};
58use miden_objects::note::{NoteId, NoteScript, NoteTag, Nullifier};
59use miden_objects::transaction::{ProvenTransaction, TransactionInputs};
60
61/// Contains domain types related to RPC requests and responses, as well as utility functions
62/// for dealing with them.
63pub mod domain;
64
65mod errors;
66pub use errors::*;
67
68mod endpoint;
69pub use endpoint::Endpoint;
70
71#[cfg(not(feature = "testing"))]
72mod generated;
73#[cfg(feature = "testing")]
74pub mod generated;
75
76#[cfg(feature = "tonic")]
77mod tonic_client;
78#[cfg(feature = "tonic")]
79pub use tonic_client::GrpcClient;
80
81use crate::rpc::domain::account_vault::AccountVaultInfo;
82use crate::rpc::domain::storage_map::StorageMapInfo;
83use crate::rpc::domain::transaction::TransactionsInfo;
84use crate::store::InputNoteRecord;
85use crate::store::input_note_states::UnverifiedNoteState;
86use crate::transaction::ForeignAccount;
87
88// RPC ENDPOINT LIMITS
89// ================================================================================================
90
91// TODO: We need a better structured way of getting limits as defined by the node (#1139)
92pub const NOTE_IDS_LIMIT: usize = 100;
93pub const NULLIFIER_PREFIXES_LIMIT: usize = 100;
94
95// NODE RPC CLIENT TRAIT
96// ================================================================================================
97
98/// Defines the interface for communicating with the Miden node.
99///
100/// The implementers are responsible for connecting to the Miden node, handling endpoint
101/// requests/responses, and translating responses into domain objects relevant for each of the
102/// endpoints.
103#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
104#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
105pub trait NodeRpcClient: Send + Sync {
106 /// Sets the genesis commitment for the client and reconnects to the node providing the
107 /// genesis commitment in the request headers. If the genesis commitment is already set,
108 /// this method does nothing.
109 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError>;
110
111 /// Given a Proven Transaction, send it to the node for it to be included in a future block
112 /// using the `/SubmitProvenTransaction` RPC endpoint.
113 async fn submit_proven_transaction(
114 &self,
115 proven_transaction: ProvenTransaction,
116 transaction_inputs: TransactionInputs,
117 ) -> Result<BlockNumber, RpcError>;
118
119 /// Given a block number, fetches the block header corresponding to that height from the node
120 /// using the `/GetBlockHeaderByNumber` endpoint.
121 /// If `include_mmr_proof` is set to true and the function returns an `Ok`, the second value
122 /// of the return tuple should always be Some(MmrProof).
123 ///
124 /// When `None` is provided, returns info regarding the latest block.
125 async fn get_block_header_by_number(
126 &self,
127 block_num: Option<BlockNumber>,
128 include_mmr_proof: bool,
129 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError>;
130
131 /// Given a block number, fetches the block corresponding to that height from the node using
132 /// the `/GetBlockByNumber` RPC endpoint.
133 async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError>;
134
135 /// Fetches note-related data for a list of [`NoteId`] using the `/GetNotesById`
136 /// RPC endpoint.
137 ///
138 /// For [`miden_objects::note::NoteType::Private`] notes, the response includes only the
139 /// [`miden_objects::note::NoteMetadata`].
140 ///
141 /// For [`miden_objects::note::NoteType::Public`] notes, the response includes all note details
142 /// (recipient, assets, script, etc.).
143 ///
144 /// In both cases, a [`miden_objects::note::NoteInclusionProof`] is returned so the caller can
145 /// verify that each note is part of the block's note tree.
146 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError>;
147
148 /// Fetches info from the node necessary to perform a state sync using the
149 /// `/SyncState` RPC endpoint.
150 ///
151 /// - `block_num` is the last block number known by the client. The returned [`StateSyncInfo`]
152 /// should contain data starting from the next block, until the first block which contains a
153 /// note of matching the requested tag, or the chain tip if there are no notes.
154 /// - `account_ids` is a list of account IDs and determines the accounts the client is
155 /// interested in and should receive account updates of.
156 /// - `note_tags` is a list of tags used to filter the notes the client is interested in, which
157 /// serves as a "note group" filter. Notice that you can't filter by a specific note ID.
158 /// - `nullifiers_tags` similar to `note_tags`, is a list of tags used to filter the nullifiers
159 /// corresponding to some notes the client is interested in.
160 async fn sync_state(
161 &self,
162 block_num: BlockNumber,
163 account_ids: &[AccountId],
164 note_tags: &BTreeSet<NoteTag>,
165 ) -> Result<StateSyncInfo, 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 /// Fetches the nullifiers corresponding to a list of prefixes using the
185 /// `/SyncNullifiers` RPC endpoint.
186 ///
187 /// - `prefix` is a list of nullifiers prefixes to search for.
188 /// - `block_num` is the block number to start the search from. Nullifiers created in this block
189 /// or the following blocks will be included.
190 /// - `block_to` is the optional block number to stop the search at. If not provided, syncs up
191 /// to the network chain tip.
192 async fn sync_nullifiers(
193 &self,
194 prefix: &[u16],
195 block_num: BlockNumber,
196 block_to: Option<BlockNumber>,
197 ) -> Result<Vec<NullifierUpdate>, RpcError>;
198
199 /// Fetches the nullifier proofs corresponding to a list of nullifiers using the
200 /// `/CheckNullifiers` RPC endpoint.
201 async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError>;
202
203 /// Fetches the account data needed to perform a Foreign Procedure Invocation (FPI) on the
204 /// specified foreign accounts, using the `GetAccountProofs` endpoint.
205 ///
206 /// The `code_commitments` parameter is a list of known code commitments
207 /// to prevent unnecessary data fetching. Returns the block number and the FPI account data. If
208 /// one of the tracked accounts is not found in the node, the method will return an error.
209 async fn get_account_proofs(
210 &self,
211 account_storage_requests: &BTreeSet<ForeignAccount>,
212 known_account_codes: BTreeMap<AccountId, AccountCode>,
213 ) -> Result<AccountProofs, RpcError>;
214
215 /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
216 /// then `None` is returned.
217 /// The `block_num` parameter is the block number to start the search from.
218 ///
219 /// The default implementation of this method uses
220 /// [`NodeRpcClient::sync_nullifiers`].
221 async fn get_nullifier_commit_height(
222 &self,
223 nullifier: &Nullifier,
224 block_num: BlockNumber,
225 ) -> Result<Option<BlockNumber>, RpcError> {
226 let nullifiers = self.sync_nullifiers(&[nullifier.prefix()], block_num, None).await?;
227
228 Ok(nullifiers
229 .iter()
230 .find(|update| update.nullifier == *nullifier)
231 .map(|update| update.block_num))
232 }
233
234 /// Fetches public note-related data for a list of [`NoteId`] and builds [`InputNoteRecord`]s
235 /// with it. If a note is not found or it's private, it is ignored and will not be included
236 /// in the returned list.
237 ///
238 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
239 async fn get_public_note_records(
240 &self,
241 note_ids: &[NoteId],
242 current_timestamp: Option<u64>,
243 ) -> Result<Vec<InputNoteRecord>, RpcError> {
244 if note_ids.is_empty() {
245 return Ok(vec![]);
246 }
247
248 let mut public_notes = Vec::with_capacity(note_ids.len());
249 let note_details = self.get_notes_by_id(note_ids).await?;
250
251 for detail in note_details {
252 if let FetchedNote::Public(note, inclusion_proof) = detail {
253 let state = UnverifiedNoteState {
254 metadata: *note.metadata(),
255 inclusion_proof,
256 }
257 .into();
258 let note = InputNoteRecord::new(note.into(), current_timestamp, state);
259
260 public_notes.push(note);
261 }
262 }
263
264 Ok(public_notes)
265 }
266
267 /// Fetches the public accounts that have been updated since the last known state of the
268 /// accounts.
269 ///
270 /// The `local_accounts` parameter is a list of account headers that the client has
271 /// stored locally and that it wants to check for updates. If an account is private or didn't
272 /// change, it is ignored and will not be included in the returned list.
273 /// The default implementation of this method uses [`NodeRpcClient::get_account_details`].
274 async fn get_updated_public_accounts(
275 &self,
276 local_accounts: &[&AccountHeader],
277 ) -> Result<Vec<Account>, RpcError> {
278 let mut public_accounts = vec![];
279
280 for local_account in local_accounts {
281 let response = self.get_account_details(local_account.id()).await?;
282
283 if let FetchedAccount::Public(account, _) = response {
284 let account = *account;
285 // We should only return an account if it's newer, otherwise we ignore it
286 if account.nonce().as_int() > local_account.nonce().as_int() {
287 public_accounts.push(account);
288 }
289 }
290 }
291
292 Ok(public_accounts)
293 }
294
295 /// Given a block number, fetches the block header corresponding to that height from the node
296 /// along with the MMR proof.
297 ///
298 /// The default implementation of this method uses
299 /// [`NodeRpcClient::get_block_header_by_number`].
300 async fn get_block_header_with_proof(
301 &self,
302 block_num: BlockNumber,
303 ) -> Result<(BlockHeader, MmrProof), RpcError> {
304 let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
305 Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
306 }
307
308 /// Fetches the note with the specified ID.
309 ///
310 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
311 ///
312 /// Errors:
313 /// - [`RpcError::NoteNotFound`] if the note with the specified ID is not found.
314 async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
315 let notes = self.get_notes_by_id(&[note_id]).await?;
316 notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
317 }
318
319 /// Fetches the note script with the specified root.
320 ///
321 /// Errors:
322 /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
323 async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError>;
324
325 /// Fetches storage map updates for specified account and storage slots within a block range,
326 /// using the `/SyncStorageMaps` RPC endpoint.
327 ///
328 /// - `block_from`: The starting block number for the range.
329 /// - `block_to`: The ending block number for the range.
330 /// - `account_id`: The account ID for which to fetch storage map updates.
331 async fn sync_storage_maps(
332 &self,
333 block_from: BlockNumber,
334 block_to: Option<BlockNumber>,
335 account_id: AccountId,
336 ) -> Result<StorageMapInfo, RpcError>;
337
338 /// Fetches account vault updates for specified account within a block range,
339 /// using the `/SyncAccountVault` RPC endpoint.
340 ///
341 /// - `block_from`: The starting block number for the range.
342 /// - `block_to`: The ending block number for the range.
343 /// - `account_id`: The account ID for which to fetch storage map updates.
344 async fn sync_account_vault(
345 &self,
346 block_from: BlockNumber,
347 block_to: Option<BlockNumber>,
348 account_id: AccountId,
349 ) -> Result<AccountVaultInfo, RpcError>;
350
351 /// Fetches transactions records for specific accounts within a block range.
352 /// Using the `/SyncTransactions` RPC endpoint.
353 ///
354 /// - `block_from`: The starting block number for the range.
355 /// - `block_to`: The ending block number for the range.
356 /// - `account_ids`: The account IDs for which to fetch storage map updates.
357 async fn sync_transactions(
358 &self,
359 block_from: BlockNumber,
360 block_to: Option<BlockNumber>,
361 account_ids: Vec<AccountId>,
362 ) -> Result<TransactionsInfo, RpcError>;
363
364 /// Fetches the network ID of the node.
365 /// Errors:
366 /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
367 async fn get_network_id(&self) -> Result<NetworkId, RpcError>;
368}
369
370// RPC API ENDPOINT
371// ================================================================================================
372//
373/// RPC methods for the Miden protocol.
374#[derive(Debug)]
375pub enum NodeRpcClientEndpoint {
376 CheckNullifiers,
377 SyncNullifiers,
378 GetAccountDetails,
379 GetAccountStateDelta,
380 GetAccountProofs,
381 GetBlockByNumber,
382 GetBlockHeaderByNumber,
383 GetNotesById,
384 SyncState,
385 SubmitProvenTx,
386 SyncNotes,
387 GetNoteScriptByRoot,
388 SyncStorageMaps,
389 SyncAccountVault,
390 SyncTransactions,
391}
392
393impl fmt::Display for NodeRpcClientEndpoint {
394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395 match self {
396 NodeRpcClientEndpoint::CheckNullifiers => write!(f, "check_nullifiers"),
397 NodeRpcClientEndpoint::SyncNullifiers => {
398 write!(f, "sync_nullifiers")
399 },
400 NodeRpcClientEndpoint::GetAccountDetails => write!(f, "get_account_details"),
401 NodeRpcClientEndpoint::GetAccountStateDelta => write!(f, "get_account_state_delta"),
402 NodeRpcClientEndpoint::GetAccountProofs => write!(f, "get_account_proofs"),
403 NodeRpcClientEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
404 NodeRpcClientEndpoint::GetBlockHeaderByNumber => {
405 write!(f, "get_block_header_by_number")
406 },
407 NodeRpcClientEndpoint::GetNotesById => write!(f, "get_notes_by_id"),
408 NodeRpcClientEndpoint::SyncState => write!(f, "sync_state"),
409 NodeRpcClientEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
410 NodeRpcClientEndpoint::SyncNotes => write!(f, "sync_notes"),
411 NodeRpcClientEndpoint::GetNoteScriptByRoot => write!(f, "get_note_script_by_root"),
412 NodeRpcClientEndpoint::SyncStorageMaps => write!(f, "sync_storage_maps"),
413 NodeRpcClientEndpoint::SyncAccountVault => write!(f, "sync_account_vault"),
414 NodeRpcClientEndpoint::SyncTransactions => write!(f, "sync_transactions"),
415 }
416 }
417}