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//! In addition, the module provides implementations for different environments (e.g. tonic-based or
13//! web-based) via feature flags ( `tonic` and `web-tonic`).
14//!
15//! ## Example
16//!
17//! ```no_run
18//! # use miden_client::rpc::{Endpoint, NodeRpcClient, TonicRpcClient};
19//! # use miden_objects::block::BlockNumber;
20//! # #[tokio::main]
21//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! // Create a Tonic RPC client instance (assumes default endpoint configuration).
23//! let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
24//! let mut rpc_client = TonicRpcClient::new(&endpoint, 1000);
25//!
26//! // Fetch the latest block header (by passing None).
27//! let (block_header, mmr_proof) = rpc_client.get_block_header_by_number(None, true).await?;
28//!
29//! println!("Latest block number: {}", block_header.block_num());
30//! if let Some(proof) = mmr_proof {
31//! println!("MMR proof received accordingly");
32//! }
33//!
34//! # Ok(())
35//! # }
36//! ```
37//! The client also makes use of this component in order to communicate with the node.
38//!
39//! For further details and examples, see the documentation for the individual methods in the
40//! [`NodeRpcClient`] trait.
41
42use alloc::{boxed::Box, collections::BTreeSet, string::String, vec::Vec};
43use core::fmt;
44
45use domain::{
46 account::{AccountProofs, FetchedAccount},
47 note::{FetchedNote, NoteSyncInfo},
48 nullifier::NullifierUpdate,
49 sync::StateSyncInfo,
50};
51use miden_objects::{
52 account::{Account, AccountCode, AccountDelta, AccountHeader, AccountId},
53 block::{BlockHeader, BlockNumber, ProvenBlock},
54 crypto::merkle::{MmrProof, SmtProof},
55 note::{NoteId, NoteTag, Nullifier},
56 transaction::ProvenTransaction,
57};
58
59/// Contains domain types related to RPC requests and responses, as well as utility functions
60/// for dealing with them.
61pub mod domain;
62
63mod errors;
64pub use errors::RpcError;
65
66mod endpoint;
67pub use endpoint::Endpoint;
68
69#[cfg(not(feature = "testing"))]
70mod generated;
71#[cfg(feature = "testing")]
72pub mod generated;
73
74#[cfg(all(feature = "tonic", feature = "web-tonic"))]
75compile_error!("features `tonic` and `web-tonic` are mutually exclusive");
76
77#[cfg(any(feature = "tonic", feature = "web-tonic"))]
78mod tonic_client;
79#[cfg(any(feature = "tonic", feature = "web-tonic"))]
80pub use tonic_client::TonicRpcClient;
81
82use crate::{
83 store::{InputNoteRecord, input_note_states::UnverifiedNoteState},
84 transaction::ForeignAccount,
85};
86
87// NODE RPC CLIENT TRAIT
88// ================================================================================================
89
90/// Defines the interface for communicating with the Miden node.
91///
92/// The implementers are responsible for connecting to the Miden node, handling endpoint
93/// requests/responses, and translating responses into domain objects relevant for each of the
94/// endpoints.
95#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
96#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
97pub trait NodeRpcClient: Send + Sync {
98 /// Given a Proven Transaction, send it to the node for it to be included in a future block
99 /// using the `/SubmitProvenTransaction` RPC endpoint.
100 async fn submit_proven_transaction(
101 &self,
102 proven_transaction: ProvenTransaction,
103 ) -> Result<BlockNumber, RpcError>;
104
105 /// Given a block number, fetches the block header corresponding to that height from the node
106 /// using the `/GetBlockHeaderByNumber` endpoint.
107 /// If `include_mmr_proof` is set to true and the function returns an `Ok`, the second value
108 /// of the return tuple should always be Some(MmrProof).
109 ///
110 /// When `None` is provided, returns info regarding the latest block.
111 async fn get_block_header_by_number(
112 &self,
113 block_num: Option<BlockNumber>,
114 include_mmr_proof: bool,
115 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError>;
116
117 /// Given a block number, fetches the block corresponding to that height from the node using
118 /// the `/GetBlockByNumber` RPC endpoint.
119 async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError>;
120
121 /// Fetches note-related data for a list of [NoteId] using the `/GetNotesById` rpc endpoint.
122 ///
123 /// For any NoteType::Private note, the return data is only the
124 /// [miden_objects::note::NoteMetadata], whereas for NoteType::Onchain notes, the return
125 /// data includes all details.
126 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError>;
127
128 /// Fetches info from the node necessary to perform a state sync using the
129 /// `/SyncState` RPC endpoint.
130 ///
131 /// - `block_num` is the last block number known by the client. The returned [StateSyncInfo]
132 /// should contain data starting from the next block, until the first block which contains a
133 /// note of matching the requested tag, or the chain tip if there are no notes.
134 /// - `account_ids` is a list of account IDs and determines the accounts the client is
135 /// interested in and should receive account updates of.
136 /// - `note_tags` is a list of tags used to filter the notes the client is interested in, which
137 /// serves as a "note group" filter. Notice that you can't filter by a specific note ID.
138 /// - `nullifiers_tags` similar to `note_tags`, is a list of tags used to filter the nullifiers
139 /// corresponding to some notes the client is interested in.
140 async fn sync_state(
141 &self,
142 block_num: BlockNumber,
143 account_ids: &[AccountId],
144 note_tags: &BTreeSet<NoteTag>,
145 ) -> Result<StateSyncInfo, RpcError>;
146
147 /// Fetches the current state of an account from the node using the `/GetAccountDetails` RPC
148 /// endpoint.
149 ///
150 /// - `account_id` is the ID of the wanted account.
151 async fn get_account_details(&self, account_id: AccountId) -> Result<FetchedAccount, RpcError>;
152
153 /// Fetches the notes related to the specified tags using the `/SyncNotes` RPC endpoint.
154 ///
155 /// - `block_num` is the last block number known by the client.
156 /// - `note_tags` is a list of tags used to filter the notes the client is interested in.
157 async fn sync_notes(
158 &self,
159 block_num: BlockNumber,
160 note_tags: &BTreeSet<NoteTag>,
161 ) -> Result<NoteSyncInfo, RpcError>;
162
163 /// Fetches the nullifiers corresponding to a list of prefixes using the
164 /// `/CheckNullifiersByPrefix` RPC endpoint.
165 ///
166 /// - `prefix` is a list of nullifiers prefixes to search for.
167 /// - `block_num` is the block number to start the search from. Nullifiers created in this block
168 /// or the following blocks will be included.
169 async fn check_nullifiers_by_prefix(
170 &self,
171 prefix: &[u16],
172 block_num: BlockNumber,
173 ) -> Result<Vec<NullifierUpdate>, RpcError>;
174
175 /// Fetches the nullifier proofs corresponding to a list of nullifiers using the
176 /// `/CheckNullifiers` RPC endpoint.
177 async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError>;
178
179 /// Fetches the account data needed to perform a Foreign Procedure Invocation (FPI) on the
180 /// specified foreign accounts, using the `GetAccountProofs` endpoint.
181 ///
182 /// The `code_commitments` parameter is a list of known code commitments
183 /// to prevent unnecessary data fetching. Returns the block number and the FPI account data. If
184 /// one of the tracked accounts is not found in the node, the method will return an error.
185 async fn get_account_proofs(
186 &self,
187 account_storage_requests: &BTreeSet<ForeignAccount>,
188 known_account_codes: Vec<AccountCode>,
189 ) -> Result<AccountProofs, RpcError>;
190
191 /// Fetches the account state delta for the specified account between the specified blocks
192 /// using the `/GetAccountStateDelta` RPC endpoint.
193 async fn get_account_state_delta(
194 &self,
195 account_id: AccountId,
196 from_block: BlockNumber,
197 to_block: BlockNumber,
198 ) -> Result<AccountDelta, RpcError>;
199
200 /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
201 /// then `None` is returned.
202 /// The `block_num` parameter is the block number to start the search from.
203 ///
204 /// The default implementation of this method uses [NodeRpcClient::check_nullifiers_by_prefix].
205 async fn get_nullifier_commit_height(
206 &self,
207 nullifier: &Nullifier,
208 block_num: BlockNumber,
209 ) -> Result<Option<u32>, RpcError> {
210 let nullifiers = self.check_nullifiers_by_prefix(&[nullifier.prefix()], block_num).await?;
211
212 Ok(nullifiers
213 .iter()
214 .find(|update| update.nullifier == *nullifier)
215 .map(|update| update.block_num))
216 }
217
218 /// Fetches public note-related data for a list of [NoteId] and builds [InputNoteRecord]s with
219 /// it. If a note is not found or it's private, it is ignored and will not be included in the
220 /// returned list.
221 ///
222 /// The default implementation of this method uses [NodeRpcClient::get_notes_by_id].
223 async fn get_public_note_records(
224 &self,
225 note_ids: &[NoteId],
226 current_timestamp: Option<u64>,
227 ) -> Result<Vec<InputNoteRecord>, RpcError> {
228 let note_details = self.get_notes_by_id(note_ids).await?;
229
230 let mut public_notes = vec![];
231 for detail in note_details {
232 if let FetchedNote::Public(note, inclusion_proof) = detail {
233 let state = UnverifiedNoteState {
234 metadata: *note.metadata(),
235 inclusion_proof,
236 }
237 .into();
238 let note = InputNoteRecord::new(note.into(), current_timestamp, state);
239
240 public_notes.push(note);
241 }
242 }
243
244 Ok(public_notes)
245 }
246
247 /// Fetches the public accounts that have been updated since the last known state of the
248 /// accounts.
249 ///
250 /// The `local_accounts` parameter is a list of account headers that the client has
251 /// stored locally and that it wants to check for updates. If an account is private or didn't
252 /// change, it is ignored and will not be included in the returned list.
253 /// The default implementation of this method uses [NodeRpcClient::get_account_details].
254 async fn get_updated_public_accounts(
255 &self,
256 local_accounts: &[&AccountHeader],
257 ) -> Result<Vec<Account>, RpcError> {
258 let mut public_accounts = vec![];
259
260 for local_account in local_accounts {
261 let response = self.get_account_details(local_account.id()).await?;
262
263 if let FetchedAccount::Public(account, _) = response {
264 // We should only return an account if it's newer, otherwise we ignore it
265 if account.nonce().as_int() > local_account.nonce().as_int() {
266 public_accounts.push(account);
267 }
268 }
269 }
270
271 Ok(public_accounts)
272 }
273
274 /// Given a block number, fetches the block header corresponding to that height from the node
275 /// along with the MMR proof.
276 ///
277 /// The default implementation of this method uses [NodeRpcClient::get_block_header_by_number].
278 async fn get_block_header_with_proof(
279 &self,
280 block_num: BlockNumber,
281 ) -> Result<(BlockHeader, MmrProof), RpcError> {
282 let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
283 Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
284 }
285
286 /// Fetches the note with the specified ID.
287 ///
288 /// The default implementation of this method uses [NodeRpcClient::get_notes_by_id].
289 ///
290 /// Errors:
291 /// - [RpcError::NoteNotFound] if the note with the specified ID is not found.
292 async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
293 let notes = self.get_notes_by_id(&[note_id]).await?;
294 notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
295 }
296}
297
298// RPC API ENDPOINT
299// ================================================================================================
300//
301/// RPC methods for the Miden protocol.
302#[derive(Debug)]
303pub enum NodeRpcClientEndpoint {
304 CheckNullifiers,
305 CheckNullifiersByPrefix,
306 GetAccountDetails,
307 GetAccountStateDelta,
308 GetAccountProofs,
309 GetBlockByNumber,
310 GetBlockHeaderByNumber,
311 SyncState,
312 SubmitProvenTx,
313 SyncNotes,
314}
315
316impl fmt::Display for NodeRpcClientEndpoint {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 match self {
319 NodeRpcClientEndpoint::CheckNullifiers => write!(f, "check_nullifiers"),
320 NodeRpcClientEndpoint::CheckNullifiersByPrefix => {
321 write!(f, "check_nullifiers_by_prefix")
322 },
323 NodeRpcClientEndpoint::GetAccountDetails => write!(f, "get_account_details"),
324 NodeRpcClientEndpoint::GetAccountStateDelta => write!(f, "get_account_state_delta"),
325 NodeRpcClientEndpoint::GetAccountProofs => write!(f, "get_account_proofs"),
326 NodeRpcClientEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
327 NodeRpcClientEndpoint::GetBlockHeaderByNumber => {
328 write!(f, "get_block_header_by_number")
329 },
330 NodeRpcClientEndpoint::SyncState => write!(f, "sync_state"),
331 NodeRpcClientEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
332 NodeRpcClientEndpoint::SyncNotes => write!(f, "sync_notes"),
333 }
334 }
335}