From eee43f09bd721a0cc193318ba41b325d49cb4ed2 Mon Sep 17 00:00:00 2001 From: Wiktor Starczewski Date: Tue, 22 Sep 2026 17:50:47 +0200 Subject: [PATCH 1/5] fix(web-client): follow the rc.6 account and note file format AccountFile fields are private and both file types encode as protobuf. NoteFile now comes from miden_client::note. --- CHANGELOG.md | 1 + crates/web-client/src/import.rs | 7 +++--- crates/web-client/src/models/account_file.rs | 23 ++++++++++++-------- crates/web-client/src/models/note_file.rs | 18 +++++++-------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2d4f94..69b0ed3c 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. * [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 }) } From 6d31a807b53af02ec5968a8238843c8eaffecce2 Mon Sep 17 00:00:00 2001 From: Wiktor Starczewski Date: Tue, 22 Sep 2026 17:51:19 +0200 Subject: [PATCH 2/5] docs: cite the web-sdk PR on the rc.6 file-format note --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b0ed3c..6e7fa105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +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. +* [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) From 30052bec232883a0dd7c30f411b69e5dca2a8173 Mon Sep 17 00:00:00 2001 From: Wiktor Starczewski Date: Tue, 22 Sep 2026 18:17:41 +0200 Subject: [PATCH 3/5] ci: start the test node from the linked client commit The node stayed on rust-sdk v0.17.0-rc.1 while the client was built from the Client PR marker, so execution looked up a protocol config the store did not have. The rc.2 sequencer also reads its batch-builder wallet from the environment, which older nodes ignore. --- .github/workflows/test.yml | 53 +++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79b8a88..e3d0f0eb 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 @@ -1040,7 +1081,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 @@ -1154,7 +1197,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. From 80f39fad5bc9a814f781fe1ff0efc1c6897fb520 Mon Sep 17 00:00:00 2001 From: Wiktor Starczewski Date: Tue, 22 Sep 2026 18:42:18 +0200 Subject: [PATCH 4/5] ci: deploy a fee collector before the rc.2 sequencer starts Node 0.17.0-rc.2 exits when fee-collector.mac is missing. The linked rust-sdk start script does not create or deploy that account. --- .github/workflows/test.yml | 12 +++++++--- scripts/patch-linked-test-node.sh | 38 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100755 scripts/patch-linked-test-node.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3d0f0eb..18f77eab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1031,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 @@ -1146,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 @@ -1262,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/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 From bee4cae65cc222a4d193ee6601987ca627e03142 Mon Sep 17 00:00:00 2001 From: igamigo Date: Wed, 23 Sep 2026 19:23:56 -0300 Subject: [PATCH 5/5] feat: integrate allowlist and prepare `0.17.0-rc.2` release (#419) --- .github/workflows/test.yml | 4 +- CHANGELOG.md | 6 +- Cargo.lock | 79 ++++---- Cargo.toml | 12 +- crates/idxdb-store/src/sync/mod.rs | 17 +- crates/web-client/README.md | 30 ++- crates/web-client/js/client.js | 34 ++-- crates/web-client/js/index.js | 15 +- crates/web-client/js/resources/accounts.js | 42 ++++ crates/web-client/js/types/api-types.d.ts | 64 +++++- crates/web-client/package.json | 2 +- crates/web-client/playwright.config.ts | 1 + crates/web-client/src/account.rs | 52 +++++ crates/web-client/src/lib.rs | 185 +++++++++--------- crates/web-client/src/mock.rs | 26 ++- crates/web-client/src/rpc_client/mod.rs | 38 ++++ crates/web-client/test/allowlist.test.ts | 67 +++++++ .../test/miden_client_api.node.test.ts | 26 +++ .../web-client/test/miden_client_api.test.ts | 85 ++++++-- .../src/web-client/library/allowlist.md | 97 +++++++++ packages/adapter/all/package.json | 2 +- packages/adapter/base/package.json | 4 +- packages/adapter/miden/package.json | 4 +- packages/adapter/react/package.json | 6 +- packages/adapter/reactui/package.json | 2 +- packages/create/package.json | 2 +- packages/node-sdk-darwin-arm64/package.json | 2 +- packages/node-sdk-darwin-x64/package.json | 2 +- packages/node-sdk-linux-x64-gnu/package.json | 2 +- packages/para/core/package.json | 4 +- packages/para/create/package.json | 2 +- packages/para/react/package.json | 8 +- packages/react-sdk/README.md | 10 +- .../react-sdk/examples/wallet/package.json | 12 +- packages/react-sdk/package.json | 4 +- packages/react-sdk/src/types/index.ts | 6 +- packages/telemetry-otel/package.json | 4 +- packages/telemetry-sentry/package.json | 4 +- packages/turnkey/core/package.json | 4 +- packages/turnkey/create/package.json | 2 +- packages/turnkey/react/package.json | 8 +- packages/vite-plugin/package.json | 2 +- 42 files changed, 715 insertions(+), 263 deletions(-) create mode 100644 crates/web-client/test/allowlist.test.ts create mode 100644 docs/external/src/web-client/library/allowlist.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18f77eab..5fa15efc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,8 @@ env: # 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 + # v0.17.0-rc.2 + MIDEN_CLIENT_REF: 0607abd66f9e9ae13c49da0857ad3f84f8e5fbcb # 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e7fa105..76a905f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,18 @@ # Changelog -## 0.17.0 (TBD) +## 0.17.0-rc.2 (TBD) ### Enhancements +* [FEATURE][web] Added `accounts.register({ account, invitationCode })` and `accounts.isAllowed(account)` for networks that enforce an account allowlist, where an account's first transaction creates it on chain only once the account is registered. `register` binds an invitation code to a tracked, not yet deployed account that is not a network account; because a registration consumes the code, it asks the node first and fails with code `ACCOUNT_ALREADY_ALLOWED` (code kept) for an account the node already allows, which is every account on a network without an allowlist. A submission that would create an account the network does not accept now fails with `ACCOUNT_NOT_ALLOWLISTED` before anything is proven or sent. The node's own rejections carry `INVITATION_NOT_FOUND`, `ALREADY_REGISTERED` or `INVALID_REGISTRATION_REQUEST`. When the network funds registered accounts, `register` returns once the funding note is committed, and consuming that note after the next `sync()` is what creates the account. `WebClient.registerAccount` / `isAccountAllowed` expose the same calls on the low-level client, and `RpcClient.registerAccount` / `isAccountAllowed` send the node requests as given, without the tracked-account checks. Requires a 0.17.0-rc.2 node ([rust-sdk#2545](https://github.com/0xMiden/rust-sdk/pull/2545), [rust-sdk#2550](https://github.com/0xMiden/rust-sdk/pull/2550)) ([#414](https://github.com/0xMiden/web-sdk/pull/414)). * [FEATURE][web] `compile.component({ code, libraries })` links the modules a component imports (e.g. auth libraries), closing the gap that forced consumers onto the low-level `createCodeBuilder()`. Each `{ namespace, code }` entry is linked as a source module with `linkModule`, the same sequence a raw code builder would run, so a component already deployed that way keeps its code commitment. ([#170](https://github.com/0xMiden/web-sdk/pull/170)) * [FEATURE][web] Added `notes.listConsumable({ account? })`, returning `ConsumableNoteRecord[]` with each note's `noteConsumability()` kept, so callers can tell notes consumable now from block-locked ones (`consumableAfterBlock`). Omit `account` to list for every tracked account. ([#170](https://github.com/0xMiden/web-sdk/pull/170)) * [FEATURE][web] Added `NoteConsumptionStatus.isConsumableNow()`, which answers what `consumableAfterBlock()` cannot: that accessor returns nothing both for a note consumable now and for one that can never be consumed. ([#170](https://github.com/0xMiden/web-sdk/pull/170)) * [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 +* [CHANGE][web] Upgraded `miden-client` to 0.17.0-rc.2, which adopts protocol 0.17.0-rc.6. Requires a 0.17.0-rc.2 node ([#414](https://github.com/0xMiden/web-sdk/pull/414)). +* [CHANGE][web] The client receives the chain's protocol configuration from the node when it syncs, so `ClientOptions.feeFaucetId` is no longer required and no longer builds one. Execution and note screening resolve the configuration the reference block commits to from what the sync stored; a client created without the option executes as any other once it has synced. The option still sets what `client.feeFaucetId()` reports before the first sync. After it, the accessor reads the faucet from the configuration the block at the sync height commits to, so a wrong `feeFaucetId` is corrected by syncing rather than executed under. The accessor rejects on a client that has neither synced nor been given the option, where it used to resolve `undefined` only before creation ([rust-sdk#2591](https://github.com/0xMiden/rust-sdk/pull/2591)) ([#414](https://github.com/0xMiden/web-sdk/pull/414)). +* [CHANGE][react] `MidenConfig.feeFaucetId` is optional for the same reason: the provider's client gets its configuration by syncing ([#414](https://github.com/0xMiden/web-sdk/pull/414)). * [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)) diff --git a/Cargo.lock b/Cargo.lock index 15d321c4..de0f1369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2047,7 +2047,7 @@ dependencies = [ [[package]] name = "js-export-macro" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" dependencies = [ "proc-macro2", "quote", @@ -2283,9 +2283,9 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741c7243663a2cd685b3c4e60b4ddadc43908cb8bc7b4cc3d30e142124bfbdf2" +checksum = "5ea327cadee7adcc06b38ce467733effe22d1ecbc73c489023e6ef51d6220788" dependencies = [ "alloy-sol-types", "fs-err", @@ -2375,9 +2375,9 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e054c08131995b76c5eb9c33a1fe9c4c352db3e6c5315f4409343cb1b0dded" +checksum = "b366a0bb6ad44c17edfef62f6f9daaf424bccede96a774be7eb35c35bf3cf872" dependencies = [ "miden-processor", "miden-protocol", @@ -2387,9 +2387,9 @@ dependencies = [ [[package]] name = "miden-client" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a4336958775c89f1ed7ba6f1b88d984f6c80173a09fcc661889c631fab163e" +checksum = "f7283f7bdd6f5ec8515f33551de54c3b02daef0b7bbebedf11934865cac94cef" dependencies = [ "anyhow", "async-trait", @@ -2400,7 +2400,6 @@ dependencies = [ "miden-agglayer", "miden-assembly-syntax", "miden-node-proto-build", - "miden-note-transport-proto-build", "miden-objects", "miden-processor", "miden-protocol", @@ -2428,9 +2427,9 @@ dependencies = [ [[package]] name = "miden-client-sqlite-store" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dfb9674f988c511dff9fe868f58188a7c0fc4c15bd8790763adf46b1cc21a6" +checksum = "bec09ad75e2e42c0a5eff5d1d1536d29b5ed9c6dc1593a085ebbe46de8ea88b1" dependencies = [ "anyhow", "async-trait", @@ -2448,7 +2447,7 @@ dependencies = [ [[package]] name = "miden-client-web" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" dependencies = [ "async-trait", "console_error_panic_hook", @@ -2640,7 +2639,7 @@ dependencies = [ [[package]] name = "miden-idxdb-store" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" dependencies = [ "async-trait", "base64", @@ -2747,16 +2746,16 @@ dependencies = [ [[package]] name = "miden-mobile-prover" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" dependencies = [ "miden-client", ] [[package]] name = "miden-node-proto-build" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea7fdb412d0a1e2b795cd5a433524bafeff0462a1e82a7f1aca000fb52869d5" +checksum = "faccdf0eb561a149dace1215ee3a18017ce555e60bbcf29f2f627c0f45286a8d" dependencies = [ "build-rs", "codegen", @@ -2767,23 +2766,11 @@ dependencies = [ "tonic-prost-build", ] -[[package]] -name = "miden-note-transport-proto-build" -version = "0.5.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9d736051a42941788c6534caf8cf779b388c0ae72c9d5f0d729d2a9872d833" -dependencies = [ - "fs-err", - "miette", - "protox", - "tonic-prost-build", -] - [[package]] name = "miden-objects" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "131901dc84b43e3c51e982b23a62eaadaf8915e3a4f939ad97a2709a9be8693e" +checksum = "13cd010bedc5fd9ae53516354741e3275e9b48333d42e48dfb2bdaec2526372f" dependencies = [ "miden-protobuf", "miden-protocol", @@ -2917,9 +2904,9 @@ dependencies = [ [[package]] name = "miden-protobuf" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6b26d407e6722f0611da021a545be691ba235f893215dd2c06f6c5f7405fe43" +checksum = "f21d2e2a6f1a60b180dbbc34a35768780278205551fb749af9b4f42cc77cb03c" dependencies = [ "miden-protobuf-derive", "proc-macro-crate", @@ -2930,9 +2917,9 @@ dependencies = [ [[package]] name = "miden-protobuf-derive" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7675f068023c65c69e5ef591d8f46be61f02a35bdd3f866749f5dd4cbcd02533" +checksum = "9f819dca311ca460a5a5d3b8e9409648ef114383261323dbe4c991da5a0cceb7" dependencies = [ "heck", "proc-macro-crate", @@ -2943,9 +2930,9 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e58b154de854083bbc71e03dfd6b8b193395c84a09dc644cc8a7e0a6f351d0d" +checksum = "a2292e260de39f202d49f1e6c19fc7375d41f961e6bcde233c059953c3c08eec" dependencies = [ "bech32", "fs-err", @@ -2974,9 +2961,9 @@ dependencies = [ [[package]] name = "miden-protocol-build-utils" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fab13bb4fac6ad39f2b751a9064ce94b944258fcc48a666b96dbfe19b0336624" +checksum = "7fd7b35b3a32e8ea2a96d60feebfe02aba6706d6a5fbfe31563ee8cabd3a0b35" dependencies = [ "fs-err", "miden-assembly", @@ -3028,9 +3015,9 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e12c7a983ab0db83bf689279e33e730b1fde16c1cc7b03a5a48bcbd8ed2441" +checksum = "fe0657d5dde94bb56eab6fbd0276c405127724f8d0f36e86463d97a86b6efb84" dependencies = [ "bon", "miden-assembly", @@ -3067,9 +3054,9 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a100a9dbe195a0f11823016a69141e831576ab3072acb8193ddb332d2581d43" +checksum = "7e86af09aa9b6e0ed88f4d8ed3bc9c4029c8c236492dfd995e32c6e4cd2476d6" dependencies = [ "anyhow", "itertools 0.15.0", @@ -3088,9 +3075,9 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e9839cb96cdc9c15db05ebd08b423b3bf0b9ddb4e32e3023c7353a6763f4a7" +checksum = "52bfd2ebbfdcbc004f12b27dc61a4a2f000a442acae8c17c40ade8b4bbed3231" dependencies = [ "bon", "miden-agglayer", @@ -3103,9 +3090,9 @@ dependencies = [ [[package]] name = "miden-tx-batch" -version = "0.17.0-rc.5" +version = "0.17.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb096837d6a1cb4b47c21da46c5cc53af573dc5a543e9804589eb285344e911a" +checksum = "68057ec4ff03260ba9f95ddad0d11585a98cc01a1f96b49032fcf71e813ef4ed" dependencies = [ "miden-processor", "miden-protocol", @@ -4893,7 +4880,7 @@ dependencies = [ [[package]] name = "strip-masp-debug" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" dependencies = [ "miden-core", "miden-mast-package", diff --git a/Cargo.toml b/Cargo.toml index 49512a4f..86acbe5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ edition = "2024" license = "MIT" repository = "https://github.com/0xMiden/web-sdk" rust-version = "1.98.1" -version = "0.17.0-rc.1" +version = "0.17.0-rc.2" [profile.dev] codegen-units = 16 @@ -41,17 +41,17 @@ codegen-units = 16 [workspace.dependencies] # Workspace crates. The versions are required when publishing path dependencies and must track # `workspace.package.version`. -idxdb-store = { default-features = false, package = "miden-idxdb-store", path = "crates/idxdb-store", version = "0.17.0-rc.1" } -js-export-macro = { path = "crates/js-export-macro", version = "0.17.0-rc.1" } +idxdb-store = { default-features = false, package = "miden-idxdb-store", path = "crates/idxdb-store", version = "0.17.0-rc.2" } +js-export-macro = { path = "crates/js-export-macro", version = "0.17.0-rc.2" } # Client crates # `miden-protocol` is the protocol crate `miden-client` itself resolves, needed directly only for # the `SequentialCommit` trait, which `miden-client` does not re-export but which the multisig auth # args are committed through. A requirement that drifts from `miden-client`'s would resolve a second # copy of the protocol types, so bump the three together. -miden-client = { default-features = false, version = "0.17.0-rc.1" } -miden-client-sqlite-store = { default-features = false, version = "0.17.0-rc.1" } -miden-protocol = { default-features = false, version = "0.17.0-rc.5" } +miden-client = { default-features = false, version = "0.17.0-rc.2" } +miden-client-sqlite-store = { default-features = false, version = "0.17.0-rc.2" } +miden-protocol = { default-features = false, version = "0.17.0-rc.6" } # External dependencies async-trait = { version = "0.1" } diff --git a/crates/idxdb-store/src/sync/mod.rs b/crates/idxdb-store/src/sync/mod.rs index 4bb48b0a..528eb9cf 100644 --- a/crates/idxdb-store/src/sync/mod.rs +++ b/crates/idxdb-store/src/sync/mod.rs @@ -6,7 +6,8 @@ use miden_client::Word; use miden_client::account::{Account, AccountId}; use miden_client::crypto::{Forest, MmrPeaks}; use miden_client::note::{BlockNumber, NoteDetailsCommitment, NoteTag}; -use miden_client::store::StoreError; +use miden_client::protocol_config::protocol_config_setting_key; +use miden_client::store::{SettingScope, StoreError}; use miden_client::sync::{ NoteTagRecord, NoteTagSource, @@ -158,6 +159,7 @@ impl IdxdbStore { note_updates, transaction_updates, account_updates, + protocol_config, ) = state_sync_update.into_parts(); let ( @@ -264,6 +266,19 @@ impl IdxdbStore { self.apply_incremental_account_patch(new_header, patch).await?; } + // Persist the protocol configuration the node sent before the chain state that commits to + // it lands. A failed write here fails the sync, which retries from the same height and is + // delivered the configuration again; a configuration stored ahead of a failed state write + // is only an unused row keyed by its commitment. + if let Some(config) = protocol_config { + self.set_setting( + SettingScope::Client, + protocol_config_setting_key(config.to_commitment()), + config.to_bytes(), + ) + .await?; + } + let state_update = JsStateSyncUpdate { block_num: block_num.as_u32(), flattened_new_block_headers: flatten_nested_u8_vec(block_headers_as_bytes), diff --git a/crates/web-client/README.md b/crates/web-client/README.md index 2c65cfda..7aeaf43f 100644 --- a/crates/web-client/README.md +++ b/crates/web-client/README.md @@ -265,14 +265,13 @@ import { MidenClient, AccountId, Felt } from "@miden-sdk/miden-sdk"; const id = AccountId.fromHex("0x…"); // sync, WASM is already initialized const felt = new Felt(42n); // sync -const client = await MidenClient.createTestnet({ feeFaucetId: FEE_FAUCET }); +const client = await MidenClient.createTestnet(); ``` -Every non-mock constructor needs `feeFaucetId`. Since 0.17 the chain's fee asset -lives in a protocol configuration the node does not serve over RPC, and the SDK -carries a per-network default for no network yet, so a client created without it -fails with an error naming the option. Snippets below leave it out where the -point they make is something else. +`feeFaucetId` is optional. Since 0.17 the chain's fee asset lives in the +protocol configuration, which the client receives from the node when it syncs, +so execution never needs the option: it only sets what `client.feeFaucetId()` +reports before the first sync. Snippets below leave it out. ### Lazy usage (`/lazy`) @@ -484,6 +483,25 @@ console.log(wallet.isPrivate()); // true console.log(wallet.isFaucet()); // false ``` +### Register on an Allowlisted Network + +A network that enforces an account allowlist creates an account on chain only once the account is registered with an invitation code from the network operator. Register a new account before its first transaction: + +```typescript +const wallet = await client.accounts.create(); + +if (!(await client.accounts.isAllowed(wallet))) { + await client.accounts.register({ account: wallet, invitationCode }); +} + +// When the network funds registered accounts, the funding note arrives on the +// next sync; consuming it is the transaction that creates the account on chain. +await client.sync(); +await client.transactions.consumeAll({ account: wallet }); +``` + +`register` fails with code `ACCOUNT_ALREADY_ALLOWED` for an account the node already allows, keeping the code, and a submission that would create an unregistered account fails with `ACCOUNT_NOT_ALLOWLISTED`. `RpcClient.registerAccount` and `RpcClient.isAccountAllowed` expose the node endpoints directly for flows that hold no account state. See [the allowlist guide](https://github.com/0xMiden/web-sdk/blob/main/docs/external/src/web-client/library/allowlist.md) for the full flow. + ### Create a Faucet ```typescript diff --git a/crates/web-client/js/client.js b/crates/web-client/js/client.js index bd7cbea1..8b5136a8 100644 --- a/crates/web-client/js/client.js +++ b/crates/web-client/js/client.js @@ -121,11 +121,9 @@ export class MidenClient { * If no `rpcUrl` is provided, defaults to testnet with full configuration * (RPC, prover, note transport, autoSync). * - * **Requires `feeFaucetId` today.** Since 0.17 the chain's fee asset lives in a - * protocol configuration the node does not serve, so the client has to be told - * which faucet mints it. The SDK carries a per-network default for no network - * yet, so every non-mock client must name one or creation fails with an error - * saying so. + * `feeFaucetId` is optional: the client receives the chain's protocol + * configuration, which names the fee asset, from the node when it syncs. The + * option only sets what `feeFaucetId()` reports before that first sync. * * @param {ClientOptions} [options] - Client configuration options. * @returns {Promise} A fully initialized client. @@ -207,11 +205,9 @@ export class MidenClient { * Defaults: rpcUrl "testnet", proverUrl "testnet", noteTransportUrl "testnet", autoSync true. * All defaults can be overridden via options. * - * **Requires `feeFaucetId` today.** Since 0.17 the chain's fee asset lives in a - * protocol configuration the node does not serve, so the client has to be told - * which faucet mints it. The SDK carries a per-network default for no network - * yet, so every non-mock client must name one or creation fails with an error - * saying so. + * `feeFaucetId` is optional: the client receives the chain's protocol + * configuration, which names the fee asset, from the node when it syncs. The + * option only sets what `feeFaucetId()` reports before that first sync. * * @param {ClientOptions} [options] - Options to override defaults. * @returns {Promise} A fully initialized testnet client. @@ -232,11 +228,9 @@ export class MidenClient { * Defaults: rpcUrl "devnet", proverUrl "devnet", noteTransportUrl "devnet", autoSync true. * All defaults can be overridden via options. * - * **Requires `feeFaucetId` today.** Since 0.17 the chain's fee asset lives in a - * protocol configuration the node does not serve, so the client has to be told - * which faucet mints it. The SDK carries a per-network default for no network - * yet, so every non-mock client must name one or creation fails with an error - * saying so. + * `feeFaucetId` is optional: the client receives the chain's protocol + * configuration, which names the fee asset, from the node when it syncs. The + * option only sets what `feeFaucetId()` reports before that first sync. * * @param {ClientOptions} [options] - Options to override defaults. * @returns {Promise} A fully initialized devnet client. @@ -424,13 +418,13 @@ export class MidenClient { } /** - * Returns the fee faucet of the protocol configuration this client - * registered at creation. + * Returns the faucet of the chain's fee asset. * * Replaces `BlockHeader.feeFaucetId()`: since 0.17 the fee asset lives in the - * protocol configuration rather than the block header, so it is the - * configuration the client registered at creation that names it - the - * `feeFaucetId` option, or, for a mock client, the mock chain's own. + * protocol configuration rather than the block header. The client receives + * that configuration from the node when it syncs, so after the first sync + * this reports the faucet the chain's configuration names; before it, the + * `feeFaucetId` option or, for a mock client, the mock chain's own. * * @returns {Promise} The fee faucet's account ID. */ diff --git a/crates/web-client/js/index.js b/crates/web-client/js/index.js index bc366831..78e76967 100644 --- a/crates/web-client/js/index.js +++ b/crates/web-client/js/index.js @@ -129,6 +129,7 @@ const WRITE_METHODS = new Set([ "newSendTransactionRequest", "newSwapTransactionRequest", "pruneAccountHistory", + "registerAccount", "removeAccountAddress", "removeTag", "removeSetting", @@ -164,6 +165,7 @@ const READ_METHODS = new Set([ "getSetting", "getSyncHeight", "getTransactions", + "isAccountAllowed", "listSettingKeys", "listTags", "executeProgram", @@ -423,10 +425,9 @@ class WebClient { * client and for its whole lifetime, whether observations carry the * high-fidelity `sensitive` channel. Both are construction-only. * @param {string | undefined} [feeFaucetId] - Faucet of the chain's fee asset, - * as a bech32 address or a hex account ID. Since 0.17 the fee asset lives in - * the protocol configuration rather than the block header, and a client that - * cannot build one can neither execute nor screen notes, so this is required - * for a network the SDK knows no fee faucet for. + * as a bech32 address or a hex account ID. Optional: the client receives the + * protocol configuration, which names the fee asset, from the node when it + * syncs, so this only sets what `feeFaucetId()` reports before the first sync. */ constructor( rpcUrl, @@ -828,9 +829,9 @@ class WebClient { * @param {{observer?: (observation: object) => void, observeSensitive?: boolean}} [observability] * - Observability fields of `ClientOptions`; see the constructor. * @param {string | undefined} feeFaucetId - Fee faucet of the chain, as a bech32 address or a - * hex account ID. Required for a network the SDK knows no fee faucet for: since 0.17 the fee - * asset lives in the protocol configuration rather than the block header, and a client - * without one cannot execute. + * hex account ID. Optional: the client receives the protocol configuration, which names the + * fee asset, from the node when it syncs, so this only sets what `feeFaucetId()` reports + * before the first sync. */ static async createClient( rpcUrl, diff --git a/crates/web-client/js/resources/accounts.js b/crates/web-client/js/resources/accounts.js index 69f0afd8..8157c569 100644 --- a/crates/web-client/js/resources/accounts.js +++ b/crates/web-client/js/resources/accounts.js @@ -207,4 +207,46 @@ export class AccountsResource { const address = wasm.Address.fromBech32(addr); await this.#inner.removeAccountAddress(id, address); } + + /** + * Binds an invitation code to a tracked account on the network allowlist, + * so the account's first transaction can create it on chain. + * + * The account must be tracked, not yet deployed, and not a network account. + * A registration consumes the code, so the node is asked first: an account + * it already allows fails with `ACCOUNT_ALREADY_ALLOWED` and the code is + * kept. The node's own rejections carry `INVITATION_NOT_FOUND`, + * `ALREADY_REGISTERED` or `INVALID_REGISTRATION_REQUEST`. When the network + * funds registered accounts, the call returns once the funding note is + * committed, which can take a few blocks; the note arrives on the next sync. + * + * @param {RegisterAccountOptions} options + * @returns {Promise} + */ + async register({ account, invitationCode }) { + this.#client.assertNotTerminated(); + if (typeof invitationCode !== "string" || invitationCode.length === 0) { + throw new Error( + "accounts.register requires a non-empty 'invitationCode' string" + ); + } + const wasm = await this.#getWasm(); + const id = resolveAccountRef(account, wasm); + await this.#inner.registerAccount(id, invitationCode); + } + + /** + * Returns whether the network lets the account be created on chain: `true` + * when the node does not enforce an account allowlist, or when the account + * is registered. + * + * @param {AccountRef} ref + * @returns {Promise} + */ + async isAllowed(ref) { + this.#client.assertNotTerminated(); + const wasm = await this.#getWasm(); + const id = resolveAccountRef(ref, wasm); + return await this.#inner.isAccountAllowed(id); + } } diff --git a/crates/web-client/js/types/api-types.d.ts b/crates/web-client/js/types/api-types.d.ts index 97acf4f6..8f39bdd2 100644 --- a/crates/web-client/js/types/api-types.d.ts +++ b/crates/web-client/js/types/api-types.d.ts @@ -235,11 +235,10 @@ export interface ClientOptions { /** * Faucet of the chain's fee asset, as a bech32 address or a hex account ID. * - * Required for a network the SDK knows no fee faucet for. Miden 0.17 moved the fee asset out of - * the block header and into the protocol configuration, which a node does not serve over RPC - * yet: execution and note screening both resolve the configuration the reference block commits - * to, so a client that cannot build one cannot execute at all. Read it back with - * `client.feeFaucetId()`, which replaces the `BlockHeader.feeFaucetId()` of earlier versions. + * Optional. Miden 0.17 moved the fee asset out of the block header and into the protocol + * configuration, which the client receives from the node when it syncs, so execution does not + * need this. It only sets what `client.feeFaucetId()` reports before the first sync; after it, + * the accessor reads the configuration the chain commits to. */ feeFaucetId?: string; /** Sync state on creation (default: false). */ @@ -422,6 +421,14 @@ export interface InsertAccountOptions { /** Options for accounts.export(). Exists for forward-compatible extensibility. */ export interface ExportAccountOptions {} +/** Options for accounts.register(). */ +export interface RegisterAccountOptions { + /** The tracked, not yet deployed account to register. */ + account: AccountRef; + /** The invitation code the network operator issued. Consumed by a successful registration. */ + invitationCode: string; +} + // ════════════════════════════════════════════════════════════════ // Transaction types // ════════════════════════════════════════════════════════════════ @@ -1023,6 +1030,42 @@ export interface AccountsResource { * @param options - Insert options. */ insert(options: InsertAccountOptions): Promise; + /** + * Bind an invitation code to a tracked account on the network allowlist. + * + * A network that enforces an account allowlist creates an account on chain only + * once it is registered. The first transaction of an account is what creates it, + * so register before submitting that transaction: a submission that would create + * an account the network does not accept fails with code `ACCOUNT_NOT_ALLOWLISTED`. + * Only creation is gated - an account that already exists on chain is never + * checked, and network accounts are exempt. + * + * The account must be tracked, not yet deployed, and not a network account. A + * registration consumes the code, so the node is asked first whether it already + * allows the account: if so this fails with `ACCOUNT_ALREADY_ALLOWED` and the code + * is kept, which is also what a network without an allowlist answers for every + * account. The node's own rejections carry `INVITATION_NOT_FOUND`, + * `ALREADY_REGISTERED` or `INVALID_REGISTRATION_REQUEST`. + * + * When the network operator runs a funding service, the node pays the registered + * account a public P2ID note with the native asset and answers once it is + * committed, so this can take a few blocks. The note is not returned: it arrives + * with the next `sync()`, and consuming it is the first transaction, which creates + * the account on chain and pays its fee out of the received funds. + * + * @param options - The account and its invitation code. + */ + register(options: RegisterAccountOptions): Promise; + /** + * Whether the network lets the account be created on chain. + * + * `true` when the node does not enforce an account allowlist, or when the account + * is registered. Only creation is gated, so the answer says nothing about an + * account that already exists on chain. + * + * @param accountId - The account to check. + */ + isAllowed(accountId: AccountRef): Promise; /** * Retrieve an account by ID. Returns `null` if not found in the local store. * @@ -1844,13 +1887,14 @@ export declare class MidenClient { terminate(): void; /** - * Returns the fee faucet of the protocol configuration this client - * registered at creation. + * Returns the faucet of the chain's fee asset. * * Replaces `BlockHeader.feeFaucetId()`: since 0.17 the fee asset lives in the - * protocol configuration rather than the block header, so this reports the - * configuration the client registered at creation - the `feeFaucetId` option, - * or, for a mock client, the mock chain's own. + * protocol configuration rather than the block header. The client receives that + * configuration from the node when it syncs, so after the first sync this reports + * the faucet named by the configuration the block at the sync height commits to. + * Before it, this falls back to the `feeFaucetId` option, or, for a mock client, + * the mock chain's own, and rejects when neither is set. */ feeFaucetId(): Promise; diff --git a/crates/web-client/package.json b/crates/web-client/package.json index 9c55697e..2749281c 100644 --- a/crates/web-client/package.json +++ b/crates/web-client/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/miden-sdk", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Miden WASM SDK for browser and Node.js. Run `npm create @miden-sdk@latest` to point your AI coding agent at version-matched guidance.", "license": "MIT", "repository": { diff --git a/crates/web-client/playwright.config.ts b/crates/web-client/playwright.config.ts index 68e19e44..332826cc 100644 --- a/crates/web-client/playwright.config.ts +++ b/crates/web-client/playwright.config.ts @@ -84,6 +84,7 @@ const ciShardProjects = process.env.CI testMatch: [ "test/account.test.ts", "test/account_component.test.ts", + "test/allowlist.test.ts", "test/account_file.test.ts", "test/account_reader.test.ts", "test/new_account.test.ts", diff --git a/crates/web-client/src/account.rs b/crates/web-client/src/account.rs index 21a7a8ef..69686025 100644 --- a/crates/web-client/src/account.rs +++ b/crates/web-client/src/account.rs @@ -233,4 +233,56 @@ impl WebClient { // SAFETY: on wasm32 usize is 32 bits, so this conversion is infallible Ok(u32::try_from(deleted).expect("deleted count should fit in u32")) } + + // ACCOUNT REGISTRATION + // -------------------------------------------------------------------------------------------- + + /// Binds an invitation code to a tracked account on the network allowlist. + /// + /// A network that enforces an account allowlist creates an account on chain only when the + /// account is registered. The first transaction of an account is what creates it, so the + /// account must be registered before that transaction is submitted: a submission that would + /// create an account the network does not accept fails with `ACCOUNT_NOT_ALLOWLISTED`. Only + /// account creation is gated. An account that already exists on chain is never checked, and + /// network accounts are exempt. + /// + /// The account must be tracked by this client, must not be deployed on chain yet, and must + /// not be a network account. A registration consumes the code, so the client asks the node + /// first and does not send it for an account the node already allows: that fails with + /// `ACCOUNT_ALREADY_ALLOWED`, which a network that does not enforce an allowlist answers for + /// every account. The node's own rejections carry `INVITATION_NOT_FOUND`, + /// `ALREADY_REGISTERED` or `INVALID_REGISTRATION_REQUEST`. + /// + /// When the network operator runs a funding service, the node pays the registered account a + /// public P2ID note with the native asset and answers once that note is committed, so this + /// call can take a few blocks. The note reaches the client on the next sync; consuming it is + /// the first transaction, which creates the account on chain and pays its fee out of the + /// received funds. + #[js_export(js_name = "registerAccount")] + pub async fn register_account( + &self, + account_id: &AccountId, + invitation_code: String, + ) -> Result<(), JsErr> { + let mut guard = self.get_mut_inner().await; + let client = guard.as_mut().ok_or_else(|| from_str_err("Client not initialized"))?; + client + .register_account(account_id.into(), &invitation_code) + .await + .map_err(|err| js_error_with_context(err, "failed to register account")) + } + + /// Returns whether the network lets the account be created on chain. + /// + /// The node answers `true` when it does not enforce an account allowlist, or when the + /// account is registered. Only account creation is gated, so the answer says nothing about + /// an account that already exists on chain. + #[js_export(js_name = "isAccountAllowed")] + pub async fn is_account_allowed(&self, account_id: &AccountId) -> Result { + let mut guard = self.get_mut_inner().await; + let client = guard.as_mut().ok_or_else(|| from_str_err("Client not initialized"))?; + client.is_account_allowed(account_id.into()).await.map_err(|err| { + js_error_with_context(err, "failed to check whether the account is allowed") + }) + } } diff --git a/crates/web-client/src/lib.rs b/crates/web-client/src/lib.rs index 9836c436..4e88839f 100644 --- a/crates/web-client/src/lib.rs +++ b/crates/web-client/src/lib.rs @@ -21,16 +21,14 @@ use js_export_macro::js_export; #[cfg(feature = "browser")] use js_sys::{Function, Reflect}; use miden_client::account::AccountId as NativeAccountId; -use miden_client::asset::AssetId; use miden_client::builder::{ClientBuilder, DEFAULT_GRPC_TIMEOUT_MS}; use miden_client::crypto::RandomCoin; #[cfg(feature = "nodejs")] use miden_client::keystore::FilesystemKeyStore; use miden_client::note_transport::NoteTransportClient; use miden_client::note_transport::grpc::GrpcNoteTransportClient; -use miden_client::protocol_config::ProtocolConfig; -use miden_client::rpc::{Endpoint, GrpcClient, NodeRpcClient, VerifyingRpcClient}; -use miden_client::store::Store; +use miden_client::rpc::{Endpoint, GrpcClient, NodeRpcClient, RpcError, VerifyingRpcClient}; +use miden_client::store::{Store, StoreError}; use miden_client::testing::mock::MockRpcApi; use miden_client::testing::note_transport::MockNoteTransportApi; use miden_client::{Client, ClientError, ErrorHint, Felt}; @@ -223,8 +221,8 @@ pub fn setup_logging(log_level: &str) { #[js_export] pub struct WebClient { inner: AsyncCell>>, - /// Faucet of the fee asset the registered protocol configuration names. Since 0.17 the block - /// header no longer carries it, so this is the only place a consumer can read it back from. + /// Fee faucet the caller declared at creation, or the mock chain's own. What `feeFaucetId()` + /// reports until the first sync stores the protocol configuration the chain commits to. fee_faucet: AsyncCell>, mock_rpc_api: AsyncCell>>, mock_note_transport_api: AsyncCell>>, @@ -279,17 +277,47 @@ impl WebClient { } } - /// Returns the fee faucet of the protocol configuration this client registered at creation. + /// Returns the faucet of the chain's fee asset. /// /// Before 0.17 any block header carried it, so a consumer could discover the chain's native /// asset by reading one. The header no longer does: the fee asset lives in the protocol - /// configuration, which the node does not serve over RPC yet, so this reports the - /// configuration the client registered when it was created - the caller's `feeFaucetId`, the - /// one this SDK knows for the network, or, for a mock client, the one the mock chain itself - /// commits to. `undefined` only on a client that has not been created yet. + /// configuration, which the client receives from the node when it syncs. After the first sync + /// this reports the faucet named by the configuration the block at the sync height commits + /// to. Before it, this falls back to the `feeFaucetId` the client was created with, or, for a + /// mock client, the one the mock chain itself commits to, and fails when neither is set. #[js_export(js_name = "feeFaucetId")] - pub async fn fee_faucet_id(&self) -> Option { - (*self.fee_faucet.lock().await).map(Into::into) + pub async fn fee_faucet_id(&self) -> Result { + let mut guard = self.get_mut_inner().await; + let client = guard.as_mut().ok_or_else(|| from_str_err("Client not initialized"))?; + + let sync_height = client + .get_sync_height() + .await + .map_err(|err| js_error_with_context(err, "failed to read the sync height"))?; + let header = client + .get_block_header_by_num(sync_height) + .await + .map_err(|err| js_error_with_context(err, "failed to read the synced block header"))?; + if let Some((header, _)) = header { + match client.get_protocol_config(header.protocol_config_commitment()).await { + Ok(config) => return Ok(config.fee_asset_id().faucet_id().into()), + // Nothing has been synced yet, so no configuration is stored for the header. + Err(ClientError::StoreError(StoreError::ProtocolConfigNotFound(_))) => {}, + Err(err) => { + return Err(js_error_with_context( + err, + "failed to read the protocol configuration", + )); + }, + } + } + + (*self.fee_faucet.lock().await).map(Into::into).ok_or_else(|| { + from_str_err( + "the chain's fee faucet is not known yet: sync the client so it receives the \ + protocol configuration from the node, or pass `feeFaucetId` when creating it", + ) + }) } /// Returns the identifier of the underlying store (e.g. `IndexedDB` database name, file path). @@ -390,8 +418,8 @@ impl WebClient { /// `MidenClientDB_{network_id}`, where `network_id` is derived from the `node_url`. /// Explicitly setting this allows for creating multiple isolated clients. /// * `fee_faucet_id`: Optional fee faucet of the chain, as a bech32 address or a hex account - /// ID. Required for a network this SDK knows no fee faucet for, since a client cannot execute - /// without the protocol configuration built from it. + /// ID. Only what `feeFaucetId()` reports before the first sync delivers the protocol + /// configuration from the node; execution does not need it. #[wasm_bindgen(js_name = "createClient")] pub async fn create_client( &self, @@ -424,17 +452,10 @@ impl WebClient { ); let keystore = WebKeyStore::new_with_callbacks(rng, store_name.clone(), None, None, None); - let protocol_config = resolve_protocol_config(&endpoint, fee_faucet_id)?; + let fee_faucet = fee_faucet_id.map(|id| parse_fee_faucet_id(&id)).transpose()?; - self.setup_client( - web_rpc_client, - store, - keystore, - rng, - note_transport_client, - protocol_config, - ) - .await?; + self.setup_client(web_rpc_client, store, keystore, rng, note_transport_client, fee_faucet) + .await?; Ok(JsValue::from_str("Client created successfully")) } @@ -452,8 +473,8 @@ impl WebClient { /// `MidenClientDB_{network_id}`, where `network_id` is derived from the `node_url`. /// Explicitly setting this allows for creating multiple isolated clients. /// * `fee_faucet_id`: Optional fee faucet of the chain, as a bech32 address or a hex account - /// ID. Required for a network this SDK knows no fee faucet for, since a client cannot execute - /// without the protocol configuration built from it. + /// ID. Only what `feeFaucetId()` reports before the first sync delivers the protocol + /// configuration from the node; execution does not need it. /// * `get_key_cb`: Callback to retrieve the secret key bytes for a given public key. /// * `insert_key_cb`: Callback to persist a secret key. /// * `sign_cb`: Callback to produce serialized signature bytes for the provided inputs. @@ -494,17 +515,10 @@ impl WebClient { let keystore = WebKeyStore::new_with_callbacks(rng, store_name, get_key_cb, insert_key_cb, sign_cb); - let protocol_config = resolve_protocol_config(&endpoint, fee_faucet_id)?; + let fee_faucet = fee_faucet_id.map(|id| parse_fee_faucet_id(&id)).transpose()?; - self.setup_client( - web_rpc_client, - store, - keystore, - rng, - note_transport_client, - protocol_config, - ) - .await?; + self.setup_client(web_rpc_client, store, keystore, rng, note_transport_client, fee_faucet) + .await?; Ok(JsValue::from_str("Client created successfully")) } @@ -517,7 +531,7 @@ impl WebClient { keystore: WebKeyStore, rng: RandomCoin, note_transport_client: Option>, - protocol_config: ProtocolConfig, + fee_faucet: Option, ) -> Result<(), JsValue> { let mut builder = ClientBuilder::new() .rpc(rpc_client) @@ -529,9 +543,6 @@ impl WebClient { builder = builder.note_transport(transport); } - let fee_faucet = protocol_config.fee_asset_id().faucet_id(); - builder = builder.protocol_config(protocol_config); - let mut client = builder .build() .await @@ -544,7 +555,7 @@ impl WebClient { // Published together with `inner`, so a creation that fails leaves neither set: the // accessor reports the faucet of a client that exists, or nothing. - *self.fee_faucet.lock().await = Some(fee_faucet); + *self.fee_faucet.lock().await = fee_faucet; *self.inner.lock().await = Some(client); Ok(()) @@ -567,8 +578,8 @@ impl WebClient { /// * `db_path`: Path to the SQLite database file. /// * `keystore_path`: Path to the directory for storing keys. /// * `fee_faucet_id`: Optional fee faucet of the chain, as a bech32 address or a hex account - /// ID. Required for a network this SDK knows no fee faucet for, since a client cannot execute - /// without the protocol configuration built from it. + /// ID. Only what `feeFaucetId()` reports before the first sync delivers the protocol + /// configuration from the node; execution does not need it. #[napi(js_name = "createClient")] pub async fn create_client( &self, @@ -604,9 +615,9 @@ impl WebClient { let keystore = FilesystemKeyStore::new(keystore_path.into()) .map_err(|e| from_str_err(&format!("Failed to initialize keystore: {e}")))?; - let protocol_config = resolve_protocol_config(&endpoint, fee_faucet_id)?; + let fee_faucet = fee_faucet_id.map(|id| parse_fee_faucet_id(&id)).transpose()?; - self.setup_client(rpc_client, store, keystore, rng, note_transport_client, protocol_config) + self.setup_client(rpc_client, store, keystore, rng, note_transport_client, fee_faucet) .await?; Ok("Client created successfully".to_string()) @@ -620,16 +631,14 @@ impl WebClient { keystore: FilesystemKeyStore, rng: RandomCoin, note_transport_client: Option>, - protocol_config: ProtocolConfig, + fee_faucet: Option, ) -> Result<(), JsErr> { - let fee_faucet = protocol_config.fee_asset_id().faucet_id(); let client = maybe_wrap_send(async move { let mut builder = ClientBuilder::new() .rpc(rpc_client) .rng(Box::new(rng)) .store(store) - .authenticator(Arc::new(keystore)) - .protocol_config(protocol_config); + .authenticator(Arc::new(keystore)); if let Some(transport) = note_transport_client { builder = builder.note_transport(transport); @@ -649,59 +658,16 @@ impl WebClient { }) .await?; - *self.fee_faucet.lock().await = Some(fee_faucet); + *self.fee_faucet.lock().await = fee_faucet; *self.inner.lock().await = Some(client); Ok(()) } } -// PROTOCOL CONFIGURATION +// FEE FAUCET // ================================================================================================ -/// Fee faucet of every network whose protocol configuration this SDK can build, keyed by network -/// ID. -/// -/// 0.17 moved the fee asset out of the block header and into the protocol configuration, which a -/// node does not serve over RPC yet. Execution and note screening both resolve the configuration -/// the reference block commits to, so a client that holds none cannot execute at all, and the only -/// two sources are this table and the caller's `feeFaucetId`. A network is added here once its -/// genesis names a fee faucet. -const KNOWN_FEE_FAUCETS: &[(&str, &str)] = &[]; - -/// Builds the protocol configuration the client executes under, from the caller's fee faucet or -/// the one this SDK knows for the endpoint's network. -/// -/// Errors when neither is available, rather than building a client that fails on its first -/// execution with a store error naming a commitment the caller cannot act on. -pub(crate) fn resolve_protocol_config( - endpoint: &Endpoint, - fee_faucet_id: Option, -) -> Result { - let network_id = endpoint.to_network_id().to_string(); - - let faucet_id = if let Some(id) = fee_faucet_id { - parse_fee_faucet_id(&id)? - } else { - let known = KNOWN_FEE_FAUCETS - .iter() - .find_map(|(network, faucet)| (*network == network_id).then_some(*faucet)) - .ok_or_else(|| { - from_str_err(&format!( - "no fee faucet is known for network `{network_id}`, so the protocol \ - configuration this chain executes under cannot be built: pass \ - `feeFaucetId` when creating the client. Miden 0.17 moved the fee asset \ - out of the block header into the protocol configuration, which the node \ - does not serve over RPC yet." - )) - })?; - parse_fee_faucet_id(known)? - }; - - ProtocolConfig::current(AssetId::new_fungible(faucet_id)) - .map_err(|err| js_error_with_context(err, "failed to build the protocol configuration")) -} - /// Reads a fee faucet written either as a bech32 address or as a hex account ID, the two spellings /// the rest of the JS surface accepts for an account. fn parse_fee_faucet_id(id: &str) -> Result { @@ -801,9 +767,38 @@ fn code_from_error(err: &(dyn Error + 'static)) -> Option<&'static str> { return match client_error { ClientError::AccountNotFoundOnChain(_) => Some("ACCOUNT_NOT_FOUND_ON_CHAIN"), ClientError::AccountAlreadyTracked(_) => Some("ACCOUNT_ALREADY_TRACKED"), + ClientError::AccountNotAllowlisted(_) => Some("ACCOUNT_NOT_ALLOWLISTED"), + ClientError::AccountAlreadyAllowed(_) => Some("ACCOUNT_ALREADY_ALLOWED"), + // The node's verdict on a registration travels inside the RPC error rather than as + // a `ClientError` variant of its own. + ClientError::RpcError(rpc_error) => registration_code(rpc_error), _ => None, }; } + if let Some(rpc_error) = err.downcast_ref::() { + return registration_code(rpc_error); + } + err.source().and_then(code_from_error) } + +/// Maps the reasons a node rejects an account registration to stable string codes, for the +/// `registerAccount` callers that branch on them. `None` for every other RPC error. +#[cfg(feature = "browser")] +fn registration_code(err: &RpcError) -> Option<&'static str> { + use miden_client::rpc::{EndpointError, RegisterAccountError}; + + let RpcError::RequestError { + endpoint_error: Some(EndpointError::RegisterAccount(reason)), + .. + } = err + else { + return None; + }; + Some(match reason { + RegisterAccountError::InvitationNotFound => "INVITATION_NOT_FOUND", + RegisterAccountError::AlreadyRegistered => "ALREADY_REGISTERED", + RegisterAccountError::InvalidRequest(_) => "INVALID_REGISTRATION_REQUEST", + }) +} diff --git a/crates/web-client/src/mock.rs b/crates/web-client/src/mock.rs index 0cf066d3..de8105ed 100644 --- a/crates/web-client/src/mock.rs +++ b/crates/web-client/src/mock.rs @@ -5,6 +5,7 @@ use idxdb_store::IdxdbStore; use js_export_macro::js_export; use miden_client::block::BlockNumber; use miden_client::crypto::eddsa_25519_sha512::KeyExchangeKey; +use miden_client::protocol_config::ProtocolConfig; use miden_client::rpc::encryption::TransactionEncryptionKey; use miden_client::store::Store; use miden_client::testing::MockChain; @@ -59,18 +60,18 @@ impl WebClient { ); let keystore = WebKeyStore::new_with_callbacks(rng, store_name, None, None, None); - // The mock chain is its own source of protocol configuration: it commits to one no - // network serves, so the client has to be given that one rather than a network's. + let protocol_config = mock_rpc_api.protocol_config(); self.setup_client( mock_rpc_api.clone(), store, keystore, rng, Some(mock_note_transport_api.clone()), - mock_rpc_api.protocol_config(), + Some(protocol_config.fee_asset_id().faucet_id()), ) .await?; + self.seed_mock_protocol_config(protocol_config).await?; self.seed_mock_transaction_encryption_key().await?; *self.mock_rpc_api.lock().await = Some(mock_rpc_api); @@ -123,18 +124,18 @@ impl WebClient { let keystore = miden_client::keystore::FilesystemKeyStore::new(keystore_path.into()) .map_err(|e| from_str_err(&format!("Failed to initialize keystore: {e}")))?; - // The mock chain is its own source of protocol configuration: it commits to one no - // network serves, so the client has to be given that one rather than a network's. + let protocol_config = mock_rpc_api.protocol_config(); self.setup_client( mock_rpc_api.clone(), store, keystore, rng, Some(mock_note_transport_api.clone()), - mock_rpc_api.protocol_config(), + Some(protocol_config.fee_asset_id().faucet_id()), ) .await?; + self.seed_mock_protocol_config(protocol_config).await?; self.seed_mock_transaction_encryption_key().await?; *self.mock_rpc_api.lock().await = Some(mock_rpc_api); @@ -145,6 +146,19 @@ impl WebClient { } impl WebClient { + /// Gives a mock-backed client the protocol configuration its chain commits to. + /// + /// A client gets its configurations from the node while syncing. The mock chain commits to + /// one no node serves and `MockRpcApi` delivers none, so the client is handed the mock + /// chain's own instead, ahead of the first execution that resolves it. + async fn seed_mock_protocol_config(&self, config: ProtocolConfig) -> Result<(), JsErr> { + let mut guard = self.get_mut_inner().await; + let client = guard.as_mut().ok_or_else(|| from_str_err("Client not initialized"))?; + client.seed_protocol_config(config).await.map_err(|err| { + js_error_with_context(err, "failed to seed the mock protocol configuration") + }) + } + /// Gives a mock-backed client the transaction encryption key that submission seals against. /// /// `MockRpcApi` refuses to serve a key, because attesting one needs a validator signature the diff --git a/crates/web-client/src/rpc_client/mod.rs b/crates/web-client/src/rpc_client/mod.rs index 0aa27722..7b1dbe22 100644 --- a/crates/web-client/src/rpc_client/mod.rs +++ b/crates/web-client/src/rpc_client/mod.rs @@ -359,4 +359,42 @@ impl RpcClient { Ok(height.map(|height| height.as_u32())) } + + /// Binds an invitation code to an account on the network allowlist, through the node's + /// `RegisterAccount` endpoint. + /// + /// Unlike `WebClient.registerAccount`, this sends the request as given: the account does not + /// have to be tracked by a client, and the node is not asked first whether it already allows + /// the account. A registration consumes the code. The node rejects an unknown code + /// (`INVITATION_NOT_FOUND`), a code or account that is already registered + /// (`ALREADY_REGISTERED`) and a malformed request (`INVALID_REGISTRATION_REQUEST`); a network + /// that does not enforce the allowlist ignores the code but still registers the account. + /// + /// When the network operator runs a funding service, the node pays the registered account a + /// public P2ID note and answers once that note is committed, so this call can take a few + /// blocks. + #[js_export(js_name = "registerAccount")] + pub async fn register_account( + &self, + account_id: &AccountId, + invitation_code: String, + ) -> Result<(), JsErr> { + self.inner + .register_account(&invitation_code, account_id.into()) + .await + .map_err(|err| js_error_with_context(err, "failed to register account")) + } + + /// Returns whether the node lets the account be created on chain, through the + /// `IsAccountAllowed` endpoint. + /// + /// `true` when the node does not enforce an account allowlist, or when the account is + /// registered. Only account creation is gated, so the answer says nothing about an account + /// that already exists on chain. + #[js_export(js_name = "isAccountAllowed")] + pub async fn is_account_allowed(&self, account_id: &AccountId) -> Result { + self.inner.is_account_allowed(account_id.into()).await.map_err(|err| { + js_error_with_context(err, "failed to check whether the account is allowed") + }) + } } diff --git a/crates/web-client/test/allowlist.test.ts b/crates/web-client/test/allowlist.test.ts new file mode 100644 index 00000000..ebaf5553 --- /dev/null +++ b/crates/web-client/test/allowlist.test.ts @@ -0,0 +1,67 @@ +// @ts-nocheck +import { test, expect } from "./test-setup"; + +// Neither the mock chain nor the CI test node enforces an account allowlist, so +// the node allows every account and the client refuses to spend an invitation +// code on one. The enforcing path (a code the node accepts, funding, the +// `ACCOUNT_NOT_ALLOWLISTED` rejection) needs a node started with +// MIDEN_ACCOUNT_ALLOWLIST=1 and an invitation code from its admin API. +test.describe("account allowlist", () => { + test("isAccountAllowed answers true when the node enforces no allowlist", async ({ + run, + }) => { + const result = await run(async ({ client, sdk }) => { + const wallet = await client.newWallet( + sdk.AccountStorageMode.private(), + sdk.AuthScheme.AuthRpoFalcon512 + ); + return { allowed: await client.isAccountAllowed(wallet.id()) }; + }); + expect(result.allowed).toBe(true); + }); + + test("registerAccount keeps the code for an account the node already allows", async ({ + run, + }) => { + const result = await run(async ({ client, sdk }) => { + const wallet = await client.newWallet( + sdk.AccountStorageMode.private(), + sdk.AuthScheme.AuthRpoFalcon512 + ); + try { + await client.registerAccount(wallet.id(), "invitation-code"); + return { threw: false }; + } catch (error) { + return { + threw: true, + code: error.code ?? null, + message: String(error.message ?? error), + }; + } + }); + expect(result.threw).toBe(true); + // The WASM build attaches the code; the Node binding only carries the message. + expect( + result.code === "ACCOUNT_ALREADY_ALLOWED" || + result.message.includes("already allowed") + ).toBe(true); + }); + + test("registerAccount rejects an account the client does not track", async ({ + run, + }) => { + const result = await run(async ({ client, sdk }) => { + const untracked = sdk.AccountId.fromHex( + "0x69817bcc6fb9f99127c2245f6979c5" + ); + try { + await client.registerAccount(untracked, "invitation-code"); + return { threw: false }; + } catch (error) { + return { threw: true, message: String(error.message ?? error) }; + } + }); + expect(result.threw).toBe(true); + expect(result.message).toContain("failed to register account"); + }); +}); diff --git a/crates/web-client/test/miden_client_api.node.test.ts b/crates/web-client/test/miden_client_api.node.test.ts index f9c8e122..fe2429f3 100644 --- a/crates/web-client/test/miden_client_api.node.test.ts +++ b/crates/web-client/test/miden_client_api.node.test.ts @@ -126,6 +126,32 @@ test.describe("MidenClient API - Mock Chain", () => { expect(result.isPublic).toBe(true); }); + test("accounts.isAllowed and accounts.register on a chain without an allowlist", async ({ + sdk, + }) => { + const MidenClient = await createMidenClient(sdk); + test.skip(!MidenClient, "requires napi binary (Node.js only)"); + const client = await MidenClient.createMock(); + + const wallet = await client.accounts.create(); + const allowed = await client.accounts.isAllowed(wallet); + + let message = null; + try { + await client.accounts.register({ + account: wallet, + invitationCode: "invitation-code", + }); + } catch (error) { + message = String(error.message ?? error); + } + + // The mock node enforces no allowlist, so every account is allowed and the + // client keeps the invitation code rather than spending it. + expect(allowed).toBe(true); + expect(message).toContain("already allowed"); + }); + test("accounts.list returns created accounts", async ({ sdk }) => { const MidenClient = await createMidenClient(sdk); test.skip(!MidenClient, "requires napi binary (Node.js only)"); diff --git a/crates/web-client/test/miden_client_api.test.ts b/crates/web-client/test/miden_client_api.test.ts index 04056f23..f1dbf529 100644 --- a/crates/web-client/test/miden_client_api.test.ts +++ b/crates/web-client/test/miden_client_api.test.ts @@ -145,6 +145,44 @@ mockTest.describe("MidenClient API - Mock Chain", () => { expect(result.isPublic).toBe(true); }); + mockTest( + "accounts.isAllowed and accounts.register on a chain without an allowlist", + async ({ page }) => { + const result = await page.evaluate(async () => { + const client = await window.MidenClient.createMock(); + const wallet = await client.accounts.create(); + const allowed = await client.accounts.isAllowed(wallet); + let code = null; + let message = null; + try { + await client.accounts.register({ + account: wallet, + invitationCode: "invitation-code", + }); + } catch (error) { + code = error.code ?? null; + message = String(error.message ?? error); + } + let emptyCodeMessage = null; + try { + await client.accounts.register({ + account: wallet, + invitationCode: "", + }); + } catch (error) { + emptyCodeMessage = String(error.message ?? error); + } + return { allowed, code, message, emptyCodeMessage }; + }); + // The mock node enforces no allowlist, so every account is allowed and + // the client keeps the invitation code rather than spending it. + expect(result.allowed).toBe(true); + expect(result.code).toBe("ACCOUNT_ALREADY_ALLOWED"); + expect(result.message).toContain("already allowed"); + expect(result.emptyCodeMessage).toContain("invitationCode"); + } + ); + mockTest("accounts.list returns created accounts", async ({ page }) => { const result = await page.evaluate(async () => { const client = await window.MidenClient.createMock(); @@ -1241,27 +1279,46 @@ nodeTest.describe("MidenClient API - Integration", () => { } ); - // The tripwire for every quickstart that now has to pass `feeFaucetId`: while - // KNOWN_FEE_FAUCETS is empty a client without one must fail, and fail naming - // the option. When a 0.17 network publishes its genesis and the table gains an - // entry, this test fails - and the docs that call the option mandatory are - // what has to change with it. + // The client gets the chain's protocol configuration from the node when it + // syncs, so a client created without `feeFaucetId` executes like any other + // and reports the chain's own faucet once synced. nodeTest( - "creating a client without a fee faucet fails and names the option", + "a client without a fee faucet syncs the protocol configuration and reports the chain's faucet", async ({ page }) => { - const message = await page.evaluate(async () => { + const result = await page.evaluate(async () => { + const client = await window.MidenClient.create({ + rpcUrl: window.rpcUrl, + storeName: "miden_client_api_synced_fee_faucet_test", + }); + + let beforeSync = null; try { - await window.MidenClient.create({ - rpcUrl: window.rpcUrl, - storeName: "miden_client_api_no_fee_faucet_test", - }); - return null; + await client.feeFaucetId(); } catch (err) { - return String(err?.message ?? err); + beforeSync = String(err?.message ?? err); } + + await client.sync(); + + const canonical = (id) => { + try { + return window.AccountId.fromBech32(id).toString(); + } catch { + return window.AccountId.fromHex(id).toString(); + } + }; + + return { + beforeSync, + configured: canonical(window.feeFaucetId), + reported: (await client.feeFaucetId()).toString(), + }; }); - expect(message).toContain("feeFaucetId"); + // Before the first sync nothing names the faucet, and the error says what + // to do about it. + expect(result.beforeSync).toContain("feeFaucetId"); + expect(result.reported).toBe(result.configured); } ); diff --git a/docs/external/src/web-client/library/allowlist.md b/docs/external/src/web-client/library/allowlist.md new file mode 100644 index 00000000..c4cb284f --- /dev/null +++ b/docs/external/src/web-client/library/allowlist.md @@ -0,0 +1,97 @@ +--- +title: Account Allowlist +sidebar_position: 40 +--- + +# Registering Accounts on an Allowlisted Network + +A network that enforces an account allowlist creates an account on chain only +once the account is registered. Registration binds an invitation code, issued +by the network operator, to the account ID. It does not create the account: the +account's first transaction does that, and the node rejects the transaction +when the account is not registered. Only creation is gated. An account that +already exists on chain is never checked, and network accounts are exempt. + +## Checking whether an account is allowed + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.create({ rpcUrl }); +const wallet = await client.accounts.create(); + +const allowed = await client.accounts.isAllowed(wallet); +``` + +`isAllowed` answers `true` when the node does not enforce an allowlist, or when +the account is registered. + +## Registering an account + +```typescript +try { + await client.accounts.register({ account: wallet, invitationCode }); +} catch (error) { + switch (error.code) { + case "ACCOUNT_ALREADY_ALLOWED": + // The node already allows the account, or enforces no allowlist. The + // code was not sent, so keep it for another account. + break; + case "INVITATION_NOT_FOUND": + case "ALREADY_REGISTERED": + case "INVALID_REGISTRATION_REQUEST": + // The node rejected the registration. + break; + default: + throw error; + } +} +``` + +The account must be tracked by the client, must not be deployed on chain yet, +and must not be a network account. A registration consumes the code, so the +client asks the node first and does not send the code for an account the node +already allows. + +When the network operator runs a funding service, the node pays the registered +account a public P2ID note with the native asset and answers once that note is +committed, so `register` can take a few blocks. The note is not part of the +response. It arrives with the next `sync()`, and consuming it is the first +transaction of the account, which creates the account on chain and pays its fee +out of the received funds: + +```typescript +await client.sync(); +await client.transactions.consumeAll({ account: wallet }); +``` + +## Submitting for an unregistered account + +`transactions.send`, `transactions.consume` and every other submission that +would create an account the network does not accept fail with the code +`ACCOUNT_NOT_ALLOWLISTED` before anything is proven or sent. Register the +account and submit again. + +Error codes are the `code` property of the thrown error on the WASM build. The +Node.js binding reports the same reason in the error message. + +## Registering without a tracked account + +`RpcClient` exposes the two node endpoints directly, for a registration flow +that never holds the account's state, such as an onboarding service that +registers IDs its users hand it: + +```typescript +import { Endpoint, RpcClient, AccountId } from "@miden-sdk/miden-sdk"; + +const rpc = new RpcClient(new Endpoint(rpcUrl)); +const accountId = AccountId.fromBech32(address); + +if (!(await rpc.isAccountAllowed(accountId))) { + await rpc.registerAccount(accountId, invitationCode); +} +``` + +Unlike `accounts.register`, `RpcClient.registerAccount` sends the request as +given: it does not check that the account is new, and it does not ask the node +first, so a code spent on an account the node already allows is consumed. diff --git a/packages/adapter/all/package.json b/packages/adapter/all/package.json index 035bcf2e..cc8dccc2 100644 --- a/packages/adapter/all/package.json +++ b/packages/adapter/all/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/miden-wallet-adapter", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Modular TypeScript wallet adapters and React components for Miden applications.", "type": "module", "module": "dist/index.js", diff --git a/packages/adapter/base/package.json b/packages/adapter/base/package.json index 3e831676..31c20d76 100644 --- a/packages/adapter/base/package.json +++ b/packages/adapter/base/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/miden-wallet-adapter-base", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Core infrastructure for connecting Miden-compatible wallets to your dApp.", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -35,7 +35,7 @@ "url": "https://github.com/0xMiden/web-sdk/issues" }, "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1" + "@miden-sdk/miden-sdk": "^0.17.0-rc.2" }, "homepage": "https://github.com/0xMiden/web-sdk" } diff --git a/packages/adapter/miden/package.json b/packages/adapter/miden/package.json index c2846f84..8d6ea438 100644 --- a/packages/adapter/miden/package.json +++ b/packages/adapter/miden/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/miden-wallet-adapter-miden", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Miden wallet adapter for the Miden Wallet.", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -31,7 +31,7 @@ "vitest": "^1.0.0" }, "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1", + "@miden-sdk/miden-sdk": "^0.17.0-rc.2", "typescript": "^5.0.0" }, "bugs": { diff --git a/packages/adapter/react/package.json b/packages/adapter/react/package.json index 202e8793..a778aa8e 100644 --- a/packages/adapter/react/package.json +++ b/packages/adapter/react/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/miden-wallet-adapter-react", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Core react infrastructure for connecting Miden-compatible wallets to your dApp.", "exports": { ".": { @@ -46,10 +46,10 @@ "vitest": "^1.0.0" }, "peerDependencies": { - "@miden-sdk/react": "^0.17.0-rc.1", + "@miden-sdk/react": "^0.17.0-rc.2", "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", - "@miden-sdk/miden-sdk": "^0.17.0-rc.1" + "@miden-sdk/miden-sdk": "^0.17.0-rc.2" }, "peerDependenciesMeta": { "@miden-sdk/react": { diff --git a/packages/adapter/reactui/package.json b/packages/adapter/reactui/package.json index 1bd5fa97..002eea1b 100644 --- a/packages/adapter/reactui/package.json +++ b/packages/adapter/reactui/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/miden-wallet-adapter-reactui", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "React UI Components for connecting Miden-compatible wallets to your dApp.", "module": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/create/package.json b/packages/create/package.json index 0ecd07c1..00e522bf 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/create", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Point your AI coding agent at version-matched Miden guidance. Run `npm create @miden-sdk@latest` in a project using the Miden web SDK.", "type": "commonjs", "main": "dist/index.js", diff --git a/packages/node-sdk-darwin-arm64/package.json b/packages/node-sdk-darwin-arm64/package.json index 6d9697cd..08820a4e 100644 --- a/packages/node-sdk-darwin-arm64/package.json +++ b/packages/node-sdk-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/node-darwin-arm64", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Miden Client SDK native module for macOS ARM64 (Apple Silicon)", "os": [ "darwin" diff --git a/packages/node-sdk-darwin-x64/package.json b/packages/node-sdk-darwin-x64/package.json index 426d768e..4eafa701 100644 --- a/packages/node-sdk-darwin-x64/package.json +++ b/packages/node-sdk-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/node-darwin-x64", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Miden Client SDK native module for macOS x64 (Intel)", "os": [ "darwin" diff --git a/packages/node-sdk-linux-x64-gnu/package.json b/packages/node-sdk-linux-x64-gnu/package.json index b3fe3e47..b3c93611 100644 --- a/packages/node-sdk-linux-x64-gnu/package.json +++ b/packages/node-sdk-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/node-linux-x64-gnu", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Miden Client SDK native module for Linux x64 (glibc)", "os": [ "linux" diff --git a/packages/para/core/package.json b/packages/para/core/package.json index 1c36d296..dcf7ab89 100644 --- a/packages/para/core/package.json +++ b/packages/para/core/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/para", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "type": "commonjs", "description": "Miden x Para Integration", "license": "MIT", @@ -33,7 +33,7 @@ }, "peerDependencies": { "@getpara/web-sdk": "^3.18.0", - "@miden-sdk/miden-sdk": "^0.17.0-rc.1" + "@miden-sdk/miden-sdk": "^0.17.0-rc.2" }, "exports": { ".": { diff --git a/packages/para/create/package.json b/packages/para/create/package.json index 77ccd1a1..800898de 100644 --- a/packages/para/create/package.json +++ b/packages/para/create/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/create-para-react", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Create a Vite react-ts app preconfigured with Miden + Para's Vite setup", "type": "module", "bin": "./bin/create-miden-para-react.mjs", diff --git a/packages/para/react/package.json b/packages/para/react/package.json index 692eb250..e369cc2f 100644 --- a/packages/para/react/package.json +++ b/packages/para/react/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/para-react", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "React hook that wires Para accounts into a Miden client", "license": "MIT", "author": "Miden Contributors", @@ -56,9 +56,9 @@ "peerDependencies": { "@getpara/react-sdk-lite": "^3.18.0", "@getpara/web-sdk": "^3.18.0", - "@miden-sdk/para": "^0.17.0-rc.1", - "@miden-sdk/miden-sdk": "^0.17.0-rc.1", - "@miden-sdk/react": "^0.17.0-rc.1", + "@miden-sdk/para": "^0.17.0-rc.2", + "@miden-sdk/miden-sdk": "^0.17.0-rc.2", + "@miden-sdk/react": "^0.17.0-rc.2", "@tanstack/react-query": "^5.0.0", "react": "^18.0.0 || ^19.0.0", "vite-plugin-node-polyfills": ">=0.23.1 <1.0.0" diff --git a/packages/react-sdk/README.md b/packages/react-sdk/README.md index 7886f1b8..271039d1 100644 --- a/packages/react-sdk/README.md +++ b/packages/react-sdk/README.md @@ -107,7 +107,7 @@ import { MidenProvider, useMiden, useCreateWallet, useAccounts } from '@miden-sd function App() { return ( - + ); @@ -154,10 +154,10 @@ function App() { // RPC endpoint (defaults to testnet). You can also use 'devnet' or 'testnet'. rpcUrl: 'devnet', - // REQUIRED: the faucet the chain mints its fee asset from, bech32 or hex. - // Since 0.17 the fee asset lives in a protocol configuration the node does - // not serve over RPC, and the SDK carries a default for no network yet, so - // a provider without this fails at client init. + // Optional: the faucet the chain mints its fee asset from, bech32 or hex. + // The client receives the chain's protocol configuration, which names the + // fee asset, from the node when it syncs; this only sets what + // `client.feeFaucetId()` reports before that first sync. feeFaucetId: FEE_FAUCET, // Auto-sync interval in milliseconds (default: 15000) diff --git a/packages/react-sdk/examples/wallet/package.json b/packages/react-sdk/examples/wallet/package.json index 463f3d3c..541bae03 100644 --- a/packages/react-sdk/examples/wallet/package.json +++ b/packages/react-sdk/examples/wallet/package.json @@ -9,16 +9,16 @@ "preview": "vite preview" }, "dependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1", - "@miden-sdk/miden-wallet-adapter-react": "^0.17.0-rc.1", - "@miden-sdk/para-react": "^0.17.0-rc.1", - "@miden-sdk/react": "^0.17.0-rc.1", - "@miden-sdk/turnkey-react": "^0.17.0-rc.1", + "@miden-sdk/miden-sdk": "^0.17.0-rc.2", + "@miden-sdk/miden-wallet-adapter-react": "^0.17.0-rc.2", + "@miden-sdk/para-react": "^0.17.0-rc.2", + "@miden-sdk/react": "^0.17.0-rc.2", + "@miden-sdk/turnkey-react": "^0.17.0-rc.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@miden-sdk/vite-plugin": "^0.17.0-rc.1", + "@miden-sdk/vite-plugin": "^0.17.0-rc.2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 9fbe1e6a..6f4fdc79 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/react", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "React hooks for the Miden web SDK. Run `npm create @miden-sdk@latest` to point your AI coding agent at version-matched guidance.", "type": "module", "module": "dist/index.mjs", @@ -44,7 +44,7 @@ "test:all": "VITE_CJS_IGNORE_WARNING=1 vitest run && playwright test" }, "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1", + "@miden-sdk/miden-sdk": "^0.17.0-rc.2", "@types/react": ">=18.0.0", "react": ">=18.0.0" }, diff --git a/packages/react-sdk/src/types/index.ts b/packages/react-sdk/src/types/index.ts index 44092e8b..0a5cd5d0 100644 --- a/packages/react-sdk/src/types/index.ts +++ b/packages/react-sdk/src/types/index.ts @@ -109,9 +109,9 @@ export interface MidenConfig { /** * Faucet of the chain's fee asset, as a bech32 address or a hex account ID. * - * Required for a network the SDK knows no fee faucet for. Since 0.17 the fee asset lives in - * the protocol configuration rather than the block header, and a client that cannot build one - * can neither execute nor screen notes. + * Optional. Since 0.17 the fee asset lives in the protocol configuration, which the client + * receives from the node when it syncs, so execution does not need this. It only sets what + * `client.feeFaucetId()` reports before the first sync. */ feeFaucetId?: string; /** Auto-sync interval in milliseconds. Set to 0 to disable. Default: 15000ms */ diff --git a/packages/telemetry-otel/package.json b/packages/telemetry-otel/package.json index 6abf47b7..3f848bf5 100644 --- a/packages/telemetry-otel/package.json +++ b/packages/telemetry-otel/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/telemetry-otel", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Opt-in OpenTelemetry binding for Miden SDK observations", "type": "module", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ "test:coverage": "vitest run --coverage" }, "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1" + "@miden-sdk/miden-sdk": "^0.17.0-rc.2" }, "devDependencies": { "@miden-sdk/miden-sdk": "workspace:*", diff --git a/packages/telemetry-sentry/package.json b/packages/telemetry-sentry/package.json index 65e8ee8e..99564b05 100644 --- a/packages/telemetry-sentry/package.json +++ b/packages/telemetry-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/telemetry-sentry", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Opt-in Sentry binding for Miden SDK observations", "type": "module", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ "test:coverage": "vitest run --coverage" }, "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1" + "@miden-sdk/miden-sdk": "^0.17.0-rc.2" }, "devDependencies": { "@miden-sdk/miden-sdk": "workspace:*", diff --git a/packages/turnkey/core/package.json b/packages/turnkey/core/package.json index 8748fbf3..f4adeba9 100644 --- a/packages/turnkey/core/package.json +++ b/packages/turnkey/core/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/turnkey", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "type": "commonjs", "description": "Miden Turnkey Integration SDK", "main": "./dist/cjs/index.js", @@ -66,6 +66,6 @@ }, "homepage": "https://github.com/0xMiden/web-sdk", "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1" + "@miden-sdk/miden-sdk": "^0.17.0-rc.2" } } diff --git a/packages/turnkey/create/package.json b/packages/turnkey/create/package.json index 03c1696b..2e1a10a5 100644 --- a/packages/turnkey/create/package.json +++ b/packages/turnkey/create/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/create-turnkey-react", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "CLI to scaffold a React + Vite app with Miden Turnkey integration", "type": "module", "bin": "./bin/create-miden-turnkey-react.mjs", diff --git a/packages/turnkey/react/package.json b/packages/turnkey/react/package.json index 06a93ab7..5c71cdd2 100644 --- a/packages/turnkey/react/package.json +++ b/packages/turnkey/react/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/turnkey-react", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "React hook for Miden Turnkey integration", "type": "commonjs", "main": "./dist/index.js", @@ -41,9 +41,9 @@ "access": "public" }, "peerDependencies": { - "@miden-sdk/miden-sdk": "^0.17.0-rc.1", - "@miden-sdk/turnkey": "^0.17.0-rc.1", - "@miden-sdk/react": "^0.17.0-rc.1", + "@miden-sdk/miden-sdk": "^0.17.0-rc.2", + "@miden-sdk/turnkey": "^0.17.0-rc.2", + "@miden-sdk/react": "^0.17.0-rc.2", "@turnkey/core": "^1.8.2", "@turnkey/react-wallet-kit": "^1.6.2", "@turnkey/sdk-browser": "^5.13.4", diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 002491fc..35017a6c 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@miden-sdk/vite-plugin", - "version": "0.17.0-rc.1", + "version": "0.17.0-rc.2", "description": "Vite plugin for Miden dApps: WASM dedup, COOP/COEP headers and a gRPC-web proxy. Run `npm create @miden-sdk@latest` to set up agent guidance.", "type": "commonjs", "main": "dist/index.js",