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