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