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}