Skip to main content

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