1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
//! RPC Client for Web Applications
//!
//! This module provides a WebAssembly-compatible RPC client for interacting with Miden nodes.
use alloc::collections::BTreeSet;
use alloc::sync::Arc;
use alloc::vec::Vec;
use js_export_macro::js_export;
use miden_client::block::BlockNumber;
use miden_client::builder::DEFAULT_GRPC_TIMEOUT_MS;
use miden_client::note::{NoteId as NativeNoteId, Nullifier};
use miden_client::rpc::domain::account::{GetAccountRequest, StorageMapFetch, VaultFetch};
use miden_client::rpc::domain::note::FetchedNote as NativeFetchedNote;
use miden_client::rpc::{AccountStateAt, GrpcClient, NodeRpcClient};
use note::FetchedNote;
use crate::js_error_with_context;
use crate::models::account_id::AccountId;
use crate::models::account_proof::AccountProof;
use crate::models::account_storage_requirements::AccountStorageRequirements;
use crate::models::block_header::BlockHeader;
use crate::models::endpoint::Endpoint;
use crate::models::fetched_account::FetchedAccount;
use crate::models::network_note_status::NetworkNoteStatusInfo;
use crate::models::note_id::NoteId;
use crate::models::note_script::NoteScript;
use crate::models::note_sync::NoteSyncInfo;
use crate::models::note_tag::NoteTag;
use crate::models::storage_map_info::StorageMapInfo;
use crate::models::word::Word;
use crate::platform::JsErr;
mod note;
/// RPC Client for interacting with Miden nodes directly.
#[js_export]
pub struct RpcClient {
inner: Arc<dyn NodeRpcClient>,
}
#[js_export]
impl RpcClient {
/// Creates a new RPC client instance.
///
/// @param endpoint - Endpoint to connect to.
#[js_export(constructor)]
pub fn new(endpoint: Endpoint) -> Result<RpcClient, JsErr> {
let rpc_client = Arc::new(GrpcClient::new(&endpoint.into(), DEFAULT_GRPC_TIMEOUT_MS));
Ok(RpcClient { inner: rpc_client })
}
/// Fetches notes by their IDs from the connected Miden node.
///
/// @param note_ids - Array of [`NoteId`] objects to fetch
/// @returns Promise that resolves to different data depending on the note type:
/// - Private notes: Returns the `noteHeader`, and the `inclusionProof`. The `note` field will
/// be `null`.
/// - Public notes: Returns the full `note` with `inclusionProof`, alongside its header.
#[allow(clippy::doc_markdown)]
#[js_export(js_name = "getNotesById")]
pub async fn get_notes_by_id(&self, note_ids: Vec<NoteId>) -> Result<Vec<FetchedNote>, JsErr> {
let native_note_ids: Vec<NativeNoteId> =
note_ids.into_iter().map(NativeNoteId::from).collect();
let fetched_notes = self
.inner
.get_notes_by_id(&native_note_ids)
.await
.map_err(|err| js_error_with_context(err, "failed to get notes by ID"))?;
let web_notes: Vec<FetchedNote> = fetched_notes
.into_iter()
.map(|native_note| match native_note {
// 0.15 surface: private fetched notes carry the note ID, metadata, and
// attachment content alongside the inclusion proof (the body stays off-chain).
NativeFetchedNote::Private(note_id, metadata, attachments, inclusion_proof) => {
let attachments = attachments.iter().map(Into::into).collect();
FetchedNote::with_attachments(
note_id.into(),
metadata.into(),
inclusion_proof.into(),
None,
attachments,
)
},
NativeFetchedNote::Public(note, inclusion_proof) => {
let note_id = note.id();
let metadata = *note.metadata();
let attachments = note.attachments().iter().map(Into::into).collect();
FetchedNote::with_attachments(
note_id.into(),
metadata.into(),
inclusion_proof.into(),
Some(note.into()),
attachments,
)
},
})
.collect();
Ok(web_notes)
}
/// Fetches a note script by its root hash from the connected Miden node.
///
/// @param script_root - The root hash of the note script to fetch.
/// @returns Promise that resolves to the `NoteScript`, or `undefined` if the node has no
/// script for that root.
#[allow(clippy::doc_markdown)]
#[js_export(js_name = "getNoteScriptByRoot")]
pub async fn get_note_script_by_root(
&self,
script_root: &Word,
) -> Result<Option<NoteScript>, JsErr> {
let native_script_root = script_root.into();
// 0.15 surface: the node returns `Option<NoteScript>` — `None` when the script root is
// unknown — rather than erroring. Surface that as `Option<NoteScript>` on the JS side.
let note_script = self
.inner
.get_note_script_by_root(native_script_root)
.await
.map_err(|err| js_error_with_context(err, "failed to get note script by root"))?;
Ok(note_script.map(Into::into))
}
/// Fetches a block header by number. When `block_num` is undefined, returns the latest header.
///
/// @param `block_num` - Optional block number. When `undefined`, returns the latest header.
/// @param `include_mmr_proof` - When `true`, includes the MMR proof in the response. Defaults
/// to `false` when `undefined`.
#[js_export(js_name = "getBlockHeaderByNumber")]
pub async fn get_block_header_by_number(
&self,
block_num: Option<u32>,
include_mmr_proof: Option<bool>,
) -> Result<BlockHeader, JsErr> {
let native_block_num = block_num.map(BlockNumber::from);
let (header, _proof) = self
.inner
.get_block_header_by_number(native_block_num, include_mmr_proof.unwrap_or(false))
.await
.map_err(|err| js_error_with_context(err, "failed to get block header by number"))?;
Ok(header.into())
}
/// Fetches account details for a specific account ID.
#[js_export(js_name = "getAccountDetails")]
pub async fn get_account_details(
&self,
account_id: &AccountId,
) -> Result<FetchedAccount, JsErr> {
let native_id: miden_client::account::AccountId = account_id.into();
// `get_account_details` returns only `Option<Account>`, without the commitment or block
// height. Issue the underlying `get_account` request directly (full storage maps + vault)
// so `FetchedAccount` can report the account commitment and last block height.
let request = GetAccountRequest::new()
.with_storage(StorageMapFetch::All)
.with_vault(VaultFetch::Always);
let (block_num, proof) = self
.inner
.get_account(native_id, request)
.await
.map_err(|err| js_error_with_context(err, "failed to get account details"))?;
FetchedAccount::from_proof(block_num, proof)
}
/// Fetches an account proof from the node.
///
/// This is a lighter-weight alternative to `getAccountDetails` that makes a single RPC call
/// and returns the account proof alongside the account header, storage slot values, and
/// account code without reconstructing the full account state.
///
/// For private accounts, the proof is returned but account details will not be available
/// since they are not stored on-chain.
///
/// Useful for reading storage slot values (e.g., faucet metadata) or specific storage map
/// entries without the overhead of fetching the complete account with all vault assets and
/// storage map entries.
///
/// @param `account_id` - The account to fetch the proof for.
/// @param `storage_requirements` - Optional storage requirements specifying which storage
/// maps and keys to include. When `undefined`, no storage map data is requested.
/// @param `block_num` - Optional block number to fetch the account state at. When `undefined`,
/// fetches the latest state (chain tip).
/// @param `known_vault_commitment` - Optional known vault commitment. When provided,
/// vault data is returned only if the account's current vault root differs from this
/// value. Use `Word.new([0, 0, 0, 0])` to always fetch. When `undefined`, vault data
/// is not requested.
#[js_export(js_name = "getAccountProof")]
pub async fn get_account_proof(
&self,
account_id: &AccountId,
storage_requirements: Option<AccountStorageRequirements>,
block_num: Option<u32>,
known_vault_commitment: Option<Word>,
) -> Result<AccountProof, JsErr> {
let native_id: miden_client::account::AccountId = account_id.into();
// Storage requirements are wrapped in a `StorageMapFetch` policy: named slots map to
// `Slots(..)`, and their absence maps to `Skip` (request only the storage header).
let storage_fetch = match storage_requirements {
Some(reqs) => StorageMapFetch::Slots(reqs.into()),
None => StorageMapFetch::Skip,
};
let account_state = match block_num {
Some(num) => AccountStateAt::Block(BlockNumber::from(num)),
None => AccountStateAt::ChainTip,
};
// 0.15 surface: `get_account_proof` was renamed/reshaped to `get_account` taking a
// `GetAccountRequest` builder. The semantics carry over 1:1 — storage requirements,
// a target block, and an optional known vault commitment to short-circuit re-sending
// unchanged vault data. `known_code` is left at its `None` default, so the node always
// re-sends the account code, matching the previous call.
let vault = match known_vault_commitment {
Some(commitment) => VaultFetch::IfChangedFrom(commitment.into()),
None => VaultFetch::Skip,
};
let request = GetAccountRequest::new()
.with_storage(storage_fetch)
.at(account_state)
.with_vault(vault);
let (block_num, proof) = self
.inner
.get_account(native_id, request)
.await
.map_err(|err| js_error_with_context(err, "failed to get account proof"))?;
Ok(AccountProof::new(proof, block_num))
}
/// Syncs storage map updates for an account within a block range.
///
/// This is used when `AccountProof.hasStorageMapTooManyEntries()` returns `true` for a
/// slot, indicating the storage map was too large to return inline. This endpoint fetches
/// the full storage map data with pagination support.
///
/// @param `block_from` - The starting block number.
/// @param `block_to` - Optional ending block number. When `undefined`, the current chain tip
/// is fetched with an extra RPC call and used as the bound (the node rejects values greater
/// than the tip).
/// @param `account_id` - The account to sync storage maps for.
#[js_export(js_name = "syncStorageMaps")]
pub async fn sync_storage_maps(
&self,
block_from: u32,
block_to: Option<u32>,
account_id: &AccountId,
) -> Result<StorageMapInfo, JsErr> {
let native_id: miden_client::account::AccountId = account_id.into();
let block_from = BlockNumber::from(block_from);
let block_to = if let Some(block_to) = block_to {
BlockNumber::from(block_to)
} else {
let (chain_tip, _) = self
.inner
.get_block_header_by_number(None, false)
.await
.map_err(|err| js_error_with_context(err, "failed to fetch chain tip"))?;
chain_tip.block_num()
};
let info = self
.inner
.sync_storage_maps(block_from, block_to, native_id)
.await
.map_err(|err| js_error_with_context(err, "failed to sync storage maps"))?;
Ok(info.into())
}
/// Fetches notes matching the provided tags from the node.
#[js_export(js_name = "syncNotes")]
pub async fn sync_notes(
&self,
block_from: u32,
block_to: u32,
note_tags: Vec<NoteTag>,
) -> Result<NoteSyncInfo, JsErr> {
let mut tags = BTreeSet::new();
for tag in note_tags {
tags.insert(tag.into());
}
let block_from = BlockNumber::from(block_from);
let block_to = BlockNumber::from(block_to);
let blocks = self
.inner
.sync_notes(block_from, block_to, &tags)
.await
.map_err(|err| js_error_with_context(err, "failed to sync notes"))?;
Ok(NoteSyncInfo::new(blocks, block_to))
}
/// Fetches the processing status of a network note by its ID.
///
/// Returns information about the note's current status in the network,
/// including whether it is pending, processed, discarded, or committed,
/// along with error details and attempt count.
///
/// @param `note_id` - The ID of the note to query.
/// @returns Promise that resolves to a `NetworkNoteStatusInfo` object.
#[js_export(js_name = "getNetworkNoteStatus")]
pub async fn get_network_note_status(
&self,
note_id: &NoteId,
) -> Result<NetworkNoteStatusInfo, JsErr> {
let native_note_id: NativeNoteId = note_id.into();
let status_info = self
.inner
.get_network_note_status(native_note_id)
.await
.map_err(|err| js_error_with_context(err, "failed to get network note status"))?;
Ok(status_info.into())
}
// TODO: This can be generalized to retrieve multiple nullifiers
/// Fetches the block height at which a nullifier was committed, if any.
#[js_export(js_name = "getNullifierCommitHeight")]
pub async fn get_nullifier_commit_height(
&self,
nullifier: &Word,
block_num: u32,
) -> Result<Option<u32>, JsErr> {
let native_word: miden_client::Word = nullifier.into();
// TODO: nullifier JS binding
let nullifier = Nullifier::from_raw(native_word);
let block_num = BlockNumber::from(block_num);
let mut requested_nullifiers = BTreeSet::new();
requested_nullifiers.insert(nullifier);
let height = self
.inner
.get_nullifier_commit_heights(requested_nullifiers, block_num)
.await
.map_err(|err| js_error_with_context(err, "failed to get nullifier commit height"))?
.into_iter()
.next()
.and_then(|(_, height)| height);
Ok(height.map(|height| height.as_u32()))
}
}