miden_client/rpc/mod.rs
1//! Provides an interface for the client to communicate with a Miden node using Remote Procedure
2//! 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//! - Submit proven batches.
9//! - Retrieve block headers (optionally with MMR proofs).
10//! - Sync state updates (including notes, nullifiers, and account updates).
11//! - Fetch details for specific notes and accounts.
12//!
13//! The client implementation adapts to the target environment automatically:
14//! - Native targets use `tonic` transport with TLS.
15//! - `wasm32` targets use `tonic-web-wasm-client` transport.
16//!
17//! ## Example
18//!
19//! ```no_run
20//! # use miden_client::rpc::{Endpoint, NodeRpcClient, GrpcClient, VerifyingRpcClient};
21//! # use miden_protocol::block::BlockNumber;
22//! # #[tokio::main]
23//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! // Create a gRPC client instance (assumes default endpoint configuration), wrapped so that
25//! // node responses are verified against the requests.
26//! let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
27//! let rpc_client = VerifyingRpcClient::new(GrpcClient::new(&endpoint, 1000));
28//!
29//! // Fetch the latest block header (by passing None).
30//! let (block_header, mmr_proof) = rpc_client.get_block_header_by_number(None, true).await?;
31//!
32//! println!("Latest block number: {}", block_header.block_num());
33//! if let Some(proof) = mmr_proof {
34//! println!("MMR proof received accordingly");
35//! }
36//!
37//! # Ok(())
38//! # }
39//! ```
40//! The client also makes use of this component in order to communicate with the node.
41//!
42//! For further details and examples, see the documentation for the individual methods in the
43//! [`NodeRpcClient`] trait.
44
45use alloc::boxed::Box;
46use alloc::collections::{BTreeMap, BTreeSet};
47use alloc::string::String;
48use alloc::vec::Vec;
49use core::fmt;
50
51use domain::account::{
52 AccountDetails,
53 AccountFetch,
54 AccountProof,
55 AccountStorageMapDetails,
56 AccountVaultDetails,
57 GetAccountRequest,
58 StorageMapEntries,
59 StorageMapEntry,
60 StorageMapFetch,
61 VaultFetch,
62};
63use domain::note::{FetchedNote, ResolvedSyncNotesBlock, SyncNotesBlock, SyncedNote};
64use domain::nullifier::NullifierUpdate;
65use domain::sync::{ChainMmrInfo, SyncTarget};
66use encryption::{AttestedTransactionEncryptionKey, SealedTransactionInputs};
67use miden_protocol::Word;
68use miden_protocol::account::{Account, AccountId};
69use miden_protocol::address::NetworkId;
70use miden_protocol::batch::{ProposedBatch, ProvenBatch};
71use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock};
72use miden_protocol::crypto::merkle::mmr::MmrProof;
73use miden_protocol::note::{
74 NoteAttachments,
75 NoteDetails,
76 NoteId,
77 NoteScript,
78 NoteTag,
79 NoteType,
80 Nullifier,
81};
82use miden_protocol::transaction::ProvenTransaction;
83use miden_protocol::vm::ExecutionProof;
84
85use crate::rpc::domain::storage_map::StorageMapInfo;
86
87/// Contains domain types related to RPC requests and responses, as well as utility functions for
88/// dealing with them.
89pub mod domain;
90pub mod encryption;
91
92mod errors;
93pub use errors::*;
94
95mod endpoint;
96pub(crate) use domain::limits::RPC_LIMITS_STORE_SETTING;
97pub use domain::limits::RpcLimits;
98pub use domain::status::{NetworkNoteStatus, NetworkNoteStatusInfo, RpcStatusInfo};
99pub use endpoint::Endpoint;
100
101#[cfg(not(feature = "testing"))]
102mod generated;
103#[cfg(feature = "testing")]
104pub mod generated;
105
106#[cfg(feature = "tonic")]
107mod tonic_client;
108#[cfg(feature = "tonic")]
109pub use tonic_client::GrpcClient;
110
111mod verifying_client;
112pub use verifying_client::VerifyingRpcClient;
113
114use crate::rpc::domain::account_vault::AccountVaultInfo;
115use crate::rpc::domain::transaction::TransactionRecord;
116use crate::store::InputNoteRecord;
117use crate::store::input_note_states::UnverifiedNoteState;
118
119/// Represents the state that we want to retrieve from the network
120#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
121pub enum AccountStateAt {
122 /// Gets the latest state, for the current chain tip
123 #[default]
124 ChainTip,
125 /// Gets the state at a specific block number
126 Block(BlockNumber),
127}
128
129// NODE RPC CLIENT TRAIT
130// ================================================================================================
131
132/// Defines the interface for communicating with the Miden node.
133///
134/// The implementers are responsible for connecting to the Miden node, handling endpoint
135/// requests/responses, and translating responses into domain objects relevant for each of the
136/// endpoints. Implementations do not check that responses correspond to the method's arguments.
137/// Wrap a client in [`VerifyingRpcClient`] to reject mismatched responses.
138#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
139#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
140pub trait NodeRpcClient: Send + Sync {
141 /// Sets the genesis commitment for the client and reconnects to the node providing the genesis
142 /// commitment in the request headers. If the genesis commitment is already set, this method
143 /// does nothing.
144 async fn set_genesis_commitment(&self, commitment: Word) -> Result<(), RpcError>;
145
146 /// Returns the genesis commitment if it has been set, without fetching from the node.
147 fn has_genesis_commitment(&self) -> Option<Word>;
148
149 /// Fetches the validator set's transaction encryption key using the
150 /// `/GetTransactionEncryptionKey` endpoint.
151 ///
152 /// The key arrives attested but untrusted: this endpoint is served by the RPC operator, so the
153 /// response must be passed through [`AttestedTransactionEncryptionKey::verify`] before it is
154 /// used to seal anything.
155 async fn get_transaction_encryption_key(
156 &self,
157 ) -> Result<AttestedTransactionEncryptionKey, RpcError>;
158
159 /// Given a Proven Transaction, send it to the node for it to be included in a future block
160 /// using the `/SubmitProvenTransaction` RPC endpoint.
161 ///
162 /// The transaction inputs are passed already sealed, since sealing needs the client's RNG for
163 /// the scheme's ephemeral key material. See [`encryption`] for how they are produced.
164 ///
165 /// Returns the node's chain tip at submission (not the block the transaction is committed in).
166 async fn submit_proven_transaction(
167 &self,
168 proven_transaction: &ProvenTransaction,
169 sealed_transaction_inputs: SealedTransactionInputs,
170 ) -> Result<BlockNumber, RpcError>;
171
172 /// Given a Proven Batch together with the corresponding [`ProposedBatch`] and the list of
173 /// [`SealedTransactionInputs`] (one per transaction, matching the ordering of the batch), sends
174 /// the batch to the node for inclusion in a future block using the `/SubmitProvenBatch` RPC
175 /// endpoint. All transactions in the batch must build on the current mempool state following
176 /// normal transaction submission rules.
177 ///
178 /// Each transaction's inputs are sealed independently against its own transaction ID, because
179 /// the node fans the batch out into one validator submission per transaction. See
180 /// [`encryption`] for how the sealed inputs are produced.
181 ///
182 /// Returns the node's chain tip at submission (not the block the batch is committed in).
183 async fn submit_proven_batch(
184 &self,
185 proven_batch: &ProvenBatch,
186 proposed_batch: &ProposedBatch,
187 transaction_inputs: Vec<SealedTransactionInputs>,
188 ) -> Result<BlockNumber, RpcError>;
189
190 /// Given a block number, fetches the block header corresponding to that height from the node
191 /// using the `/GetBlockHeaderByNumber` endpoint. If `include_mmr_proof` is set to true and the
192 /// function returns an `Ok`, the second value of the return tuple should always be
193 /// Some(MmrProof).
194 ///
195 /// When `None` is provided, returns info regarding the latest block.
196 ///
197 /// The returned header is not verified against the requested `block_num`;
198 /// [`VerifyingRpcClient`] performs that check.
199 async fn get_block_header_by_number(
200 &self,
201 block_num: Option<BlockNumber>,
202 include_mmr_proof: bool,
203 ) -> Result<(BlockHeader, Option<MmrProof>), RpcError>;
204
205 /// Given a block number, fetches the block corresponding to that height from the node using the
206 /// `/GetBlockByNumber` RPC endpoint.
207 ///
208 /// The node returns the block and its proof in separate fields, so the proof is returned
209 /// alongside the signed block. It is [`None`] when `include_proof` is false, and also when the
210 /// node has not proven the block yet.
211 ///
212 /// The returned block is not verified against the requested `block_num`; [`VerifyingRpcClient`]
213 /// performs that check.
214 async fn get_block_by_number(
215 &self,
216 block_num: BlockNumber,
217 include_proof: bool,
218 ) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError>;
219
220 /// Fetches note-related data for a list of [`NoteId`] using the `/GetNotesById` RPC endpoint.
221 ///
222 /// For [`miden_protocol::note::NoteType::Private`] notes, the response includes only the
223 /// [`miden_protocol::note::NoteMetadata`].
224 ///
225 /// For [`miden_protocol::note::NoteType::Public`] notes, the response includes all note details
226 /// (recipient, assets, script, etc.).
227 ///
228 /// In both cases, a [`miden_protocol::note::NoteInclusionProof`] is returned so the caller can
229 /// verify that each note is part of the block's note tree.
230 ///
231 /// Returned notes are not verified to be among the requested `note_ids`; [`VerifyingRpcClient`]
232 /// performs that check.
233 async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError>;
234
235 /// Fetches the MMR delta for a given block range using the `/SyncChainMmr` RPC endpoint.
236 ///
237 /// - `current_block_height` is the last block number already present in the caller's MMR.
238 /// - `upper_bound` determines the upper bound of the sync range. Can be a specific block number
239 /// (`BlockNumber`), or a chain tip finality level: `CommittedChainTip` syncs up to the latest
240 /// committed block (the chain tip), while `ProvenChainTip` syncs up to the latest proven
241 /// block which may be behind the committed tip.
242 async fn sync_chain_mmr(
243 &self,
244 current_block_height: BlockNumber,
245 upper_bound: SyncTarget,
246 ) -> Result<ChainMmrInfo, RpcError>;
247
248 /// Fetches the full state of a public account from the node using the `/GetAccount` endpoint,
249 /// and then resolves oversized vault and storage map entries via the `SyncVault` and
250 /// `SyncStorageMap` endpoints when needed.
251 ///
252 /// - `account_id` is the ID of the wanted account.
253 ///
254 /// Returns `Ok(None)` for accounts without public state.
255 async fn get_account_details(
256 &self,
257 account_id: AccountId,
258 ) -> Result<Option<Account>, RpcError> {
259 // Accounts without public state have no full state to fetch; only a commitment is on-chain.
260 if !account_id.is_public() {
261 return Ok(None);
262 }
263
264 // A single request fetches the full public state: every storage map's entries plus the
265 // vault, with the storage layout discovered server-side.
266 let (block_number, mut proof) = self
267 .get_account(
268 GetAccountRequest::new(account_id).with_fetch(
269 AccountFetch::new()
270 .with_storage(StorageMapFetch::All)
271 .with_vault(VaultFetch::Always),
272 ),
273 )
274 .await?;
275
276 if let Some(details) = proof.details_mut() {
277 self.resolve_oversize_vault(account_id, block_number, details).await?;
278 self.resolve_oversize_storage_maps(account_id, block_number, details).await?;
279 }
280
281 let details = proof.into_details().ok_or(RpcError::ExpectedDataMissing(
282 "public account returned without details".into(),
283 ))?;
284
285 Ok(Some(Account::try_from(&details)?))
286 }
287
288 /// Fetches notes related to the specified tags using the `/SyncNotes` RPC endpoint, paginating
289 /// over the full block range and returning, in block-number order, every block in that range
290 /// that contains at least one note matching the requested tags.
291 ///
292 /// - `block_from`: The starting block number for the range (inclusive).
293 /// - `block_to`: The ending block number for the range (inclusive).
294 /// - `note_tags` is the set of tags used to filter the notes the client is interested in.
295 ///
296 /// Every returned note carries its full metadata, and its attachment content when the response
297 /// carried it. Use [`NodeRpcClient::sync_notes_with_content`] to resolve the rest.
298 ///
299 /// Returned notes are not verified to carry one of the requested `note_tags`;
300 /// [`VerifyingRpcClient`] performs that check.
301 async fn sync_notes(
302 &self,
303 block_from: BlockNumber,
304 block_to: BlockNumber,
305 note_tags: &BTreeSet<NoteTag>,
306 ) -> Result<Vec<SyncNotesBlock>, RpcError>;
307
308 /// Calls [`NodeRpcClient::sync_notes`] for the requested range, then makes a single
309 /// [`NodeRpcClient::get_notes_by_id`] call to resolve the note content the sync response did
310 /// not already carry, according to `fetch`, folding it into each note.
311 ///
312 /// A note whose attachments the sync response already carried needs no request: only notes
313 /// reporting `needs_attachment_fetch` have theirs fetched.
314 ///
315 /// With [`NoteContentFetch::PublicDetailsAndAttachments`], all public notes in the range are
316 /// additionally fetched so the request does not reveal the client's interest set. Narrowing it
317 /// reveals nothing either, since the omissions follow the node's own sync records.
318 ///
319 /// Returns one [`ResolvedSyncNotesBlock`] per matching block, each note carrying its inclusion
320 /// data alongside its content.
321 ///
322 /// A note whose resolved content contradicts its sync record is dropped with a warning rather
323 /// than failing the call, since anyone can commit a note whose content they never publish.
324 async fn sync_notes_with_content(
325 &self,
326 block_from: BlockNumber,
327 block_to: BlockNumber,
328 note_tags: &BTreeSet<NoteTag>,
329 fetch: NoteContentFetch,
330 ) -> Result<Vec<ResolvedSyncNotesBlock>, RpcError> {
331 let blocks = self.sync_notes(block_from, block_to, note_tags).await?;
332 let note_ids: Vec<NoteId> = blocks
333 .iter()
334 .flat_map(|block| block.notes.values())
335 .filter(|note| match fetch {
336 NoteContentFetch::PublicDetailsAndAttachments => {
337 note.note_type() == NoteType::Public || note.needs_attachment_fetch()
338 },
339 NoteContentFetch::AttachmentsOnly => note.needs_attachment_fetch(),
340 })
341 .map(|note| *note.note_id())
342 .collect();
343
344 let mut fetched_content: BTreeMap<NoteId, (Option<NoteDetails>, Option<NoteAttachments>)> =
345 BTreeMap::new();
346 if !note_ids.is_empty() {
347 for fetched_note in self.get_notes_by_id(¬e_ids).await? {
348 let (note_id, details, attachments) = match fetched_note {
349 FetchedNote::Public(note, _) => {
350 let note_id = note.id();
351 let (assets, _, recipient, attachments) = note.into_parts();
352 (note_id, Some(NoteDetails::new(assets, recipient)), attachments)
353 },
354 FetchedNote::Private(note_id, _, attachments, _) => {
355 (note_id, None, attachments)
356 },
357 };
358
359 // An empty set carries nothing, so it is recorded as absent rather than as content:
360 // keeping it would shadow the attachments the note's own sync record may already
361 // have carried, for a public note as much as for a private one.
362 let attachments = (!attachments.is_empty()).then_some(attachments);
363 fetched_content.insert(note_id, (details, attachments));
364 }
365 }
366
367 // Fold the resolved content into each note, keeping the per-block grouping so the inclusion
368 // data (header + MMR path) is carried once per block. `SyncedNote::new` rejects content
369 // that is inconsistent with its sync record (mismatched or missing attachment content);
370 // such notes are dropped rather than failing the sync, since a tracked record is never
371 // stored incomplete this way (it stays expected and can be retried by re-importing), while
372 // a hard error would wedge every sync scanning this block range.
373 let mut synced_blocks = Vec::with_capacity(blocks.len());
374 for block in blocks {
375 let mut notes = BTreeMap::new();
376 for (note_id, committed) in block.notes {
377 // Fetched attachments win when the response actually carried some: a public note's
378 // attachments are bound to the requested id, which `VerifyingRpcClient` checks. The
379 // sync record is the fallback, and a note reporting neither has none.
380 let (details, fetched_attachments) =
381 fetched_content.remove(¬e_id).unwrap_or_default();
382 let attachments = fetched_attachments
383 .or_else(|| committed.attachments().cloned())
384 .unwrap_or_else(NoteAttachments::empty);
385
386 match SyncedNote::new(committed, details, attachments) {
387 Ok(synced_note) => {
388 notes.insert(note_id, synced_note);
389 },
390 Err(err) => {
391 tracing::warn!(%note_id, %err, "skipping synced note with unusable content");
392 },
393 }
394 }
395 synced_blocks.push(ResolvedSyncNotesBlock {
396 block_header: block.block_header,
397 mmr_path: block.mmr_path,
398 notes,
399 });
400 }
401
402 Ok(synced_blocks)
403 }
404
405 /// Fetches the nullifiers corresponding to a list of prefixes using the `/SyncNullifiers` RPC
406 /// endpoint.
407 ///
408 /// - `prefix` is a list of nullifiers prefixes to search for.
409 /// - `block_from`: The starting block number for the range (inclusive).
410 /// - `block_to`: The ending block number for the range (inclusive).
411 ///
412 /// Returned nullifiers are not verified to carry one of the requested prefixes;
413 /// [`VerifyingRpcClient`] performs that check.
414 async fn sync_nullifiers(
415 &self,
416 prefix: &[u16],
417 block_from: BlockNumber,
418 block_to: BlockNumber,
419 ) -> Result<Vec<NullifierUpdate>, RpcError>;
420
421 /// Fetches the account from the node, using the `/GetAccount` endpoint.
422 ///
423 /// The response carries an
424 /// [`AccountWitness`](miden_protocol::block::account_tree::AccountWitness) and the target
425 /// block. Public accounts additionally get [`AccountDetails`]; for private accounts the other
426 /// `request` fields are ignored.
427 ///
428 /// For a fully oversize-resolved account, use [`NodeRpcClient::get_account_details`].
429 ///
430 /// The response block number is not verified against the requested one; [`VerifyingRpcClient`]
431 /// performs that check.
432 ///
433 /// # Errors
434 ///
435 /// - If the account isn't found in the network
436 async fn get_account(
437 &self,
438 request: GetAccountRequest,
439 ) -> Result<(BlockNumber, AccountProof), RpcError>;
440
441 /// Binds an invitation code to an account on the network, using the `/RegisterAccount`
442 /// endpoint.
443 ///
444 /// A network that enforces an account allowlist creates an account on chain only when the
445 /// account is registered. Registration does not create the account. The first transaction of
446 /// the account does that, and the node rejects it when the account is not registered.
447 ///
448 /// When the network operator runs a funding service, the node pays the registered account a
449 /// public P2ID note with the native asset. The node answers before that note is committed. The
450 /// note is not part of the response. The caller must sync until the note arrives. A network
451 /// that does not enforce the allowlist ignores the code, but still registers and funds the
452 /// account.
453 ///
454 /// A retry with the same code and account succeeds without changes, and does not request
455 /// funding again.
456 ///
457 /// # Errors
458 ///
459 /// The node rejects the registration with a [`RegisterAccountError`] when the code is unknown,
460 /// when the code or the account is already registered, or when the request is malformed. A
461 /// funding failure comes back as an `Unavailable` status. The account stays registered in that
462 /// case, and a retry does not fund it.
463 ///
464 /// [`RegisterAccountError`]: crate::rpc::RegisterAccountError
465 async fn register_account(
466 &self,
467 invitation_code: &str,
468 account_id: AccountId,
469 ) -> Result<(), RpcError>;
470
471 /// Returns whether the account may be created on chain, using the `/IsAccountAllowed` endpoint.
472 ///
473 /// The node answers `true` when it does not enforce an account allowlist, or when the account
474 /// is registered. The allowlist gates account creation only, so an account that already exists
475 /// on chain is not checked.
476 async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, RpcError>;
477
478 /// Returns whether the invitation code can be used, using the `/IsInvitationCodeValid`
479 /// endpoint.
480 ///
481 /// The node answers `true` when it does not enforce an account allowlist, or when the code
482 /// exists and is not registered to an account. Unknown and registered codes return `false`. An
483 /// empty code returns `INVALID_ARGUMENT` when enforcement is enabled. This query does not
484 /// consume the code or identify the account that holds it.
485 async fn is_invitation_code_valid(&self, invitation_code: &str) -> Result<bool, RpcError>;
486
487 /// Fills in the asset list when the vault came back as [`AccountVaultDetails::TooManyAssets`],
488 /// by querying [`NodeRpcClient::sync_account_vault`] over `[GENESIS, block_to]`. No-op for the
489 /// other variants.
490 async fn resolve_oversize_vault(
491 &self,
492 account_id: AccountId,
493 block_to: BlockNumber,
494 details: &mut AccountDetails,
495 ) -> Result<(), RpcError> {
496 if !details.vault_details.is_too_many_assets() {
497 return Ok(());
498 }
499 let vault_info =
500 self.sync_account_vault(BlockNumber::GENESIS, block_to, account_id).await?;
501 // Syncing from genesis merges the full vault history into an absolute patch, so its updated
502 // (non-removed) assets are the account's current vault contents.
503 details.vault_details =
504 AccountVaultDetails::Assets(vault_info.vault_patch.updated_assets().collect());
505 Ok(())
506 }
507
508 /// Fills in the entries of any storage map the node reported as oversize, by querying
509 /// [`NodeRpcClient::sync_storage_maps`] over `[GENESIS, block_to]`. No-op when no map is
510 /// oversize.
511 async fn resolve_oversize_storage_maps(
512 &self,
513 account_id: AccountId,
514 block_to: BlockNumber,
515 details: &mut AccountDetails,
516 ) -> Result<(), RpcError> {
517 if !details
518 .storage_details
519 .map_details
520 .iter()
521 .any(AccountStorageMapDetails::is_limit_exceeded)
522 {
523 return Ok(());
524 }
525 let info = self.sync_storage_maps(BlockNumber::GENESIS, block_to, account_id).await?;
526 for map_details in &mut details.storage_details.map_details {
527 if !map_details.is_limit_exceeded() {
528 continue;
529 }
530 // Syncing from genesis merges the full history of each slot into its absolute current
531 // entries, so the result is the complete map content.
532 let entries: Vec<StorageMapEntry> = info
533 .map_entries
534 .get(&map_details.slot_name)
535 .map(|entries| {
536 entries
537 .as_map()
538 .iter()
539 .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
540 .collect()
541 })
542 .unwrap_or_default();
543 map_details.entries = StorageMapEntries::AllEntries(entries);
544 }
545 Ok(())
546 }
547
548 /// Fetches the commit height where the nullifier was consumed. If the nullifier isn't found,
549 /// then `None` is returned. The `block_num` parameter is the block number to start the search
550 /// from (inclusive).
551 ///
552 /// The default implementation of this method makes two RPC requests: one to
553 /// [`NodeRpcClient::get_block_header_by_number`] to resolve the chain tip, and one to
554 /// [`NodeRpcClient::sync_nullifiers`] to search up to that tip.
555 async fn get_nullifier_commit_heights(
556 &self,
557 requested_nullifiers: BTreeSet<Nullifier>,
558 block_from: BlockNumber,
559 ) -> Result<BTreeMap<Nullifier, Option<BlockNumber>>, RpcError> {
560 let prefixes: Vec<u16> =
561 requested_nullifiers.iter().map(crate::note::Nullifier::prefix).collect();
562 let (chain_tip, _) = self.get_block_header_by_number(None, false).await?;
563 let retrieved_nullifiers =
564 self.sync_nullifiers(&prefixes, block_from, chain_tip.block_num()).await?;
565
566 let mut nullifiers_height = BTreeMap::new();
567 for nullifier in requested_nullifiers {
568 if let Some(update) =
569 retrieved_nullifiers.iter().find(|update| update.nullifier == nullifier)
570 {
571 nullifiers_height.insert(nullifier, Some(update.block_num));
572 } else {
573 nullifiers_height.insert(nullifier, None);
574 }
575 }
576
577 Ok(nullifiers_height)
578 }
579
580 /// Fetches public note-related data for a list of [`NoteId`] and builds [`InputNoteRecord`]s
581 /// with it. If a note is not found or it's private, it is ignored and will not be included in
582 /// the returned list.
583 ///
584 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
585 async fn get_public_note_records(
586 &self,
587 note_ids: &[NoteId],
588 current_timestamp: Option<u64>,
589 ) -> Result<Vec<InputNoteRecord>, RpcError> {
590 if note_ids.is_empty() {
591 return Ok(vec![]);
592 }
593
594 let mut public_notes = Vec::with_capacity(note_ids.len());
595 let note_details = self.get_notes_by_id(note_ids).await?;
596
597 for detail in note_details {
598 if let FetchedNote::Public(note, inclusion_proof) = detail {
599 let state = UnverifiedNoteState {
600 metadata: *note.metadata(),
601 inclusion_proof,
602 }
603 .into();
604 let attachments = note.attachments().clone();
605 let note = InputNoteRecord::new(note.into(), attachments, current_timestamp, state);
606
607 public_notes.push(note);
608 }
609 }
610
611 Ok(public_notes)
612 }
613
614 /// Given a block number, fetches the block header corresponding to that height from the node
615 /// along with the MMR proof.
616 ///
617 /// The default implementation of this method uses
618 /// [`NodeRpcClient::get_block_header_by_number`].
619 async fn get_block_header_with_proof(
620 &self,
621 block_num: BlockNumber,
622 ) -> Result<(BlockHeader, MmrProof), RpcError> {
623 let (header, proof) = self.get_block_header_by_number(Some(block_num), true).await?;
624 Ok((header, proof.ok_or(RpcError::ExpectedDataMissing(String::from("MmrProof")))?))
625 }
626
627 /// Fetches the note with the specified ID.
628 ///
629 /// The default implementation of this method uses [`NodeRpcClient::get_notes_by_id`].
630 ///
631 /// Errors:
632 /// - [`RpcError::NoteNotFound`] if the note with the specified ID is not found.
633 async fn get_note_by_id(&self, note_id: NoteId) -> Result<FetchedNote, RpcError> {
634 let notes = self.get_notes_by_id(&[note_id]).await?;
635 notes.into_iter().next().ok_or(RpcError::NoteNotFound(note_id))
636 }
637
638 /// Fetches the note script with the specified root, returning `None` if the node has no script
639 /// registered for that root.
640 ///
641 /// A returned script's root is not verified to match the requested `root`;
642 /// [`VerifyingRpcClient`] performs that check.
643 async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError>;
644
645 /// Fetches storage map updates for specified account and storage slots within a block range,
646 /// using the `/SyncStorageMaps` RPC endpoint.
647 ///
648 /// - `block_from`: The starting block number for the range (inclusive).
649 /// - `block_to`: The ending block number for the range (inclusive). The node rejects values
650 /// greater than the chain tip.
651 /// - `account_id`: The account ID for which to fetch storage map updates.
652 async fn sync_storage_maps(
653 &self,
654 block_from: BlockNumber,
655 block_to: BlockNumber,
656 account_id: AccountId,
657 ) -> Result<StorageMapInfo, RpcError>;
658
659 /// Fetches account vault updates for specified account within a block range, using the
660 /// `/SyncAccountVault` RPC endpoint.
661 ///
662 /// - `block_from`: The starting block number for the range (inclusive).
663 /// - `block_to`: The ending block number for the range (inclusive). The node rejects values
664 /// greater than the chain tip.
665 /// - `account_id`: The account ID for which to fetch storage map updates.
666 async fn sync_account_vault(
667 &self,
668 block_from: BlockNumber,
669 block_to: BlockNumber,
670 account_id: AccountId,
671 ) -> Result<AccountVaultInfo, RpcError>;
672
673 /// Fetches transaction records for specific accounts within a block range using the
674 /// `/SyncTransactions` RPC endpoint.
675 ///
676 /// - `block_from`: The starting block number for the range (inclusive).
677 /// - `block_to`: The ending block number for the range (inclusive).
678 /// - `account_ids`: The account IDs for which to fetch transactions.
679 async fn sync_transactions(
680 &self,
681 block_from: BlockNumber,
682 block_to: BlockNumber,
683 account_ids: Vec<AccountId>,
684 ) -> Result<Vec<TransactionRecord>, RpcError>;
685
686 /// Fetches the network ID of the node.
687 /// Errors:
688 /// - [`RpcError::ExpectedDataMissing`] if the note with the specified root is not found.
689 async fn get_network_id(&self) -> Result<NetworkId, RpcError>;
690
691 /// Fetches the RPC limits configured on the node.
692 ///
693 /// Implementations may cache the result internally to avoid repeated network calls.
694 async fn get_rpc_limits(&self) -> Result<RpcLimits, RpcError>;
695
696 /// Returns the RPC limits if they have been set, without fetching from the node.
697 fn has_rpc_limits(&self) -> Option<RpcLimits>;
698
699 /// Sets the RPC limits internally to be used by the client.
700 async fn set_rpc_limits(&self, limits: RpcLimits);
701
702 /// Fetches the RPC status without requiring Accept header validation.
703 ///
704 /// This is useful for diagnostics when version negotiation fails, as it allows retrieving node
705 /// information even when there's a version mismatch.
706 async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError>;
707
708 /// Fetches the status of a specific network note ID.
709 ///
710 /// This is useful for debugging when a network note fails.
711 async fn get_network_note_status(
712 &self,
713 note_id: NoteId,
714 ) -> Result<NetworkNoteStatusInfo, RpcError>;
715}
716
717/// Selects which note content [`NodeRpcClient::sync_notes_with_content`] resolves via
718/// `GetNotesById` after syncing note inclusions.
719///
720/// This enables the possibility of optimizing the call by not requesting more data than needed. For
721/// example, when a public note's details are already known (but not the attachments),
722/// `AttachmentsOnly` can be used. One example of this is when importing notes through
723/// `NoteDetails`.
724///
725/// Neither policy requests attachment content the sync response already carried in full, so a note
726/// whose attachments all fit in a single word is never fetched for its attachments alone.
727#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub enum NoteContentFetch {
729 /// Fetch the full body of every public note in the range, plus any attachment content still
730 /// missing.
731 PublicDetailsAndAttachments,
732 /// Fetch only the attachment content still missing.
733 AttachmentsOnly,
734}
735
736// RPC API ENDPOINT
737// ================================================================================================
738//
739/// RPC methods for the Miden protocol.
740#[derive(Debug, Clone, Copy)]
741pub enum RpcEndpoint {
742 Status,
743 SyncNullifiers,
744 GetAccount,
745 RegisterAccount,
746 IsAccountAllowed,
747 IsInvitationCodeValid,
748 GetBlockByNumber,
749 GetBlockHeaderByNumber,
750 GetNotesById,
751 SyncChainMmr,
752 SubmitProvenTx,
753 SubmitProvenBatch,
754 SyncNotes,
755 GetNoteScriptByRoot,
756 SyncStorageMaps,
757 SyncAccountVault,
758 SyncTransactions,
759 GetLimits,
760 GetNetworkNoteStatus,
761 GetTransactionEncryptionKey,
762}
763
764impl RpcEndpoint {
765 /// Returns the endpoint name as used in the RPC service definition.
766 pub fn proto_name(&self) -> &'static str {
767 match self {
768 RpcEndpoint::Status => "Status",
769 RpcEndpoint::SyncNullifiers => "SyncNullifiers",
770 RpcEndpoint::GetAccount => "GetAccount",
771 RpcEndpoint::RegisterAccount => "RegisterAccount",
772 RpcEndpoint::IsAccountAllowed => "IsAccountAllowed",
773 RpcEndpoint::IsInvitationCodeValid => "IsInvitationCodeValid",
774 RpcEndpoint::GetBlockByNumber => "GetBlockByNumber",
775 RpcEndpoint::GetBlockHeaderByNumber => "GetBlockHeaderByNumber",
776 RpcEndpoint::GetNotesById => "GetNotesById",
777 RpcEndpoint::SyncChainMmr => "SyncChainMmr",
778 RpcEndpoint::GetTransactionEncryptionKey => "GetTransactionEncryptionKey",
779 RpcEndpoint::SubmitProvenTx => "SubmitProvenTransaction",
780 RpcEndpoint::SubmitProvenBatch => "SubmitProvenBatch",
781 RpcEndpoint::SyncNotes => "SyncNotes",
782 RpcEndpoint::GetNoteScriptByRoot => "GetNoteScriptByRoot",
783 RpcEndpoint::SyncStorageMaps => "SyncStorageMaps",
784 RpcEndpoint::SyncAccountVault => "SyncAccountVault",
785 RpcEndpoint::SyncTransactions => "SyncTransactions",
786 RpcEndpoint::GetLimits => "GetLimits",
787 RpcEndpoint::GetNetworkNoteStatus => "GetNetworkNoteStatus",
788 }
789 }
790
791 /// Returns whether repeating the call is safe when the outcome of the previous attempt is
792 /// unknown.
793 ///
794 /// Calls that change state on the node are not: the node may have applied the change before the
795 /// response was lost, so a repeat can come back as a conflict that cannot be told apart from a
796 /// genuine one.
797 ///
798 /// The match is exhaustive on purpose, so a new endpoint has to be classified before it
799 /// compiles.
800 #[cfg(feature = "tonic")]
801 pub(crate) fn is_idempotent(self) -> bool {
802 match self {
803 RpcEndpoint::SubmitProvenTx
804 | RpcEndpoint::SubmitProvenBatch
805 | RpcEndpoint::RegisterAccount => false,
806 RpcEndpoint::Status
807 | RpcEndpoint::SyncNullifiers
808 | RpcEndpoint::GetAccount
809 | RpcEndpoint::IsAccountAllowed
810 | RpcEndpoint::IsInvitationCodeValid
811 | RpcEndpoint::GetBlockByNumber
812 | RpcEndpoint::GetBlockHeaderByNumber
813 | RpcEndpoint::GetNotesById
814 | RpcEndpoint::SyncChainMmr
815 | RpcEndpoint::SyncNotes
816 | RpcEndpoint::GetNoteScriptByRoot
817 | RpcEndpoint::SyncStorageMaps
818 | RpcEndpoint::SyncAccountVault
819 | RpcEndpoint::SyncTransactions
820 | RpcEndpoint::GetLimits
821 | RpcEndpoint::GetNetworkNoteStatus
822 | RpcEndpoint::GetTransactionEncryptionKey => true,
823 }
824 }
825}
826
827impl fmt::Display for RpcEndpoint {
828 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
829 match self {
830 RpcEndpoint::Status => write!(f, "status"),
831 RpcEndpoint::SyncNullifiers => {
832 write!(f, "sync_nullifiers")
833 },
834 RpcEndpoint::GetAccount => write!(f, "get_account"),
835 RpcEndpoint::RegisterAccount => write!(f, "register_account"),
836 RpcEndpoint::IsAccountAllowed => write!(f, "is_account_allowed"),
837 RpcEndpoint::IsInvitationCodeValid => write!(f, "is_invitation_code_valid"),
838 RpcEndpoint::GetBlockByNumber => write!(f, "get_block_by_number"),
839 RpcEndpoint::GetBlockHeaderByNumber => {
840 write!(f, "get_block_header_by_number")
841 },
842 RpcEndpoint::GetNotesById => write!(f, "get_notes_by_id"),
843 RpcEndpoint::SyncChainMmr => write!(f, "sync_chain_mmr"),
844 RpcEndpoint::GetTransactionEncryptionKey => {
845 write!(f, "get_transaction_encryption_key")
846 },
847 RpcEndpoint::SubmitProvenTx => write!(f, "submit_proven_transaction"),
848 RpcEndpoint::SubmitProvenBatch => write!(f, "submit_proven_batch"),
849 RpcEndpoint::SyncNotes => write!(f, "sync_notes"),
850 RpcEndpoint::GetNoteScriptByRoot => write!(f, "get_note_script_by_root"),
851 RpcEndpoint::SyncStorageMaps => write!(f, "sync_storage_maps"),
852 RpcEndpoint::SyncAccountVault => write!(f, "sync_account_vault"),
853 RpcEndpoint::SyncTransactions => write!(f, "sync_transactions"),
854 RpcEndpoint::GetLimits => write!(f, "get_limits"),
855 RpcEndpoint::GetNetworkNoteStatus => write!(f, "get_network_note_status"),
856 }
857 }
858}