diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79b8a88..18f77eab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,8 +24,14 @@ env: # to in Cargo.lock: that ref's own Cargo.lock pins the node rev the client is # built against. A commit sha (not the branch name) keeps cache keys stable. # Re-pin whenever Cargo.lock moves, and to the release tag once one ships. + # A `Client PR:` marker overrides this for the test-node jobs + # (resolve-client-ref); this value stays the fallback for pushes to next. # v0.17.0-rc.1 MIDEN_CLIENT_REF: 2fb20aca0869034dd26aa35473dbfb14fa017083 + # Sequencer 0.17.0-rc.2 will not start without a batch-builder wallet. The + # binary reads this variable; the node repo's local runner uses the same + # placeholder. A node that does not know it ignores the variable. + MIDEN_NODE_BATCH_BUILDER_WALLET_ACCOUNT_ID: "0xcc0000000000dd010000ee000000ff" jobs: # Pre-flight: detect whether any non-docs files changed. See build.yml's @@ -826,11 +832,45 @@ jobs: # the node rev pinned in its Cargo.lock, and builds the gen-genesis fixture # generator from its test-node-genesis crate. This job pre-builds both # pieces so the consumer jobs only download and start them. + # The published pin above tracks Cargo.lock. A PR that builds against an + # unreleased client via `Client PR:` must start the node from that same + # commit, or the block's protocol-config commitment is not the one the + # client stored. + resolve-client-ref: + name: Resolve test node ref + runs-on: ubuntu-24.04 + outputs: + ref: ${{ steps.out.outputs.ref }} + steps: + - id: out + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + FALLBACK: ${{ env.MIDEN_CLIENT_REF }} + run: | + set -euo pipefail + ref="$FALLBACK" + if [ -n "${PR_NUMBER:-}" ]; then + body=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.body // ""') + marker=$(printf '%s\n' "$body" | grep -ioE '^[[:space:]]*Client PR:[[:space:]]*([0-9a-zA-Z._-]+/[0-9a-zA-Z._-]+)?#[0-9]+' | head -1 || true) + if [ -n "$marker" ]; then + repo=$(printf '%s\n' "$marker" | grep -oE '[0-9a-zA-Z._-]+/[0-9a-zA-Z._-]+' | head -1 || true) + repo=${repo:-0xMiden/rust-sdk} + num=$(printf '%s\n' "$marker" | grep -oE '[0-9]+$') + ref=$(gh api "repos/${repo}/pulls/${num}" --jq '.head.sha') + echo "linked client ${repo}#${num} -> ${ref}" + fi + fi + echo "ref=${ref}" >> "$GITHUB_OUTPUT" + echo "test node rust-sdk ref: ${ref}" + build-test-node: name: Build test node runs-on: warp-ubuntu-latest-x64-8x - needs: [changes] + needs: [changes, resolve-client-ref] if: needs.changes.outputs.non_docs == 'true' + env: + MIDEN_CLIENT_REF: ${{ needs.resolve-client-ref.outputs.ref }} steps: - name: Checkout miden-client for test infra uses: actions/checkout@v6 @@ -901,9 +941,10 @@ jobs: # before the split. name: Web client tests (Node.js) runs-on: warp-ubuntu-latest-x64-8x - needs: [build-test-node] + needs: [build-test-node, resolve-client-ref] env: SCCACHE_GHA_ENABLED: "true" + MIDEN_CLIENT_REF: ${{ needs.resolve-client-ref.outputs.ref }} steps: - uses: actions/checkout@v6 # Auto-patch miden-client dep against any "Client PR: #N" marker in @@ -990,7 +1031,9 @@ jobs: # with the native fee asset, so run the node fee-free. Remove once the # tests draw fee balances from the genesis funder wallets. MIDEN_VERIFICATION_BASE_FEE: "0" - run: ./scripts/start-test-node.sh --background + run: | + ../scripts/patch-linked-test-node.sh ./scripts/start-test-node.sh + ./scripts/start-test-node.sh --background # Since 0.17 the fee asset lives in the protocol configuration rather than # the block header, so a client is told which faucet issues it instead of # reading it off a block. Each genesis mints a fresh one, so it is read @@ -1040,7 +1083,9 @@ jobs: # for the rationale and the per-shard file lists. name: Integration tests (${{ matrix.project }}) runs-on: warp-ubuntu-latest-x64-8x - needs: [changes, build-web-client-dist-folder, build-test-node] + needs: [changes, build-web-client-dist-folder, build-test-node, resolve-client-ref] + env: + MIDEN_CLIENT_REF: ${{ needs.resolve-client-ref.outputs.ref }} if: needs.changes.outputs.non_docs == 'true' strategy: fail-fast: false @@ -1103,7 +1148,9 @@ jobs: # with the native fee asset, so run the node fee-free. Remove once the # tests draw fee balances from the genesis funder wallets. MIDEN_VERIFICATION_BASE_FEE: "0" - run: ./scripts/start-test-node.sh --background + run: | + ../scripts/patch-linked-test-node.sh ./scripts/start-test-node.sh + ./scripts/start-test-node.sh --background # Since 0.17 the fee asset lives in the protocol configuration rather than # the block header, so a client is told which faucet issues it instead of # reading it off a block. Each genesis mints a fresh one, so it is read @@ -1154,7 +1201,9 @@ jobs: integration-tests-remote-prover-web-client: name: Integration tests for remote prover runs-on: warp-ubuntu-latest-x64-8x - needs: [changes, build-web-client-dist-folder, build-test-node] + needs: [changes, build-web-client-dist-folder, build-test-node, resolve-client-ref] + env: + MIDEN_CLIENT_REF: ${{ needs.resolve-client-ref.outputs.ref }} # Remote prover tests are slow (~14 min) and rarely affected by typical PR # changes. Skip on PRs unless explicitly opted in via the # 'run-remote-prover' label; always run on push to main/next. @@ -1217,7 +1266,9 @@ jobs: # with the native fee asset, so run the node fee-free. Remove once the # tests draw fee balances from the genesis funder wallets. MIDEN_VERIFICATION_BASE_FEE: "0" - run: ./scripts/start-test-node.sh --background + run: | + ../scripts/patch-linked-test-node.sh ./scripts/start-test-node.sh + ./scripts/start-test-node.sh --background # Since 0.17 the fee asset lives in the protocol configuration rather than # the block header, so a client is told which faucet issues it instead of # reading it off a block. Each genesis mints a fresh one, so it is read diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2d4f94..6e7fa105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [FEATURE][web] Exported `isConsumableNow(record, accountIdHex?)`, the rule `notes.listAvailable` and `transactions.consumeAll` apply, for code that reads the low-level client directly. ([#170](https://github.com/0xMiden/web-sdk/pull/170)) ### Changes +* [BREAKING][web] `AccountFile` and `NoteFile` bytes are the protocol 0.17.0-rc.6 protobuf file format. `serialize()` and `deserialize()` keep the same shape, but bytes written by 0.17.0-rc.1 do not decode, and the other way around. The types moved onto `miden-objects` in [rust-sdk#2594](https://github.com/0xMiden/rust-sdk/pull/2594), which dropped the old `Serializable` codec. ([#414](https://github.com/0xMiden/web-sdk/pull/414)) * [BREAKING][behavior][web] `notes.listAvailable({ account })` and `transactions.consumeAll({ account })` no longer return or consume block-locked notes. Both keep only notes the client's note screener reports as consumable by `account` at the last synced block, so `consumeAll` no longer fails a whole transaction on one time-locked note, and its `consumed`/`remaining` counts follow. Use `notes.listConsumable()` to also see block-locked notes. ([#170](https://github.com/0xMiden/web-sdk/pull/170)) * [BREAKING][behavior][react] `useNotes().consumableNotes` (and `consumableNoteSummaries`), `useWaitForNotes().waitForConsumableNotes` and `useSessionAccount`'s funding poll now apply the same rule: block-locked notes are not reported as consumable, are not waited on as if they were, and are no longer put into a consume transaction that the whole account's funding step would fail on. ([#170](https://github.com/0xMiden/web-sdk/pull/170)) ## 0.17.0-rc.1 (2026-09-21) diff --git a/crates/web-client/src/import.rs b/crates/web-client/src/import.rs index 048555f6..cf3e5421 100644 --- a/crates/web-client/src/import.rs +++ b/crates/web-client/src/import.rs @@ -1,7 +1,7 @@ use js_export_macro::js_export; use miden_client::account::{AccountFile as NativeAccountFile, AccountId as NativeAccountId}; use miden_client::keystore::Keystore; -use miden_client::notes::NoteFile as NativeNoteFile; +use miden_client::note::NoteFile as NativeNoteFile; #[cfg(feature = "browser")] use wasm_bindgen::prelude::*; @@ -23,9 +23,8 @@ impl WebClient { let mut guard = self.get_mut_inner().await; let client = guard.as_mut().ok_or_else(|| from_str_err("Client not initialized"))?; let account_data: NativeAccountFile = account_file.into(); - let account_id = account_data.account.id().to_string(); - - let NativeAccountFile { account, auth_secret_keys } = account_data; + let account_id = account_data.account().id().to_string(); + let (account, auth_secret_keys) = account_data.into_parts(); client .add_account(&account.clone(), false) diff --git a/crates/web-client/src/models/account_file.rs b/crates/web-client/src/models/account_file.rs index d167e18e..6c03203f 100644 --- a/crates/web-client/src/models/account_file.rs +++ b/crates/web-client/src/models/account_file.rs @@ -1,10 +1,10 @@ use js_export_macro::js_export; use miden_client::account::AccountFile as NativeAccountFile; +use crate::js_error_with_context; use crate::models::account::Account; use crate::models::account_id::AccountId; -use crate::platform::{JsBytes, JsErr}; -use crate::utils::{deserialize_from_bytes, serialize_to_bytes}; +use crate::platform::{JsBytes, JsErr, bytes_to_js, js_to_bytes}; #[derive(Debug, Clone)] #[js_export] @@ -15,28 +15,33 @@ impl AccountFile { /// Returns the account ID. #[js_export(js_name = "accountId")] pub fn account_id(&self) -> AccountId { - self.0.account.id().into() + self.0.account().id().into() } /// Returns the account data. pub fn account(&self) -> Account { - self.0.account.clone().into() + self.0.account().clone().into() } /// Returns the number of auth secret keys included. #[js_export(js_name = "authSecretKeyCount")] pub fn auth_secret_key_count(&self) -> usize { - self.0.auth_secret_keys.len() + self.0.auth_secret_keys().len() } - /// Serializes the `AccountFile` into a byte array + /// Encodes this file as protobuf account-file bytes. + /// + /// Bytes written by web-sdk 0.17.0-rc.1 used the old `Serializable` codec and do not decode. pub fn serialize(&self) -> JsBytes { - serialize_to_bytes(&self.0) + bytes_to_js(&self.0.to_bytes()) } - /// Deserializes a byte array into an `AccountFile` + /// Decodes protobuf account-file bytes. + /// + /// Rejects bytes produced by web-sdk 0.17.0-rc.1. pub fn deserialize(bytes: JsBytes) -> Result { - let native_account_file: NativeAccountFile = deserialize_from_bytes(&bytes)?; + let native_account_file = NativeAccountFile::try_from_bytes(&js_to_bytes(&bytes)) + .map_err(|err| js_error_with_context(err, "account file deserialization failed"))?; Ok(Self(native_account_file)) } } diff --git a/crates/web-client/src/models/note_file.rs b/crates/web-client/src/models/note_file.rs index b8033f8c..631cba23 100644 --- a/crates/web-client/src/models/note_file.rs +++ b/crates/web-client/src/models/note_file.rs @@ -2,12 +2,11 @@ use js_export_macro::js_export; use miden_client::block::BlockNumber as NativeBlockNumber; use miden_client::note::{ NoteDetails as NativeNoteDetails, + NoteFile as NativeNoteFile, NoteId as NativeNoteId, NoteSyncHint as NativeNoteSyncHint, NoteTag as NativeNoteTag, }; -use miden_client::notes::NoteFile as NativeNoteFile; -use miden_client::{Deserializable, Serializable}; #[cfg(feature = "nodejs")] use napi_derive::napi; #[cfg(feature = "browser")] @@ -118,19 +117,20 @@ impl NoteFile { } } - /// Turn a notefile into its byte representation. + /// Encodes this file as protobuf note-file bytes. + /// + /// Bytes written by web-sdk 0.17.0-rc.1 used the old `Serializable` codec and do not decode. #[js_export(js_name = serialize)] pub fn serialize(&self) -> Vec { - let mut buffer = vec![]; - self.inner.write_into(&mut buffer); - buffer + self.inner.to_bytes() } - /// Given a valid byte representation of a `NoteFile`, - /// return it as a struct. + /// Decodes protobuf note-file bytes. + /// + /// Rejects bytes produced by web-sdk 0.17.0-rc.1. #[js_export(js_name = deserialize)] pub fn deserialize(bytes: &[u8]) -> Result { - let deserialized = NativeNoteFile::read_from_bytes(bytes) + let deserialized = NativeNoteFile::try_from_bytes(bytes) .map_err(|err| js_error_with_context(err, "notefile deserialization failed"))?; Ok(Self { inner: deserialized }) } diff --git a/scripts/patch-linked-test-node.sh b/scripts/patch-linked-test-node.sh new file mode 100755 index 00000000..f4ba39e0 --- /dev/null +++ b/scripts/patch-linked-test-node.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# The rust-sdk start script checked out for a linked client PR can predate the +# node it installs. Node 0.17.0-rc.2 will not start a sequencer until +# fee-collector.mac exists in the node data directory and that account has been +# deployed. Insert those two commands when the script does not already have them. +set -euo pipefail + +target="${1:?path to start-test-node.sh}" + +if grep -q 'fee-collector create' "$target"; then + exit 0 +fi + +python3 - "$target" <<'PY' +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +text = path.read_text() +needle = ( + "# Let the validator bind before the sequencer starts producing blocks against it.\n" + "sleep 2\n" + "start sequencer" +) +insert = ( + "# Let the validator bind before the sequencer starts producing blocks against it.\n" + "sleep 2\n" + "if [ ! -f \"$DATA/node/fee-collector.mac\" ]; then\n" + " \"$BIN/miden-node\" fee-collector create --data-directory \"$DATA/node\"\n" + "fi\n" + "\"$BIN/miden-node\" fee-collector deploy " + "--data-directory \"$DATA/node\" --validator.url \"http://$VALIDATOR\"\n" + "start sequencer" +) +if needle not in text: + raise SystemExit(f"could not find the sequencer startup in {path}") +path.write_text(text.replace(needle, insert, 1)) +PY