Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ env:
# cryptic 'invalid wire type' decode errors on the client side (e.g. Digest
# fields encoded as bytes vs fixed64), or 'accept header validation failed'
# if the protocol version differs.
# Pinned to the head of the rust-sdk `encrypted-tx-inputs` branch, matching
# the miden-client git patch in Cargo.toml: its Cargo.lock pins the node rev
# that serves `GetTransactionEncryptionKey`, which the patched client calls
# on every first submission. A commit sha (not the branch name) keeps cache
# keys stable. Re-pin to the release tag once the next alpha ships.
MIDEN_CLIENT_REF: cf510e39fafa647aea8fb863ba779e92a8f7d9c2
# Pinned to the head of the rust-sdk `note_filter_script_root` branch, matching
# the miden-client git dependency in Cargo.toml: its Cargo.lock pins the node
# rev that serves `GetTransactionEncryptionKey`, which the client calls on
# every first submission. A commit sha (not the branch name) keeps cache keys
# stable. Re-pin to the release tag once the next alpha ships.
MIDEN_CLIENT_REF: e70d3df8651872a5095b55bc78774aa5ab24a1bc

jobs:
# Pre-flight: detect whether any non-docs files changed. See build.yml's
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

* [BREAKING][web] Transaction submissions now encrypt their private inputs, so the RPC operator relaying them cannot read them: only holders of the validator set's shared encryption secret can. On first submission the client fetches the validator set's transaction encryption key from the node, verifies a validator attestation for it against the validator set committed in a trusted block header (bound to the chain's genesis commitment, so an attestation cannot be replayed from another network), and caches the verified key in the store; a submission rejected for having been sealed against a retired key evicts the cached key and the next submission re-fetches. Requires a node that unseals submitted inputs — such nodes reject plaintext submissions, and older nodes reject sealed ones, so client and node must be upgraded together. ([#252](https://github.com/0xMiden/web-sdk/pull/252), client [#2341](https://github.com/0xMiden/rust-sdk/pull/2341))
* [BREAKING][web] Removed `notes.fetchPrivate({ mode: "all" })` (`WasmWebClient.fetchAllPrivateNotes`). `fetchPrivate()` now takes no arguments and always fetches incrementally from the stored pagination cursor. The full re-scan is no longer needed: historical notes for a newly tracked tag sit below the shared cursor and are now backfilled automatically during `sync()`, one tag at a time, so callers that previously reached for `mode: "all"` after adding a tag should just sync. Callers passing the option get a type error; the argument is otherwise ignored at runtime.
* [CHANGE][web] `miden-client` and `miden-client-sqlite-store` are pinned to the rust-sdk `encrypted-tx-inputs` branch ([#2341](https://github.com/0xMiden/rust-sdk/pull/2341), `32dfba8d`), ahead of the `0.16.0-alpha.1` release, pending the next alpha. Inherited upstream changes include encrypted transaction submission (see above), note-transport attachment support, a note-screener batch cache, faster historical-note retrieval, and single-account note screening: `notes.listAvailable({ account })` now screens the given account only, instead of screening every tracked account and discarding the rest, so its cost no longer grows with the number of tracked accounts. Protocol-layer versions are unchanged (`miden-protocol` / `miden-standards` / `miden-tx` at `0.16.0-alpha.4`).
* [CHANGE][web] `miden-client` and `miden-client-sqlite-store` are pinned to the rust-sdk `note_filter_script_root` branch ([#2335](https://github.com/0xMiden/rust-sdk/pull/2335), `e70d3df8`), which merges rust-sdk `next` (where encrypted transaction inputs landed, [#2341](https://github.com/0xMiden/rust-sdk/pull/2341)), ahead of the `0.16.0-alpha.1` release, pending the next alpha. Inherited upstream changes include encrypted transaction submission (see above), the store-level note script root filter (see below), note-transport attachment support, a note-screener batch cache, faster historical-note retrieval, and single-account note screening: `notes.listAvailable({ account })` now screens the given account only, instead of screening every tracked account and discarding the rest, so its cost no longer grows with the number of tracked accounts. Protocol-layer versions are unchanged (`miden-protocol` / `miden-standards` / `miden-tx` at `0.16.0-alpha.4`).

### Enhancements

* [FEATURE][web] `notes.list({ scriptRoots: [...] })` filters received notes by note script root, given as hex strings or `Word` instances (e.g. from `NoteScript.root()`). This narrows candidate notes at the store level, without loading and screening unrelated notes. `notes.listSent` returns an empty list for this query, since script roots are only tracked for received notes. ([#249](https://github.com/0xMiden/web-sdk/pull/249), client [#2335](https://github.com/0xMiden/rust-sdk/pull/2335))

## 0.16.0-alpha.1 (2026-07-19)

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ js-export-macro = { path = "crates/js-export-macro", version = "0.16.0-alpha.1"
# Cargo.lock intentionally follows the rust-sdk v0.16.0-alpha.1 release graph, including the
# Miden VM 0.25.3 family and wincode 0.5.5. Broad `cargo update` runs currently select newer
# transitive versions; validate those against this alpha before accepting lockfile changes.
miden-client = { branch = "next", default-features = false, git = "https://github.com/0xMiden/rust-sdk" }
miden-client-sqlite-store = { branch = "next", default-features = false, git = "https://github.com/0xMiden/rust-sdk" }
# TEMPORARY: pinned to the note-filter-by-script-root branch (0xMiden/rust-sdk#2335), which
# carries the store-level note script root filter and merges rust-sdk `next`. Point these back
# at `next` once #2335 lands there, and at registry versions once the next alpha is published.
miden-client = { branch = "note_filter_script_root", default-features = false, git = "https://github.com/0xMiden/rust-sdk" }
miden-client-sqlite-store = { branch = "note_filter_script_root", default-features = false, git = "https://github.com/0xMiden/rust-sdk" }

# External dependencies
async-trait = { version = "0.1" }
Expand Down
13 changes: 13 additions & 0 deletions crates/idxdb-store/src/js/notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ export async function getInputNotesFromDetailsCommitments(dbId, detailsCommitmen
logWebStoreError(err, "Failed to get input notes from details commitments");
}
}
export async function getInputNotesFromScriptRoots(dbId, scriptRoots) {
try {
const db = getDatabase(dbId);
let notes = await db.inputNotes
.where("scriptRoot")
.anyOf(scriptRoots)
.toArray();
return await processInputNotes(dbId, notes);
}
catch (err) {
logWebStoreError(err, "Failed to get input notes from script roots");
}
}
export async function getOutputNotesFromDetailsCommitments(dbId, detailsCommitments) {
try {
const db = getDatabase(dbId);
Expand Down
2 changes: 1 addition & 1 deletion crates/idxdb-store/src/js/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const V1_STORES = {
[Table.Addresses]: indexes("address", "id"),
[Table.Transactions]: indexes("id", "statusVariant"),
[Table.TransactionScripts]: indexes("scriptRoot"),
[Table.InputNotes]: indexes("detailsCommitment", "noteId", "nullifier", "stateDiscriminant", "[consumedBlockHeight+consumedTxOrder+noteId]"),
[Table.InputNotes]: indexes("detailsCommitment", "noteId", "nullifier", "scriptRoot", "stateDiscriminant", "[consumedBlockHeight+consumedTxOrder+noteId]"),
[Table.OutputNotes]: indexes("detailsCommitment", "noteId", "recipientDigest", "stateDiscriminant", "nullifier"),
[Table.NotesScripts]: indexes("scriptRoot"),
[Table.BlockchainCheckpoint]: indexes("id"),
Expand Down
6 changes: 6 additions & 0 deletions crates/idxdb-store/src/note/js_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ extern "C" {
details_commitments: Vec<String>,
) -> js_sys::Promise;

#[wasm_bindgen(js_name = getInputNotesFromScriptRoots)]
pub fn idxdb_get_input_notes_from_script_roots(
db_id: &str,
script_roots: Vec<String>,
) -> js_sys::Promise;

#[wasm_bindgen(js_name = getOutputNotes)]
pub fn idxdb_get_output_notes(db_id: &str, states: Vec<u8>) -> js_sys::Promise;

Expand Down
19 changes: 16 additions & 3 deletions crates/idxdb-store/src/note/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ use alloc::vec::Vec;

use miden_client::Word;
use miden_client::account::AccountId;
use miden_client::note::{BlockNumber, NoteDetailsCommitment, NoteScript, Nullifier};
use miden_client::note::{
BlockNumber,
NoteDetailsCommitment,
NoteScript,
NoteScriptRoot,
Nullifier,
};
use miden_client::store::{
InputNoteRecord,
InputNoteState,
Expand All @@ -26,6 +32,7 @@ use js_bindings::{
idxdb_get_input_notes_from_details_commitments,
idxdb_get_input_notes_from_ids,
idxdb_get_input_notes_from_nullifiers,
idxdb_get_input_notes_from_script_roots,
idxdb_get_note_script,
idxdb_get_output_notes,
idxdb_get_output_notes_from_details_commitments,
Expand Down Expand Up @@ -178,7 +185,8 @@ fn input_note_state_discriminants(filter: &NoteFilter) -> Option<Vec<u8>> {
NoteFilter::List(_)
| NoteFilter::Unique(_)
| NoteFilter::Nullifiers(_)
| NoteFilter::DetailsCommitments(_) => None,
| NoteFilter::DetailsCommitments(_)
| NoteFilter::ScriptRoots(_) => None,
}
}

Expand Down Expand Up @@ -223,6 +231,11 @@ impl NoteFilterExt for NoteFilter {
commitments.iter().map(NoteDetailsCommitment::to_hex).collect();
idxdb_get_input_notes_from_details_commitments(db_id, commitments_as_str)
},
NoteFilter::ScriptRoots(script_roots) => {
let script_roots_as_str: Vec<String> =
script_roots.iter().map(NoteScriptRoot::to_hex).collect();
idxdb_get_input_notes_from_script_roots(db_id, script_roots_as_str)
},
}
}

Expand Down Expand Up @@ -255,7 +268,7 @@ impl NoteFilterExt for NoteFilter {

idxdb_get_output_notes(db_id, states)
},
NoteFilter::Processing | NoteFilter::Unverified => {
NoteFilter::Processing | NoteFilter::ScriptRoots(_) | NoteFilter::Unverified => {
Promise::resolve(&JsValue::from(Array::new()))
},
NoteFilter::List(ids) => {
Expand Down
36 changes: 36 additions & 0 deletions crates/idxdb-store/src/ts/notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getInputNotes,
getInputNotesFromIds,
getInputNotesFromNullifiers,
getInputNotesFromScriptRoots,
getOutputNotes,
getOutputNotesFromIds,
getOutputNotesFromNullifiers,
Expand Down Expand Up @@ -538,6 +539,41 @@ describe("getInputNotesFromIds", () => {
});
});

// ================================================================================================
// getInputNotesFromScriptRoots
// ================================================================================================

describe("getInputNotesFromScriptRoots", () => {
it("returns notes matching the given script roots", async () => {
const dbId = await openTestDb();
await insertNote(dbId, "root-note-1", { scriptRoot: "0xroot1" });
await insertNote(dbId, "root-note-2", { scriptRoot: "0xroot1" });
await insertNote(dbId, "root-note-3", { scriptRoot: "0xroot2" });

const result = await getInputNotesFromScriptRoots(dbId, ["0xroot1"]);
expect(result).toHaveLength(2);
// createdAt holds the noteId (see insertNote)
expect(result?.map((note) => note.createdAt).sort()).toEqual([
"root-note-1",
"root-note-2",
]);

const combined = await getInputNotesFromScriptRoots(dbId, [
"0xroot1",
"0xroot2",
]);
expect(combined).toHaveLength(3);
});

it("returns empty array for unmatched script roots", async () => {
const dbId = await openTestDb();
await insertNote(dbId, "root-note-1", { scriptRoot: "0xroot1" });

const result = await getInputNotesFromScriptRoots(dbId, ["0xother"]);
expect(result).toEqual([]);
});
});

// ================================================================================================
// getInputNotesFromNullifiers
// ================================================================================================
Expand Down
16 changes: 16 additions & 0 deletions crates/idxdb-store/src/ts/notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ export async function getInputNotesFromDetailsCommitments(
}
}

export async function getInputNotesFromScriptRoots(
dbId: string,
scriptRoots: string[]
) {
try {
const db = getDatabase(dbId);
let notes = await db.inputNotes
.where("scriptRoot")
.anyOf(scriptRoots)
.toArray();
return await processInputNotes(dbId, notes);
} catch (err) {
logWebStoreError(err, "Failed to get input notes from script roots");
}
}

export async function getOutputNotesFromDetailsCommitments(
dbId: string,
detailsCommitments: string[]
Expand Down
1 change: 1 addition & 0 deletions crates/idxdb-store/src/ts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ const V1_STORES: Record<string, string> = {
"detailsCommitment",
"noteId",
"nullifier",
"scriptRoot",
"stateDiscriminant",
"[consumedBlockHeight+consumedTxOrder+noteId]"
),
Expand Down
17 changes: 17 additions & 0 deletions crates/web-client/js/__tests__/resources/notes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function makeWasm(overrides = {}) {
Processing: "Processing",
Unverified: "Unverified",
List: "List",
ScriptRoots: "ScriptRoots",
};
const filterInstance = { type: "filter" };
return {
Expand All @@ -18,6 +19,9 @@ function makeWasm(overrides = {}) {
NoteId: {
fromHex: vi.fn((hex) => ({ hex })),
},
Word: {
fromHex: vi.fn((hex) => ({ hex })),
},
NoteExportFormat: { Full: "Full" },
AccountId: {
fromHex: vi.fn((hex) => ({ hex })),
Expand Down Expand Up @@ -125,6 +129,19 @@ describe("NotesResource", () => {
expect(wasm.NoteFilter).toHaveBeenCalledWith("List", expect.any(Array));
});

it("builds NoteFilter with script roots when query.scriptRoots provided", async () => {
inner.getInputNotes.mockResolvedValue([]);
const resource = makeResource();
const wordRoot = { word: true };
await resource.list({ scriptRoots: ["0xabc", wordRoot] });
expect(wasm.Word.fromHex).toHaveBeenCalledTimes(1);
expect(wasm.Word.fromHex).toHaveBeenCalledWith("0xabc");
expect(wasm.NoteFilter).toHaveBeenCalledWith("ScriptRoots", undefined, [
{ hex: "0xabc" },
wordRoot,
]);
});

it("falls back to All when empty query object", async () => {
inner.getInputNotes.mockResolvedValue([]);
const resource = makeResource();
Expand Down
11 changes: 11 additions & 0 deletions crates/web-client/js/resources/notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ function buildNoteFilter(query, wasm) {
return new wasm.NoteFilter(wasm.NoteFilterTypes.List, noteIds);
}

if (query.scriptRoots) {
const scriptRoots = query.scriptRoots.map((root) =>
typeof root === "string" ? wasm.Word.fromHex(root) : root
);
return new wasm.NoteFilter(
wasm.NoteFilterTypes.ScriptRoots,
undefined,
scriptRoots
);
}

if (query.status) {
const statusMap = {
consumed: wasm.NoteFilterTypes.Consumed,
Expand Down
18 changes: 14 additions & 4 deletions crates/web-client/js/types/api-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,14 @@ export type NoteQuery =
| "processing"
| "unverified";
}
| { ids: (string | NoteId)[] };
| { ids: (string | NoteId)[] }
/**
* Filter received notes by note script root, given as hex strings or Word
* instances (e.g. from `NoteScript.root()`). Notes match regardless of their
* state. Only supported by `notes.list`; `notes.listSent` returns an empty
* list for this query.
*/
| { scriptRoots: (string | Word)[] };

/** Options for standalone note creation utilities. */
export interface NoteOptions {
Expand Down Expand Up @@ -817,9 +824,10 @@ export interface TransactionsResource {

export interface NotesResource {
/**
* List received (input) notes, optionally filtered by status or IDs.
* List received (input) notes, optionally filtered by status, IDs, or note
* script roots.
*
* @param query - Optional filter by note status or note IDs.
* @param query - Optional filter by note status, note IDs, or script roots.
*/
list(query?: NoteQuery): Promise<InputNoteRecord[]>;
/**
Expand All @@ -830,7 +838,9 @@ export interface NotesResource {
get(noteId: NoteInput): Promise<InputNoteRecord | null>;

/**
* List sent (output) notes, optionally filtered by status or IDs.
* List sent (output) notes, optionally filtered by status or IDs. A script
* root query returns an empty list, since script roots are only tracked for
* received notes.
*
* @param query - Optional filter by note status or note IDs.
*/
Expand Down
Loading
Loading