Client Changes
A SQLite store written by 0.16, or by any 0.17 release candidate, is rejected when it opens, inside ClientBuilder::build: store is at schema version 2, which is newer than the highest version this client supports (1) (3 for a 0.17.0-rc.5 store). The store is older, not newer: 0.17.0 squashed its migrations into one. There is no migration path: delete the database together with its -wal and -shm files, and re-sync. Browser applications upgrading from 0.16.3 are handled automatically, as in 0.16: the IndexedDB store detects the version bump and deletes the whole database on first open. The default browser keystore keeps its secret keys in that database, so the reset deletes every key it holds. Back the keys up on 0.16.3 before upgrading (see the Web SDK steps in (Store)); only an app with an external keystore (keystore callbacks) keeps its keys. An IndexedDB store created by a 0.17 release candidate is not reset: 0.17.0 keeps it, fails to decode it, and no later 0.17.x resets it either, so delete it yourself after backing up its keys. In every case any state that existed only locally is lost, including private account state and notes not yet on chain.
AccountFile and NoteFile are Protobuf-encoded in 0.17, and there is no reader for the 0.16 format. .mac / .mno files exported by a 0.16 client, and AccountFile / NoteFile bytes serialized by Web SDK 0.16, fail to decode. The 0.16 guide's advice to export private note files before upgrading does not work for this upgrade. Consume private notes on 0.16 before upgrading, or have the sender re-send them once both sides run 0.17. What carries over is the filesystem keystore directory (CLI, Rust client, Node.js) and the CLI's miden-client.toml; the default browser keystore does not.
A 0.17.0 client talks only to a stable 0.17 node: the node matches major.minor and the pre-release label, so a 0.16 node and any 0.17 release-candidate node reject it. The remote prover and note transport gRPC services changed as well, so both services must be upgraded to 0.17. See (Node) Client, node, remote prover and note transport must all be 0.17.
Quick Fix
# CLI: delete the old store of the ACTIVE config and refresh its bundled packages; miden-client.toml
# and the keystore carry over. Run it where you normally run miden-client: the CLI uses
# ./.miden/miden-client.toml if it exists, else $MIDEN_CLIENT_HOME, else ~/.miden
if [ -f .miden/miden-client.toml ]; then dir="$PWD/.miden"; else dir="${MIDEN_CLIENT_HOME:-$HOME/.miden}"; fi
cfg="$dir/miden-client.toml"
# Requires Python 3.11+ (tomllib). Parse TOML before any destructive command.
paths=$(python3 - "$cfg" <<'PYTHON'
import sys
import tomllib
from pathlib import Path
config = Path(sys.argv[1]).absolute()
with config.open('rb') as file:
data = tomllib.load(file)
paths = []
for key in ('store_filepath', 'package_directory'):
value = data.get(key)
if not isinstance(value, str) or not value.strip() or '\n' in value or '\r' in value:
raise SystemExit(f'{key} must be a nonempty, single-line TOML string; stop before deleting or copying')
path = Path(value)
if not path.is_absolute():
path = config.parent / path
path = path.resolve()
if path == Path(path.anchor) or path == config.parent.resolve():
raise SystemExit(f'{key} resolves to a directory root; inspect the configuration manually')
paths.append(path)
if paths[0].is_dir() or paths[0] == paths[1]:
raise SystemExit('The store must be a file path distinct from the package directory')
for path in paths:
print(path)
PYTHON
) || exit 1
store=$(printf '%s\n' "$paths" | sed -n '1p')
pkgs=$(printf '%s\n' "$paths" | sed -n '2p')
echo "$cfg | $store | $pkgs" # check these three paths before running the next block
set -eu
: "${store:?Run and verify the path-resolution block first}"
: "${pkgs:?Run and verify the path-resolution block first}"
rm -f "$store" "$store-wal" "$store-shm"
tmp=$(mktemp -d) && (cd "$tmp" && miden-client init --local >/dev/null) \
&& mkdir -p "$pkgs" && cp -R "$tmp/.miden/packages/." "$pkgs/" && rm -rf "$tmp"
# point [rpc] endpoint AND [note_transport] endpoint in "$cfg" at 0.17 services, then
miden-client sync
To confirm which directory the CLI loads: on 0.16, before upgrading, miden-client info prints Config directory: <dir> (Local) or (Global). With 0.17.0, RUST_LOG=miden_client_cli=debug miden-client keys --list prints Loaded configuration from <dir> (Local|Global) without opening the store.
// Rust: relay a private note only with its inclusion proof, after its transaction committed and synced
client.send_private_note_with_proof(note, &address, inclusion_proof).await?;
// Rust: the fee faucet comes from the synced protocol configuration, not the block header
client.sync_state().await?;
let header = client.get_latest_block_header().await?;
let fee_faucet_id = client
.get_protocol_config(header.protocol_config_commitment())
.await?
.fee_asset_id()
.faucet_id();
# Web: back up browser-keystore keys on 0.16.3 first (see (Store)), then bump every @miden-sdk/* package together
npm install @miden-sdk/[email protected] @miden-sdk/[email protected]
// Web: point rpcUrl and noteTransportUrl at 0.17 services, sync, then read the fee faucet
const client = await MidenClient.create({ rpcUrl: "devnet", noteTransportUrl: "devnet" });
await client.sync();
const feeFaucet = await client.feeFaucetId(); // replaces header.feeFaucetId()
// Private notes: sendPrivate takes inclusionProof instead of scanAfterBlockNum, and
// sendPrivateOutput({ noteId, to }) works only once the note's commit is synced
If you encounter errors, continue reading for detailed migration steps.
Summary
The client changes fall into five groups. Local state does not survive the upgrade: the SQLite store (rejected at open with a misleading "schema version ... newer" error), the IndexedDB store (with the default browser keystore's secret keys), exported .mac / .mno files and the CLI's bundled .miden/packages all have to be recreated or backed up first. Deployment pairing is stricter: client, node, remote prover and note transport service must all be stable 0.17, and a 0.17 node may require an invitation code before it creates a new account. The fee faucet left the block header: both the Rust client and the Web SDK now read it from the protocol configuration the node delivers on sync, so sync before executing. API churn (ValidatorKeys → ValidatorConfig, ProvingOptions → Prover, AccountType.FungibleFaucet → FaucetType.FungibleFaucet, exec --script-path → exec --package, private notes relayed only with an inclusion proof, a test-only ClientBuilder::rng, new Store and transport trait methods, removed and added error variants) fails to compile or fails loudly. And several silent behavioural changes will not fail your build: multisig requests built with fee_conversion_salt fail at execution, expiration_delta now expires consume-only requests, a note stranded by a discarded transaction is refused, a broken note transport or one bad delivery no longer fails sync_state but stalls private notes, the keystore returns an empty set instead of an error, the same Web SDK seed draws different randomness, and the Web SDK drops block-locked notes from "available" lists.
(Store) Every 0.16 SQLite store must be recreated
Summary
0.17.0 rejects every SQLite store written by 0.16 or by a 0.17 release candidate when it opens. Its migrations were squashed into one, so it knows schema version 1 only, while stores from 0.16.x and 0.17.0-rc.1 to rc.4 are at version 2 and a 0.17.0-rc.5 store at version 3. The migration layer reports such a store as too new, and SqliteStore::new fails before anything is read. The data could not be read anyway: block headers, accounts and notes are now stored as Protobuf (the new miden-client-proto crate), and the protocol objects themselves changed, for example BlockHeader replaced tx_kernel_commitment / validator_keys with validator_config, protocol_config_commitment and next_protocol_config, and FeeParameters lost the fee faucet.
Affected Code
The 0.17.0 CLI on a 0.16 store fails on every command that builds a client (account, notes, tx, info, sync, new-wallet) with exit code 1:
Error: cli::client_error
× client error
├─▶ storage error
╰─▶ database-related non-query error: store is at schema version 2, which is newer than the highest version this client supports (1)
In Rust the same store makes ClientBuilder::build().await return ClientError::StoreError(StoreError::DatabaseError(_)) with that message, and SqliteStore::new(path) returns the StoreError directly. The check runs before any data is read, so a store that never synced fails the same way.
The store is older than the client, not newer: 0.17.0 numbers its schema from 1 again. Expect 2 for a store from 0.16.x or 0.17.0-rc.1 to rc.4, and 3 for one from 0.17.0-rc.5. The changelog says only that a new client database is required.
Migration Steps
Rust and CLI (SQLite store)
- Delete the store file together with its
-waland-shmfiles (the client opens SQLite in WAL mode) and let the client recreate it, thensync. In Rust that is the path you pass tosqlite_store(path)orSqliteStore::new(path). For the CLI, delete the store of the configuration the CLI actually loads:./.miden/miden-client.tomlwhen it exists, otherwise$MIDEN_CLIENT_HOME/miden-client.tomlwhen the variable is set, otherwise~/.miden/miden-client.toml, with a relativestore_filepathresolved against that directory. A fixed~/.miden/store.sqlite3can be another project's store. The Quick Fix script resolves the path, and the line after it shows how to confirm the directory. - Do not plan to carry private accounts over with
export: 0.16.mac/.mnofiles do not decode in 0.17 either. A 0.17 client also cannot talk to the 0.16 network the old store was synced against, so plan to recreate accounts on the 0.17 network. - Keep the keystore directory. A 0.16 filesystem keystore (key files plus
key_index.json) loads unchanged in 0.17, andmiden-client keys --listshows the old keys and their account associations. A recreated account gets a new ID. The CLI can commit it to a kept ECDSA key's public key (new-wallet --ecdsa <PUBLIC_KEY>), but--falconalways generates a new key. - Keep
miden-client.toml. A 0.16 CLI config parses under 0.17 and the CLI creates a fresh store next to it. Point[rpc] endpointat a 0.17 node and[note_transport] endpointat a transport that servesmiden.note_transport.v1.NoteTransportService. With a transport that serves only the 0.16 service,synckeeps succeeding while private notes stop arriving. Then refresh.miden/packages(see (CLI) Re-create.miden/packages). - If you implement a custom
Store, see the custom-store bullet in (Rust) Other library changes.
Web SDK (IndexedDB store)
-
Back up the browser keystore while the app still runs 0.16.3. The reset deletes the whole database, and the default browser keystore stores its secret keys in it (table
accountAuth), so the first 0.17 open deletes every key it holds. Rolling back afterwards cannot bring them back. Apps that pass an external keystore (keystore: { getKey, insertKey, sign }) keep their keys outside IndexedDB.// Before upgrading, still on 0.16.3: the first 0.17 open deletes these keys
const backup: { accountId: string; secretKeys: Uint8Array[] }[] = [];
for (const header of await client.accounts.list()) {
const accountId = header.id();
const secretKeys: Uint8Array[] = [];
for (const commitment of await client.keystore.getCommitments(accountId)) {
const key = await client.keystore.get(commitment);
if (key) secretKeys.push(key.serialize());
}
backup.push({ accountId: accountId.toString(), secretKeys });
}
// Keep `backup` outside IndexedDB, and encrypted: it holds secret keysThe key encoding did not change, so
AuthSecretKey.deserialize(bytes)reads these bytes in 0.17, andclient.keystore.insert(accountId, key)stores a key again. A 0.16exportStoredump also holds the keys (hex, inaccountAuth), but see step 3. -
Expect the store to be deleted on first open. The SDK logs:
IndexedDB client version mismatch (stored=0.16.3, expected=0.17.0). Resetting store.Re-sync from scratch, and recover accounts from the keys you backed up, or from seeds, not from 0.16 exports.
-
Do not carry a 0.16
exportStoredump into a 0.17 client withimportStore: the import copies tables verbatim, including the storedclientVersion. -
Rolling back from 0.17 to 0.16 does not wipe the store (a stored version newer than the client only rewrites the version). Delete the
MidenClientDB_<network ID>database (MidenClientDB_mtston testnet,MidenClientDB_mdevon devnet), or yourstoreName, yourself. -
The reset fires only when the stored version is older and a different major.minor, so moving between 0.17 patch releases keeps the store. That includes a store created by a 0.17 release candidate, which 0.17.0 cannot read: the first open records
0.17.0as the store's version and client creation fails withFailed to create client: storage error: failed to deserialize data from the store: ...(the cause underneath depends on the bytes; for an rc.5 store it ends ininvalid tag value: 0). No later 0.17.x resets it. On the rc build, back up the keys as in step 1, then delete the database before the first 0.17.0 open (or after the failure), re-sync and re-import the accounts from the backed-up keys:await new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase("MidenClientDB_mtst"); // testnet default, or your storeName
request.onsuccess = () => resolve(undefined);
request.onerror = () => reject(request.error);
}); -
Point
noteTransportUrlat a 0.17 transport as well asrpcUrlat a 0.17 node.createTestnet()andnoteTransportUrl: "testnet"resolve tohttps://transport.miden.io. -
Node.js: the Node entry uses a SQLite store that the SDK never resets:
~/.miden/stores/<storeName>/<storeName>.dbwhen you passstoreName, and a fresh temporary directory otherwise. A 0.16 or 0.17 rc.dbfails with the schema-version error above. Delete the.dbfile (and its-wal/-shmfiles) yourself; thekeystoredirectory next to it is a filesystem keystore and carries over.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
database-related non-query error: store is at schema version 2, which is newer than the highest version this client supports (1) (3 for a 0.17.0-rc.5 store) | A SQLite store written by 0.16 or by a 0.17 release candidate | Delete the store with its -wal and -shm files and re-sync. |
Failed to create client: storage error: failed to deserialize data from the store: ... (Web) | An IndexedDB store created by a 0.17 release candidate | Delete the database and re-import accounts from backed-up keys. |
(Node) Client, node, remote prover and note transport must all be 0.17
Summary
Every RPC call carries accept: application/vnd.miden; version=<miden-client crate version>[; genesis=<hex>]. The node accepts it only if major.minor match its own version and the pre-release label matches: a stable node accepts stable clients of any patch, and a release-candidate node accepts only release-candidate clients of its exact major.minor.patch (the rc number is ignored).
| Client | 0.16.x node | 0.17.0-rc node | Stable 0.17.x node |
|---|---|---|---|
| 0.16.1 | accepted | rejected | rejected |
| 0.17.0 | rejected | rejected | accepted (any 0.17 patch) |
A client built on a 0.17 release candidate (for example Web SDK 0.17.0-rc.5) is rejected by a stable 0.17 node in the same way. The node's own gRPC service is now miden.node.v1.NodeService (was rpc.Api), which matters only to code calling miden_client::rpc::generated or raw gRPC-web paths.
Two more services must match the client:
- Remote prover. The service is
miden.remote_prover.v1.ProverServicewithProveRequest/ProveResponse, typedoneofmessages, instead ofremote_prover.ApiwithProofRequest/Proof(aproof_typeenum plus an opaquebytes payload). The remote prover has no version negotiation, so a 0.17.0 client calling an older prover fails with a gRPC error (typicallyUnimplemented) rather than a version error. The endpoint constants (TESTNET_PROVER_ENDPOINT,DEVNET_PROVER_ENDPOINT, newMAINNET_PROVER_ENDPOINT) keep their URLs; the service behind them must match. - Note transport. The client now speaks
miden.note_transport.v1.NoteTransportService(SendNoteWithProof,FetchNotes; defined in the node repository) instead ofmiden_note_transport.MidenNoteTransport. The endpoint URLs are unchanged, so the server behind them must be upgraded. A transport failure no longer failssync_state, so a mismatched transport server is silent; see (Rust) Note transport.
Affected Code
# What the node returns (gRPC status 3, InvalidArgument)
server does not support any of the specified application/vnd.miden content types
# What the Rust client surfaces (ClientError -> RpcError -> AcceptHeaderError)
RPC error
accept header validation failed
server rejected request - please check your version and network settings (client version: 0.17.0, genesis commitment: none)
The Web SDK surfaces the same rejection as Failed to ensure genesis in place: ... accept header validation failed.
Migration Steps
- Point a 0.17.0 client at a stable 0.17 node, public or a local node from the matching release. A 0.16 node and a 0.17 release-candidate node reject it.
- Upgrade every client in the graph together (Rust client, CLI, Web SDK, any prover crate pinning
miden-client), and move every client off release candidates. A mixed graph fails at the first RPC. - For a local node, run node
0.17.0, not a 0.17.0-rc. - Use a remote prover from the 0.17 node release (it serves
miden.remote_prover.v1.ProverService), and a note transport server that servesmiden.note_transport.v1.NoteTransportService. Switch the transport endpoint together with the RPC endpoint: the CLI's[note_transport] endpoint(init --network devnetor--note-transport-endpoint <URL>sets it), the Web SDK'snoteTransportUrl, or in RustClientBuilder::for_devnet()orClientBuilder::note_transport(..). - The node still keeps account state for only 50 blocks; that window did not change.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
server rejected request - please check your version and network settings (client version: 0.17.0, genesis commitment: ...) | Client and node differ in major.minor, or one of them is a release candidate and the other is not | Use a stable 0.17 node with a stable 0.17 client. |
Failed to ensure genesis in place: ... accept header validation failed (Web) | Same | Same. |
(Node) New accounts may need an invitation code
Summary
A 0.17 node creates a new (non-network) account on chain only if it is registered on the node's allowlist, unless the operator runs it with --disable-account-allowlist (MIDEN_NODE_DISABLE_ACCOUNT_ALLOWLIST). Before submitting a transaction that creates such an account (after it is proven), or before proving a batch that does, the client asks the node and fails with ClientError::AccountNotAllowlisted. Register the account with an invitation code first. Existing on-chain accounts and network accounts are never gated.
Affected Code
// New in 0.17 (Rust): register a tracked, not-yet-deployed, non-network account before its first transaction
client.sync_state().await?; // also puts genesis in place, which RegisterAccount requires
if !client.is_account_allowed(account_id).await? {
client.register_account(account_id, invitation_code).await?;
}
// If the operator funds registrations: sync until the funding P2ID note arrives, then consume it in the
// account's first transaction.
# New in 0.17 (CLI)
miden-client new-wallet
miden-client sync # puts genesis in place
miden-client account --register <ACCOUNT_ID> --invitation-code <CODE>
miden-client sync # if the network funds registrations
miden-client consume-notes --account <ACCOUNT_ID> # the funding note creates the account on chain
// New in 0.17 (Web)
if (!(await client.accounts.isAllowed(account))) {
await client.accounts.register({ account, invitationCode });
}
Migration Steps
- On a network that enforces the allowlist, obtain an invitation code and register before the account's first transaction. Note the argument order:
Client::register_account(account_id, invitation_code)is the reverse ofNodeRpcClient::register_account(invitation_code, account_id). - Make sure genesis is in place first (any
sync_state): the node requires thegenesisaccept parameter forRegisterAccount, andregister_accountdoes not fetch genesis itself. - In the Web SDK, a submission that would create an account the network does not accept fails with code
ACCOUNT_NOT_ALLOWLISTEDwhen it is submitted, after the transaction has been executed and proven; checkaccounts.isAllowedfirst to avoid a wasted proof. The other registration codes areACCOUNT_ALREADY_ALLOWED,INVITATION_NOT_FOUND,ALREADY_REGISTEREDandINVALID_REGISTRATION_REQUEST. - Node operators running a dev network without invitations start the node with
--disable-account-allowlist.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
account <ID> is not registered on the network allowlist | Submitting a transaction that creates an unregistered account | Register the account first. |
account <ID> is already allowed on the network and does not need an invitation code | Already registered, or the node does not enforce the allowlist | Skip registration. |
account <ID> is a network account and does not need an invitation code | Network accounts are exempt | Skip registration. |
account <ID> is already deployed and does not need an invitation code | The account exists on chain | Skip registration. |
invitation code does not exist / the invitation code or the account is already registered | The node rejected the code | Use a valid, unused code. |
(Rust) The fee faucet comes from the synced protocol configuration
Summary
In 0.17 the block header no longer names the fee faucet; it commits to a ProtocolConfig, which carries the fee asset. The client stores each configuration it receives during Client::sync_state and looks it up by the reference block's protocol_config_commitment() for execution and for note screening. Read the fee faucet through Client::get_protocol_config. The protocol side is covered in Transaction Changes.
Affected Code
// Before (0.16): the fee faucet was part of the block header's fee parameters
let header = client.get_latest_block_header().await?;
let fee_faucet_id = header.fee_parameters().fee_faucet_id();
// After (0.17): sync first, then read the configuration the header commits to
client.sync_state().await?;
let header = client.get_latest_block_header().await?;
let config = client.get_protocol_config(header.protocol_config_commitment()).await?;
let fee_faucet_id = config.fee_asset_id().faucet_id();
ProtocolConfig is re-exported as miden_client::protocol_config::ProtocolConfig, alongside NextProtocolConfig and ProtocolConfigError.
Migration Steps
- Replace
header.fee_parameters().fee_faucet_id()withclient.get_protocol_config(header.protocol_config_commitment()).await?.fee_asset_id().faucet_id().FeeParametersnow carries onlyverification_base_fee(). - Call
client.sync_state()at least once on a new store before executing a transaction or listing consumable notes. Fetching genesis alone does not deliver a configuration, and note screening loads it as soon as there is a note to screen, soget_consumable_notesfails as well as execution. - The node sends the configuration only when a sync starts at genesis or when the configuration changed over the synced range. A store whose sync height is past genesis but holds no configuration for the current commitment never receives it, and syncing again does not help. If
protocol configuration ... is not storedpersists after a sync, delete the store and sync from genesis. There is no supported way to supply a configuration yourself;Client::seed_protocol_configexists only behind thetestingfeature, for mock chains.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
protocol configuration <commitment> is not stored; sync the client to get it from the node | Execution or note screening before the first sync_state, or a store past genesis that never received a configuration | Run sync_state; if it persists, recreate the store. |
error[E0599]: no method named `fee_faucet_id` found on &FeeParameters | The fee faucet moved to ProtocolConfig | Use get_protocol_config(..).fee_asset_id().faucet_id(). |
received an invalid response from the Miden node: node returned a protocol configuration with commitment ... for a block header that commits to ... | The node sent a configuration that does not match the header | Report it to the node operator. |
(Rust) Multisig requests need MultisigAuthArgs and block_numbers
0.17 multisig components (AuthMultisig, AuthMultisigSmart, AuthGuardedMultisig) read a three-word auth-args preimage and bind the transaction summary to a block the caller chooses instead of the reference block. The client does not build that preimage: TransactionRequestBuilder::fee_conversion_salt(salt) still commits the 0.16 two-word preimage, which the 0.17 components cannot read. Nothing fails to compile; the transaction fails at execution. Build MultisigAuthArgs yourself, set it as the auth arg with its preimage in the advice map, add the bound block with the new TransactionRequestBuilder::block_numbers, and execute at the chain tip. The full flow, with code, is in Transaction Changes.
Client-specific details:
MultisigAuthArgsandFeeConversionInforesolve frommiden_client::account::component.SequentialCommit(forto_commitment/to_elements) is not re-exported bymiden-client, so it needs a directmiden-protocoldependency.- Setting any non-empty auth arg makes the client skip its own fee commitment. Set one on a fee-free chain too: the component asserts the preimage is present whatever the base fee.
- Nothing derives the bound block from the auth args: add it with
.block_numbers([bound_block]). The client fetches that header from the node if it is not in the store. TransactionRequestnow always serializesblock_numbersfirst and the new code of an account code upgrade last, so stored request bytes from any 0.16 client, or from 0.17.0-rc.4 or earlier, do not deserialize (typicallyunexpected end of file). Rebuild and re-serialize them.chain_anchor_for_requestandexecute_transaction_atstill exist and now also track the blocks inblock_numbers. Executing at the tip is the 0.17 flow. Re-executing a proposal at an old anchor needs the state of every foreign account the transaction loads, the fee faucet included, at that block, while the node keeps account state for only 50 blocks: supply it asForeignAccount::Prefetchedinputs fetched before the node pruned that block.- Take the fee faucet from
get_protocol_config, not from the block header.
The 0.16 guide names TransactionRequestBuilder::fee_conversion_info(info, salt). In 0.16.0, 0.16.1 and 0.17.0 the builder has fee_conversion_salt(salt) instead, and the client builds the native FeeConversionInfo from the reference block itself.
(Rust) AccountFile and NoteFile moved to miden-objects, and 0.16 files no longer load
Summary
Both types now live in miden-objects and are Protobuf-encoded; the protocol side is covered in Imports & Dependencies. The client re-exports them from miden_client::account and miden_client::note, and the old miden_client::notes module is gone. AccountFile fields are private, read / write return AccountFileError / NoteFileError instead of std::io::Error, and the types no longer implement Serializable / Deserializable.
Affected Code
// Before (0.16)
use miden_client::account::AccountFile;
use miden_client::notes::NoteFile; // or miden_client::note::NoteFile
use miden_client::Deserializable;
let file = AccountFile::read_from_bytes(&bytes)?;
let account = file.account;
let keys = file.auth_secret_keys;
AccountFile::new(account, keys).write("acct.mac")?; // std::io::Result<()>
// After (0.17)
use miden_client::account::{AccountFile, AccountFileError};
use miden_client::note::{NoteFile, NoteFileError};
let file = AccountFile::try_from_bytes(&bytes)?; // Result<AccountFile, AccountFileError>
let (account, keys) = file.into_parts(); // or file.account() / file.auth_secret_keys()
AccountFile::new(account, keys).write("acct.mac")?; // Result<(), AccountFileError>
let note_file = NoteFile::read("note.mno")?; // Result<NoteFile, NoteFileError>
Migration Steps
- Replace
miden_client::notes::NoteFilewithmiden_client::note::NoteFile. - Replace
AccountFile::read_from_bytes/NoteFile::read_from_byteswithtry_from_bytes.to_bytes()is now an inherent method and still works. - Replace field access with
account(),auth_secret_keys()orinto_parts(). - Map
AccountFileError/NoteFileErrorwhere you previously expectedstd::io::Errorfromread/write. - Regenerate every exported file with a 0.17 client.
NoteFilevariants are unchanged.
The changelog says NoteSyncHint "likewise exposes after_block_num() and tag()". It already had private fields and exactly those accessors in 0.16.1; only its crate changed.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error[E0432]: unresolved import `miden_client::notes` (could not find notes in miden_client) | miden_client::notes removed | Use miden_client::note::NoteFile. |
error[E0599]: no associated function or constant named `read_from_bytes` found for struct `AccountFile` | Deserializable impl removed | Use try_from_bytes. |
error[E0616]: field `account` of struct `AccountFile` is private | Fields made private | Use account() or into_parts(). |
failed to decode the account file / failed to decode the note file | A file written by 0.16 | Re-export with 0.17, or recreate the account or note. |
(Rust) Removed and renamed re-exports
Summary
The client dropped or renamed several re-exports, and made a custom client RNG test-only. The token helpers moved into the CLI crate as private functions, so there is no public replacement.
Affected Code
- use miden_client::block::ValidatorKeys;
+ use miden_client::block::ValidatorConfig;
- use miden_client::transaction::ProvingOptions;
+ use miden_client::transaction::Prover;
- use miden_client::asset::{FungibleAssetDelta, NonFungibleAssetDelta, NonFungibleDeltaAction};
- use miden_client::crypto::{RandomCoin, SmtForest};
- use miden_client::ClientFeltRng;
+ use miden_client::ClientCryptoRng;
- use miden_client::notes::NoteFile;
+ use miden_client::note::NoteFile;
- use miden_client::utils::{base_units_to_tokens, tokens_to_base_units, TokenParseError};
// Before (0.16)
let prover = LocalTransactionProver::new(ProvingOptions::new(Poseidon2));
let builder = builder.rng(Box::new(RandomCoin::new(seed)));
// After (0.17): the same prover configuration is the default
let prover = LocalTransactionProver::default();
// ClientBuilder::rng exists only with the `testing` feature and takes a CryptoRng
let builder = builder.rng(Box::new(ChaCha20Rng::seed_from_u64(42))); // rand_chacha 0.10
Migration Steps
ValidatorKeys→ValidatorConfig. See (Rust) State sync authenticates the chain tip.ProvingOptions→Prover(frommiden-prover). UseLocalTransactionProver::default()for the standard configuration.- The vault delta types are gone because
AccountVaultDeltanow tracks whole assets (see Assets, Vault & Faucet);AccountVaultDeltaitself is still re-exported.SmtForesthas no client re-export. - Token formatting: copy
tokens_to_base_units/base_units_to_tokensfrom 0.16.1 into your code if you need them. 0.17 keeps them only aspub(crate)in the CLI. - Custom RNG: outside tests drop the
ClientBuilder::rng(..)call; the client always uses an OS-seededChaCha20Rng. With thetestingfeature it takes aBox<dyn ClientCryptoRng>(CryptoRng + Send + Sync, renamed fromClientFeltRng) instead of aFeltRngsuch asRandomCoin. Where you drew aFeltorWordfrom aRandomCoin, use arand0.10 generator with the newmiden_client::rng::{draw_felt, draw_word}.ClientRng::newis no longer public.
Prover::default() and Prover::new() select Blake3_256. LocalTransactionProver::default() uses Prover::new().with_hash_fn(Poseidon2), which is what 0.16's LocalTransactionProver::default() used. Do not translate ProvingOptions::default() to Prover::default() without checking the hash function.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error[E0432]: unresolved import `miden_client::block::ValidatorKeys` | Renamed | ValidatorConfig |
error[E0432]: unresolved import `miden_client::transaction::ProvingOptions` | Replaced | Prover, or LocalTransactionProver::default() |
error[E0432]: unresolved import `miden_client::utils::tokens_to_base_units` (a braced import of several missing items reports them in one unresolved imports error) | Moved into the CLI | Copy the helper. |
error[E0432]: unresolved import `miden_client::crypto::RandomCoin` | Re-export removed | A rand generator plus miden_client::rng::{draw_felt, draw_word}. |
error[E0599]: no method named `rng` found on ClientBuilder | rng is behind the testing feature | Drop the call, or enable testing in tests. |
(Rust) ForeignAccount::Prefetched and get_foreign_account_inputs are unchanged from 0.16.1
0.16.1's prefetched foreign accounts carry over to 0.17.0 unchanged: ForeignAccount::Prefetched(AccountInputs), From<AccountInputs> for ForeignAccount, Client::get_foreign_account_inputs(foreign_accounts, block_num) -> Result<Vec<AccountInputs>, ClientError>, TransactionRequestError::ForeignAccountNotAtReferenceBlock and the miden_client::transaction::AccountInputs re-export. 0.16.1 code that uses them compiles unchanged. The inputs open against the account tree of block_num only, so execute at exactly that block (execute_transaction_at with an anchor at it, or the sync height without one, so do not sync in between), or fail with inputs for foreign account <id> do not open against the account tree of the transaction's reference block <N>. The 0.17.0 release candidates did not have these items, and Web SDK 0.17.0 still does not expose them (see (Web) Prefetched foreign accounts removed).
New in 0.17, and separate from prefetching: Client::track_account_witness(account_id) registers an account whose witness every sync refreshes in the store, so a transaction using it as a foreign account resolves the witness locally (untrack_account_witness, tracked_account_witnesses). A public foreign account must also be tracked by the client for its code and storage to come from the store. While a registered account has no witness the node can return, for example one not in the account tree, every sync fails until it is unregistered.
(Rust) expiration_delta now applies to consume-only and bare requests
Summary
In 0.16 TransactionRequestBuilder::expiration_delta only took effect through the SendNotes script, so a request without own output notes (build_consume_notes, a PSWAP cancel, a bare build()) silently never expired: its expiration_block_num() was u32::MAX. In 0.17 such a request runs the standard ExpirationTransactionScript with the delta as its script argument, so the delta you set is enforced. CustomScript requests are unchanged: build() still rejects expiration_delta combined with a custom script (transaction script template error: Cannot set expiration delta when a custom script is set). The changelog files this under fixes, but it changes what existing code does.
Affected Code
// Same code in 0.16 and 0.17
let request = TransactionRequestBuilder::new()
.expiration_delta(10)
.build_consume_notes(notes)?;
// 0.16: executed without a transaction script; never expires
// 0.17: runs ExpirationTransactionScript; expires 10 blocks after the reference block
Migration Steps
- Audit requests that set
expiration_deltawithout own output notes: they can now expire and be discarded. - Drop the call if you relied on it being a no-op.
- After submitting, confirm the transaction reached
Committedafter a sync before treating it as done (next section). Transactions that call standards procedures reading security-sensitive foreign state (faucet transfer policies, the fee manager) also get a 20-block expiration; see Transaction Changes.
(Rust) A note held by a pending transaction is refused, and a discarded transaction strands it
Summary
Executing a request that consumes a note the store marks as being processed by a local transaction now fails before execution with TransactionRequestError::InputNoteBeingProcessed. In 0.16 the transaction was executed, proven and submitted, and only the local store update failed.
The client never moves such a note back when its transaction is discarded: sync undoes the account state of a discarded transaction, not its input notes. The new check therefore turns a note stranded by a discarded transaction into a hard refusal. This has been observed with a consume transaction the node accepted and then dropped as expired: its input notes stayed Processing, consume-notes found nothing consumable, and consuming them by ID was refused with the new error. It is more likely on 0.17 because standards procedures that read security-sensitive foreign state (faucet transfer policies, the fee manager) cap expiration at 20 blocks.
Affected Code
let tx_id = client.submit_new_transaction(account_id, request).await?; // Ok is not "landed"
client.sync_state().await?;
let records = client.get_transactions(TransactionFilter::Ids(vec![tx_id])).await?;
// check records[0].status: Pending, Committed { .. } or Discarded(cause)
Migration Steps
- Treat a successful submit as "accepted", not "committed": sync and read the transaction's status until it is
CommittedorDiscarded. - Handle
TransactionRequestError::InputNoteBeingProcessed { note, transaction_id }(reached throughClientError::TransactionRequestError) instead of retrying the same consume. - Keep the window between building a request and submitting it short when the transaction reads foreign state.
- We found no supported way in 0.17 to return a stranded note to a consumable state. Retrying the consume is refused; the observed recovery was a fresh note.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
invalid transaction request with source note with details commitment <hex> is being consumed by pending transaction <id> | The note is still marked as processing, including after its transaction was discarded | Wait for the pending transaction; if it was discarded, see step 4. |
(Rust) Note transport: sends carry an inclusion proof, new service, silent failures and a new cursor
Summary
Two API changes and four behaviour changes:
- Private notes are sent with their inclusion proof.
Client::send_private_noteandsend_private_note_with_block_hintare replaced byClient::send_private_note_with_proof(note, &address, inclusion_proof). The transport verifies the proof against its node before it stores the note and gives the recipient the exact commitment block, so a note can be relayed only after the transaction that created it is committed and the sender has synced past that block.NoteTransportClient::send_noteandsend_note_with_block_hintare replaced by one required method,send_note_with_proof(note: TransportNote, inclusion_proof).TransportNote::new(header, details)fails unless the header commits to the details;TransportNote::from(note)also works. A relay outbox entry written by an earlier version is dropped with the warningdropping unreadable relay outbox; resetting to empty. - Cursor and streaming API.
NoteTransportCursoris now a (nonce, sequence) pair, andNoteTransportClient::stream_notesand theNoteStreamtrait were removed. - New gRPC service. The client speaks
miden.note_transport.v1.NoteTransportServiceinstead ofmiden_note_transport.MidenNoteTransport, at unchanged URLs, so the server must be upgraded (see (Node) Client, node, remote prover and note transport must all be 0.17). The default endpoints areNOTE_TRANSPORT_TESTNET_ENDPOINT = "https://transport.miden.io",NOTE_TRANSPORT_DEVNET_ENDPOINT, and the newNOTE_TRANSPORT_MAINNET_ENDPOINT. - Failures no longer fail
sync_state. A transport failure insideClient::sync_stateis logged (note transport fetch failed; syncing the chain without it) and the chain sync still applies, so an incompatible or unreachable transport server is silent. CallingClient::sync_note_transportdirectly still returns the error. - A bad delivery fails the fetch. 0.16 dropped, with a warning, a delivery that did not decode or carried a tag the client had not requested. 0.17 returns
NoteTransportError::InvalidFetchedNote,NoteDetailsMismatchorUnrequestedTagfromsync_note_transport/fetch_private_notesand leaves the cursor on that page. Insidesync_statethat error is only logged (item 4), so one bad delivery silently stops private notes from arriving. - Deliveries are screened. Transport-delivered notes whose tag matches a tracked account's tag are screened, and notes no tracked account can consume are dropped instead of imported. Notes with other tags are kept as before.
Affected Code
// Before (0.16)
client.send_private_note_with_block_hint(note, &address, block_hint).await?; // or send_private_note(note, &address)
let cursor = NoteTransportCursor::new(42); // also NoteTransportCursor::from(42u64)
let raw: u64 = cursor.value();
let stream = transport.stream_notes(tag, cursor).await?;
// After (0.17): once the transaction that created the note is committed
client.sync_state().await?; // stores the note's inclusion proof
let record = client.get_output_note(note_id).await?.expect("output note");
let proof = record.inclusion_proof().cloned().expect("committed and synced");
client.send_private_note_with_proof(record.try_into()?, &address, proof).await?;
let cursor = NoteTransportCursor::from_parts(nonce, sequence); // or NoteTransportCursor::init()
let parts: Option<(u64, u64)> = cursor.parts(); // None for the initial cursor
let (notes, next) = transport.fetch_notes(&[tag], cursor).await?; // poll instead of streaming
// Custom NoteTransportClient: send_note(header, details) is replaced by send_note_with_proof, the only send method
async fn send_note_with_proof(&self, note: TransportNote, inclusion_proof: NoteInclusionProof)
-> Result<(), NoteTransportError>;
Migration Steps
- Relay a private note with
send_private_note_with_proofafter its transaction is committed and a sync has stored the proof:OutputNoteRecord::inclusion_proof()for a note this client created,InputNoteRecord::inclusion_proof()for one it received. The CLI'snotes --senddoes this for you and now looks the ID up among output notes before input notes. - Check that your transport endpoint serves
miden.note_transport.v1.NoteTransportService. - Do not rely on
sync_stateerroring to detect transport problems or bad deliveries; callsync_note_transportor watch the logs if you need to know. - Expect private notes addressed to another account with a colliding tag to no longer appear in the store.
- Replace
NoteTransportCursor::new(u64),From<u64>andvalue()withfrom_parts(nonce, sequence),init()andparts(). - Replace
stream_noteswith pollingfetch_notes. CustomNoteTransportClientimplementations: replacesend_noteandsend_note_with_block_hintwithsend_note_with_proof, and deletestream_notes.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error[E0599]: no method named `send_private_note_with_block_hint` found (or send_private_note) | Replaced | send_private_note_with_proof with the note's inclusion proof. |
error[E0046]: not all trait items implemented, missing: `send_note_with_proof` (with error[E0407] for the old send_note) | New required trait method | Implement send_note_with_proof; delete send_note. |
input error: note <hex> has no inclusion proof yet; wait for its transaction to be committed, sync and retry | CLI notes --send before the commit was synced | Wait for the commit, sync, retry. |
note transport returned an invalid note: ..., note details commitment <hex> does not match the header commitment <hex> or note transport returned a note with tag <tag>, which the client did not request | A bad delivery, from sync_note_transport / fetch_private_notes (only logged by sync_state) | Report it to the transport operator; private notes stall until it stops serving that note. |
(Rust) Custom NodeRpcClient implementations must follow the trait
Summary
The trait gained two required methods, takes submitted payloads by reference, and returns a signed block plus an optional proof. The ChainMmrInfo your sync_chain_mmr returns must now carry the chain tip's validator signatures and, when present, the protocol configuration. This only affects code that implements NodeRpcClient or builds its return types.
Affected Code
// Before (0.16)
async fn submit_proven_transaction(&self, proven_transaction: ProvenTransaction,
sealed_transaction_inputs: SealedTransactionInputs) -> Result<BlockNumber, RpcError>;
async fn submit_proven_batch(&self, proven_batch: ProvenBatch, proposed_batch: ProposedBatch,
transaction_inputs: Vec<SealedTransactionInputs>) -> Result<BlockNumber, RpcError>;
async fn get_block_by_number(&self, block_num: BlockNumber, include_proof: bool)
-> Result<ProvenBlock, RpcError>;
// After (0.17)
async fn submit_proven_transaction(&self, proven_transaction: &ProvenTransaction,
sealed_transaction_inputs: SealedTransactionInputs) -> Result<BlockNumber, RpcError>;
async fn submit_proven_batch(&self, proven_batch: &ProvenBatch, proposed_batch: &ProposedBatch,
transaction_inputs: Vec<SealedTransactionInputs>) -> Result<BlockNumber, RpcError>;
async fn get_block_by_number(&self, block_num: BlockNumber, include_proof: bool)
-> Result<(SignedBlock, Option<ExecutionProof>), RpcError>;
async fn register_account(&self, invitation_code: &str, account_id: AccountId) -> Result<(), RpcError>;
async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, RpcError>;
// ChainMmrInfo gained two public fields (no constructor, struct literal only)
ChainMmrInfo { block_from, block_to, mmr_delta, block_header, protocol_config, block_signatures }
Migration Steps
- Add
register_accountandis_account_allowed. A wrapper can delegate, asVerifyingRpcClientdoes. - Change the two submit methods to take references; clone inside if you need ownership.
- Return
(SignedBlock, Option<ExecutionProof>)fromget_block_by_number. The proof isNonewhen not requested, and also when the node has not proven the block yet. - Fill
ChainMmrInfo::block_signatures(the validator signatures overblock_header) andChainMmrInfo::protocol_config(Somewhen the node sent one) insync_chain_mmr. Sync rejects a tip whose signatures do not verify. - Handle the new
RpcEndpoint::RegisterAccount,RpcEndpoint::IsAccountAllowedandEndpointError::RegisterAccountvariants in exhaustive matches. - Still wrap a custom client in
VerifyingRpcClient::new(..)if you pass it toClientBuilder::rpc, as in 0.16.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error[E0046]: not all trait items implemented, missing: `register_account`, `is_account_allowed` | New required methods | Implement or delegate them. |
error[E0053]: method `submit_proven_transaction` has an incompatible type for trait | Payload now by reference | Take &ProvenTransaction (and &ProvenBatch, &ProposedBatch). |
error[E0063]: missing fields `block_signatures` and `protocol_config` in initializer of `ChainMmrInfo` | New public fields | Fill them from the node response. |
error[E0004]: non-exhaustive patterns on RpcEndpoint or EndpointError | New variants | Add arms. |
(Rust) State sync authenticates the chain tip with ValidatorConfig
Summary
Every sync_chain_mmr response is checked against the validator set committed by the client's latest stored header. StateSync::new takes that ValidatorConfig, and the miden_client::block::ValidatorKeys re-export became ValidatorConfig. Client::sync_state does this for you; only callers of StateSync::new change.
Affected Code
// Before (0.16)
let sync = StateSync::new(rpc, note_screener, tx_discard_delta);
// After (0.17)
let validator_config = client.get_validator_config().await?;
let sync = StateSync::new(rpc, note_screener, tx_discard_delta, validator_config);
Migration Steps
- Pass
client.get_validator_config().await?as the fourthStateSync::newargument. - Rename
ValidatorKeystoValidatorConfig. Its constructor also takes aquorum, which must equal the number of keys; see Transaction Changes.AttestedTransactionEncryptionKey::verifynow takes&ValidatorConfig.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
chain validation error: chain tip block header <N> does not carry valid validator signatures: <err> | The node served a tip not signed by the stored validator set | Check the endpoint points at the chain the store was synced on. |
error[E0061]: this function takes 4 arguments but 3 arguments were supplied | StateSync::new gained validator_config | Pass get_validator_config(). |
(Rust) Error enums: unused variants removed, new variants added
Summary
The client removed error variants it no longer constructs, including the TransactionScriptError wrappers, and added variants for new failures. None of these enums is #[non_exhaustive], so both a match on a removed variant and an exhaustive match without a wildcard stop compiling. Several removed variants were #[from] conversions, so a ? that relied on them no longer compiles either; custom Store code is the most likely to hit this.
Affected Code
Removed:
ClientError: AccountPatchError, HexParseError, MerkleError, MissingTransactionEncryptionKey,
SendNotesTransactionScriptError, TransactionInputError, TransactionScriptError
StoreError: AccountCodeDataNotFound, AccountDeltaError, AccountStorageIndexNotFound, AddressError,
HexParseError, InvalidInt, NoteRecordError, NoteTagAlreadyTracked, SmtProofError,
TransactionScriptError, VaultDataNotFound
TransactionRequestError: AssetVaultError, InvalidTransactionScript, NoteNotFound, UnsupportedAuthSchemeId
RpcError: AccountUpdateForPrivateAccountReceived
GetNotesByIdError: NoteNotFound, NoteNotPublic
GetNoteScriptByRootError: ScriptNotFound
GetBlockByNumberError: DeserializationFailed
SyncAccountStorageMapsError: AccountNotFound
SyncTransactionsError: AccountNotFound, WitnessError
RemoteProverClientError: InvalidEndpoint
Added:
ClientError: AccountNotAllowlisted, AccountAlreadyAllowed, AccountIsNetworkAccount, AccountIsNotNew,
UnscreenedNoteBlocks, AccountTagLimitExceeded
StoreError: DatabaseTransientError, DatabasePermanentError, ProtocolConfigNotFound,
ProtocolConfigCommitmentMismatch
TransactionRequestError: SwapNoteWithZeroAsset, InputNoteBeingProcessed
BatchBuilderError: BatchSubmissionOutcomeUnknown
NoteTransportError: NoteDetailsMismatch, InvalidFetchedNote, UnrequestedTag
AddTransactionError: MissingFee, ConsumesInflightFeeNotes, InvalidFeeAsset, BatchIdMismatch, AuthenticationFailed
NoteSyncError, SyncNullifiersError, SyncAccountVaultError, SyncAccountStorageMapsError,
SyncTransactionsError: FutureBlock
SyncChainMmrError: new enum (Internal, FutureBlock, Unknown), reached through EndpointError::SyncChainMmr
EndpointError: SyncChainMmr, RegisterAccount
FutureBlock (requested block is ahead of the node's chain tip) means the request named a block the node has not reached yet, typically a follower node behind the chain tip. In 0.16 the same node code on note sync decoded as NoteSyncError::DeserializationFailed.
Migration Steps
- Delete arms for the removed variants. Where
?relied on a removed#[from](for exampleHexParseError,MerkleErrororAccountPatchErrorintoClientError, orTryFromIntError,NoteRecordErrororSmtProofErrorintoStoreError), map the error explicitly. - Add arms, or a wildcard, for the new variants. Treat
FutureBlockas retryable: sync again after a delay. - Code that retried on
StoreError::DatabaseErrorfor a busy or locked SQLite database must now matchStoreError::DatabaseTransientError. Constraint violations and corrupt files areDatabasePermanentError.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error[E0599]: no variant, associated function, or constant named `TransactionScriptError` found for enum `ClientError` | Variant removed | Delete the arm. |
error[E0277]: `?` couldn't convert the error to `ClientError` (or StoreError) | A removed #[from] variant | Map the error explicitly. |
error[E0004]: non-exhaustive patterns | New variants | Add arms. |
requested block is ahead of the node's chain tip | The node is behind the requested block | Retry after a delay. |
(Rust) SyncedNote is flat and NoteObserver::observe takes it
Summary
SyncedNote carries note_id, metadata and inclusion_proof directly instead of a nested committed: CommittedNote, and NoteObserver::observe receives the whole SyncedNote.
Affected Code
// Before (0.16)
async fn observe(&self, committed_note: &CommittedNote, attachments: &NoteAttachments)
-> Result<bool, ClientError>;
let id = synced.committed.note_id();
// After (0.17)
async fn observe(&self, note: &SyncedNote) -> Result<bool, ClientError>;
let id = synced.note_id;
let attachments = ¬e.attachments;
let committed: CommittedNote = synced.into_committed_note(); // when you need the old record
Migration Steps
- Change
NoteObserver::observeto take&SyncedNote, and read attachments fromnote.attachments. - Replace
synced.committed.<x>()with the flat fields, or callinto_committed_note().
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error[E0050]: method `observe` has 3 parameters but the declaration in trait `NoteObserver::observe` has 2 | Signature changed | Take &SyncedNote. |
error[E0609]: no field `committed` on type `SyncedNote` | Flattened | Use note_id, metadata, inclusion_proof. |
(Rust) Protobuf conversions moved to miden-objects
Summary
Protocol object messages now come from the canonical miden-objects schemas. The client's own From / TryFrom conversions (modules rpc::domain::{block, digest, merkle, smt}) are gone, RpcConversionError lost five variants and gained CanonicalConversion, and the remote prover's conversion is now TryFrom<proto::ProveResponse> for ProvenTransaction (was TryFrom<proto::Proof>) and fails with TransactionProverError instead of DeserializationError. The generated code follows the v1 gRPC packages (miden.node.v1, miden.remote_prover.v1, miden.note_transport.v1). This affects code using miden_client::rpc::generated (public only with the testing feature) or matching RpcConversionError.
Affected Code
// Before (0.16)
match err {
RpcConversionError::DeserializationError(e) => ..,
RpcConversionError::NotAValidFelt | RpcConversionError::NoteTypeError(_)
| RpcConversionError::MerkleError(_) | RpcConversionError::InvalidInt(_) => ..,
RpcConversionError::InvalidField(_) | RpcConversionError::MissingFieldInProtobufRepresentation { .. } => ..,
}
// After (0.17)
match err {
RpcConversionError::CanonicalConversion(e /* miden_client::rpc::ConversionError */) => ..,
RpcConversionError::InvalidField(_) | RpcConversionError::MissingFieldInProtobufRepresentation { .. } => ..,
}
Migration Steps
- Replace matches on the removed variants with
RpcConversionError::CanonicalConversion.ConversionErroris re-exported frommiden_client::rpc. - Drop imports of
rpc::domain::block,digest,merkleandsmt. If you handle raw protobuf messages, convert throughmiden_objects(DecodeMessageExt::decode_and_verify/decode_and_build_unchecked).primitives.Digestis nowprimitives.Word. - Convert prover responses with
ProvenTransaction::try_from(proto::ProveResponse)and expectTransactionProverError; requests areproto::ProveRequest(wasProofRequest).
(Rust) BatchBuilder::submit reports an unknown outcome as its own error
Summary
When a batch submission fails without a definite answer from the node (any gRPC failure other than a deliberate rejection such as InvalidArgument or FailedPrecondition), submit returns ClientError::BatchBuilder(BatchBuilderError::BatchSubmissionOutcomeUnknown { submission, source }) instead of ClientError::RpcError. Resend the carried submission with the new Client::retry_proven_batch, which re-seals the inputs and does not execute or prove again. Single-transaction submission already behaved this way in 0.16 (ClientError::SubmissionOutcomeUnknown).
Affected Code
let mut batch = client.new_transaction_batch();
batch.push(account_id, request).await?;
let outcome = batch.submit().await;
match outcome {
Ok(block_num) => { /* accepted at block_num */ },
Err(ClientError::BatchBuilder(BatchBuilderError::BatchSubmissionOutcomeUnknown { submission, .. })) => {
client.retry_proven_batch(&submission).await?;
},
Err(err) => return Err(err.into()),
}
Migration Steps
- Code matching
ClientError::RpcErroraround batch submission still compiles but no longer sees transport-level failures. MatchBatchSubmissionOutcomeUnknownand retry withretry_proven_batch. - A retry of a batch that did land is rejected by the node as a conflict. The batch ID is fixed, so nothing is duplicated.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
submission of a batch of <N> transactions came back without a definite outcome, so the node may or may not have accepted it; nothing was recorded locally | Indeterminate submission | retry_proven_batch(&submission) |
(Rust) Keystore::get_account_key_commitments returns an empty set for an unknown account
Summary
FilesystemKeyStore used to fail with KeyStoreError::StorageError("account not found <id>") for an account without keys. It now returns Ok with an empty set, because an account may sign with keys held elsewhere (for example an external ECDSA key). get_keys_for_account returns an empty vector. Code that treated the error as "no keys" still compiles and silently stops detecting it.
Affected Code
// Before (0.16): an error meant "no keys for this account"
if keystore.get_account_key_commitments(&account_id).await.is_err() { /* unknown */ }
// After (0.17)
if keystore.get_account_key_commitments(&account_id).await?.is_empty() { /* no local keys */ }
Migration Steps
- Check for an empty set instead of an error.
- Custom
Keystoreimplementations should return an empty set, not an error, for an account they hold no key for.
(Rust) Other library changes
- Zero-amount swaps are rejected when the request is built.
TransactionRequestBuilder::build_swapandbuild_pswap_createreturnTransactionRequestError::SwapNoteWithZeroAsset("offered" | "requested")for a zero-amount fungible asset on either side. In 0.16 they produced a note that paid or received nothing. The CLIswapandpswapcommands are affected the same way. TransactionRequest::incoming_assetsreturns(BTreeMap<AccountId, u64>, Vec<Asset>)instead ofVec<NonFungibleAsset>for the second element, becauseAssetis now a struct. Filter withasset.is_non_fungible().NoteExecutionHint(re-exported asmiden_client::note::NoteExecutionHint) gainedUnknown(Felt),into_parts()returnsOption<(u8, u32)>, and theu64conversions becameFeltconversions. See Note Changes.- Custom
Storeimplementations must persist the protocol configuration a sync delivers:StateSyncUpdate::from_partsgained a trailingprotocol_config: Option<ProtocolConfig>argument andinto_partsa sixth element. Store it in client settings undermiden_client::protocol_config::protocol_config_setting_key(config.to_commitment()); execution and screening read it from there.TransactionFilter::to_querywas removed (the SQL moved into the SQLite store), and the stored note-transport cursor setting changed format (no longer eight big-endian bytes). TheStoretrait also gains five required methods with no default body, for account witness caching:track_account_witness,untrack_account_witness,tracked_account_witnesses,get_account_witnessandupdate_account_witness(error[E0046]: not all trait items implemented, missing: `track_account_witness`, ...).apply_state_syncmust write each witness inupdate.account_updates().account_witnesses()for the accounts registered withtrack_account_witness, as the SQLite store does. miden-client-sqlite-store:column_value_as_u64,u64_to_valueand theSqliteStore::{apply_transaction, apply_transaction_batch, prune_account_history, prune_irrelevant_blocks, upsert_foreign_account_code}associated functions are no longer public. This is not in the changelog.- Testing helpers (
testingfeature): the loose functions inmiden_client::testing::common(execute_tx_and_sync,wait_for_tx,mint_note, ...) are now methods onTestClient,insert_new_walletand its_with_seed/_unfundedvariants are replaced byTestClient::insert_wallet(account_type), andTestClient::keystore()exposes the keystore.ClientConfig::into_client/into_unsynced_client(inmiden-client-integration-tests) returnTestClientinstead of(TestClient, FilesystemKeyStore). DapProgramExecutor(dapfeature): itsProgramExecutor::newnow returnsResult<Self, AdviceError>. This is not in the changelog.AccountReader::get_balancenow errors, instead of returningAssetAmount::ZERO, when the stored value under the fungible asset ID cannot be decoded as a fungible asset. In practice only a corrupt store hits this.- Account tag limit.
Client::add_account(for a native account) andClient::add_addressfail withClientError::AccountTagLimitExceeded(client already tracks maximum number of account tags possible: 128) once the client tracksClient::MAX_ACCOUNT_TAGS(128) distinct account tags, the most the note transport accepts in one request. Watched accounts are exempt. The CLI'snew-wallet,new-accountandimporthit it at the 129th distinct tag. - Account code upgrades.
TransactionRequestBuilder::account_code_upgrade(code)gives a transaction the new code for a script that upgrades the executing account,build_account_code_upgrade(code)builds a request that upgrades an account withUpgradeManagerandAuthority::AuthControlled, andTransactionRequest::account_code_upgrade()reads it back. For a network account, build anUpgradeNoteand add it withown_output_notes(see Note Changes and Account Changes). The new field is serialized last, soTransactionRequestbytes from 0.16 or from 0.17.0-rc.4 or earlier fail to deserialize. - Removed unused items:
ClientBuilder::tx_graceful_blocks(deprecated; usetx_discard_delta),NoteTransportUpdate, theAccountProofsalias,AcceptHeaderContext::unknown(), and the CLI'sCliError::{Internal, AccountId, MissingFlag, InvalidAccount}. block_numbersadds blocks the transaction authenticates beyond its reference block. Multisig uses it for the bound block; a MASM script that reads an older block withtx::get_block_commitmentalso needs that block in the transaction inputs (see MASM Changes).
New in 0.17, no migration needed: the miden-client-proto crate (Protobuf encoding of client types, as the SQLite store uses), Client::track_account_witness / untrack_account_witness / tracked_account_witnesses, Client::MAX_ACCOUNT_TAGS, miden_client::rng::{draw_felt, draw_word}, Endpoint::mainnet(), ClientBuilder::for_mainnet(), MAINNET_PROVER_ENDPOINT, NOTE_TRANSPORT_MAINNET_ENDPOINT, Client::get_validator_config, Client::get_protocol_config, Client::register_account, Client::is_account_allowed, Client::retry_proven_batch, TransactionRequestBuilder::block_numbers, SyncedNote::into_committed_note, Client::fetch_chain_updates / apply_chain_updates with ChainSyncData, the keystore helpers FilesystemKeyStore::{store_key, list_keys, associate_key, disassociate_key, account_ids_for_key} with StoredKeyInfo, and pricing re-exports (NetworkNotePricer, NotePricingError, NoteCheckerError, transaction::{TransactionFee, TransactionFeeError}, note::{NoteCost, NoteConsumptionCost}) that remove the need for a direct miden-tx dependency to price note consumption.
(Web) Bump every @miden-sdk/* package together
Summary
All 20 published @miden-sdk/* packages share one version, and every first-party peer range moved to ^0.17.0 (react, para, para-react, turnkey, turnkey-react, wallet-adapter-base, wallet-adapter-miden, wallet-adapter-react, telemetry-otel, telemetry-sentry). A 0.16 peer range does not accept 0.17, so bump them together.
Affected Code
- "@miden-sdk/miden-sdk": "0.16.3",
- "@miden-sdk/react": "0.16.3"
+ "@miden-sdk/miden-sdk": "0.17.0",
+ "@miden-sdk/react": "0.17.0"
Migration Steps
- Before the bump reaches users, back up the default browser keystore's secret keys on 0.16.3: the first 0.17 open deletes them with the rest of the IndexedDB store. See (Store) Every 0.16 SQLite store must be recreated.
- Install
0.17.0(npm install <package>@0.17.0). Do the same for every other@miden-sdk/*package you depend on (vite plugin, wallet adapters, Para, Turnkey, telemetry). - The 0.16 guide pinned
miden-sdk 0.16.1withreact 0.16.0; since 0.16.2 the two ship in lockstep at the same version. - Node.js: the Node entry still resolves its native binary through the
optionalDependencies@miden-sdk/node-darwin-arm64,node-darwin-x64andnode-linux-x64-gnu, pinned to the exact same version. Do not install them directly. - Rust crates published from the Web SDK repository (
miden-client-web,miden-idxdb-store,js-export-macro,miden-mobile-prover) are at0.17.0.miden-client-webrequiresmiden-client0.17.0andmiden-protocol0.17.0;miden-idxdb-storerequiresmiden-client0.17.0and, new in 0.17,miden-client-proto0.17.0;miden-mobile-proverrequiresmiden-client0.17.0.
(Web) BlockHeader.feeFaucetId() removed: ask the client
Summary
The fee asset moved from the block header into the protocol configuration. The client receives that configuration from the node when it syncs, so await client.feeFaucetId() replaces header.feeFaucetId(). A bare RpcClient can no longer discover the fee faucet.
Affected Code
// Before (0.16)
const header = await new RpcClient(Endpoint.testnet()).getBlockHeaderByNumber(undefined);
const feeFaucet = header.feeFaucetId();
// After (0.17)
const client = await MidenClient.create({ rpcUrl });
await client.sync();
const feeFaucet = await client.feeFaucetId(); // AccountId
// Optional: seed what feeFaucetId() reports before the first sync (bech32 or hex)
const client = await MidenClient.create({ rpcUrl, feeFaucetId: "0x..." });
// React: the same option on MidenProvider
<MidenProvider config={{ rpcUrl, feeFaucetId: "0x..." }}>{children}</MidenProvider>
Migration Steps
- Replace
header.feeFaucetId()withawait client.feeFaucetId(). The low-levelWasmWebClient/WebClienthave the same method. - Sync before reading it. After the first sync it reports the faucet named by the protocol configuration the block at the sync height commits to. Before it, it falls back to
ClientOptions.feeFaucetId(or the mock chain's), and rejects when neither is set. - Code that read the faucet from a bare
RpcClientmust now use a synced client, or configure the faucet per network. BlockHeader.verificationBaseFee()is unchanged.
ClientOptions.feeFaucetId and MidenConfig.feeFaucetId are optional. You do not need them to execute or screen notes, and a wrong value is corrected by the next sync rather than executed under. Some shipped 0.17.0 text still says it is required (the JSDoc on MidenClient.create, createTestnet and createDevnet, and some doc samples); the code makes it optional.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
Property 'feeFaucetId' does not exist on type 'BlockHeader'. (TS2339) | Accessor removed | await client.feeFaucetId() |
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 | feeFaucetId() before the first sync, with no option set | Sync first, or pass the option. |
`<value>` is not a fee faucet: expected a bech32 address or a hex account ID | Malformed feeFaucetId option | Pass a bech32 address or a hex account ID. |
(Web) AccountType is the visibility enum; faucets use FaucetType
Summary
The JS object that shadowed the native AccountType ({FungibleFaucet, NonFungibleFaucet}) is gone. AccountType is now AccountType.Private / AccountType.Public on both the browser and Node entries, and faucets are selected with the new FaucetType.FungibleFaucet (the string "FungibleFaucet"). accounts.create validates its selectors and throws instead of silently creating a wallet.
Affected Code
// Before (0.16): worked at runtime in JS; TypeScript already rejected it (TS2339)
const faucet = await client.accounts.create({
type: AccountType.FungibleFaucet, symbol: "TOK", decimals: 8, maxSupply: 10_000_000n,
});
// After (0.17)
import { FaucetType, AccountType, AccountBuilder } from "@miden-sdk/miden-sdk";
const faucet = await client.accounts.create({
type: FaucetType.FungibleFaucet, symbol: "TOK", decimals: 8, maxSupply: 10_000_000n,
});
new AccountBuilder(seed).accountType(AccountType.Public); // works now (was undefined in 0.16)
Migration Steps
- Replace
AccountType.FungibleFaucetwithFaucetType.FungibleFaucet. On Node the 0.16 value was already the string"FungibleFaucet", so this is a pure rename there; in the browser the 0.16 value was the number0. - Drop
AccountType.NonFungibleFaucet.FaucetTypehas no non-fungible member; non-fungible faucets are rejected. - Remove imports of the
AccountTypeValuetype, and useFaucetTypeforFaucetCreateOptions.type. - Never pass a visibility value as
type:0/1(=AccountType.Private/Public) are still read as legacy faucet selectors. Choose visibility withstorage. - Code that passed faucet fields without a faucet type, or an unknown
type, used to get a wallet silently (a contract, for an unknowntypewithcomponents), andcomponentson a faucet were silently ignored. Each now throws aTypeErrorbefore creating anything.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
Property 'FungibleFaucet' does not exist on type 'typeof AccountType'. (TS2339) | Stale selector | FaucetType.FungibleFaucet |
TypeError: accounts.create(): symbol, decimals, maxSupply only apply to faucets, and no faucet type was given. Pass type: FaucetType.FungibleFaucet for a faucet, omit type for a wallet, or pass components for a contract. | JS passed AccountType.FungibleFaucet, now undefined (the field list varies with the fields passed) | FaucetType.FungibleFaucet |
accounts.create(): a faucet request needs symbol, decimals, maxSupply. ... | type: AccountType.Private / Public, or a legacy 0 / 1, without faucet fields | Use storage for visibility. |
(Web) Block-locked notes are no longer "available"
Summary
notes.listAvailable({ account }) and transactions.consumeAll({ account }) keep only notes the note screener reports as consumable now, at the last synced block. consumeAll's consumed / remaining count only those, so remaining === 0 no longer means "no unconsumed notes". The React hooks apply the same rule (see (React) Hook behaviour changes). Nothing fails to compile.
Affected Code
// 0.17: to also see notes that unlock later
// Read the hex first: in the browser build, listConsumable consumes an AccountId handle passed to it
const accountIdHex = accountId.toString();
const records = await client.notes.listConsumable({ account: accountIdHex }); // ConsumableNoteRecord[]
const nowOnly = records.filter((r) => isConsumableNow(r, accountIdHex));
// low-level: NoteConsumptionStatus.isConsumableNow()
Migration Steps
- If you showed pending or time-locked notes from
listAvailable, switch to the newnotes.listConsumable()and read each record'snoteConsumability()[i].consumptionStatus().consumableAfterBlock(), or filter with the exportedisConsumableNow(). - Do not treat
consumeAll(...).remaining === 0as "inbox empty".
(Web) Private note relay takes an inclusion proof
Summary
notes.sendPrivate takes the note's inclusionProof instead of scanAfterBlockNum, and the transport verifies the proof before it stores the note. A proof exists only once the transaction that created the note is committed and the client has synced past that block. notes.sendPrivateOutput({ noteId, to }) keeps its signature but now reads the proof sync stored on the output note: in 0.16.3 it worked right after submit, in 0.17 it throws until the commit is synced. React's useTransaction({ privateNoteTarget }) already waits for the commit, so React apps change nothing.
Affected Code
// Before (0.16.3)
await client.notes.sendPrivate({ note: noteId, to, scanAfterBlockNum: submitHeight });
// After (0.17): commit and sync first
const r = await client.transactions.send({
account, to, token, amount, type: "private", returnNote: true, waitForConfirmation: true,
});
await client.notes.sendPrivateOutput({ noteId: r.note.id(), to });
// Any other note: pass the proof from its record
const record = await client.notes.get(noteId);
await client.notes.sendPrivate({ note: noteId, to, inclusionProof: record!.inclusionProof()! });
Migration Steps
- Replace
scanAfterBlockNumwithinclusionProof. Proofs come fromInputNoteRecord.inclusionProof(),OutputNoteRecord.inclusionProof()andNoteFile.inclusionProof(). - Call
sendPrivateOutputonly after the creating transaction is committed and synced, for example withwaitForConfirmation: trueortransactions.waitFor(txId). - Tests against a mock note transport can build a proof with the test-only
NoteInclusionProof.mockAtBlock(blockNum); a real transport rejects it.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
sendPrivate requires inclusionProof: a NoteInclusionProof the transport verifies. ... | JavaScript still passing scanAfterBlockNum (TypeScript rejects it at compile time) | Pass inclusionProof. |
output note has no inclusion proof; sync past the block that committed it | sendPrivateOutput before the commit was synced | Wait for confirmation, then retry. |
(Web) Prefetched foreign accounts removed
Prefetched foreign accounts came with miden-client 0.16.1 and are exposed by Web SDK 0.16.1 through 0.16.3. Web SDK 0.17 removes them deliberately, as its changelog records, in favour of executing against a recent block or at the tip. The Rust client 0.17.0 keeps them (see (Rust) ForeignAccount::Prefetched), but Web SDK 0.17.0 does not expose them.
Summary
ForeignAccount.prefetched(), AccountInputs, AccountInputsArray, client.transactions.foreignAccountInputs() and WebClient.getForeignAccountInputs() are gone. 0.17 resolves a foreign account's vault and storage entries during execution against the reference block.
Affected Code
// Before (0.16)
const inputs = await client.transactions.foreignAccountInputs([ForeignAccount.public(id, reqs)], anchor.blockNum());
const fa = ForeignAccount.prefetched(AccountInputs.deserialize(bytes));
// After (0.17)
const targets = new ForeignAccountArray();
targets.push(ForeignAccount.public(id, new AccountStorageRequirements()));
const request = (await client.feeAwareTransactionRequestBuilder(account)).withForeignAccounts(targets).build();
withForeignAccounts takes a ForeignAccountArray, not a plain array; TypeScript rejects a plain array at both 0.16 and 0.17.
Migration Steps
- Declare foreign accounts with
ForeignAccount.public(id, requirements)orForeignAccount.private(account). - Execute against a recent reference block, within the node's 50-block account-history window: capture an anchor close to execution, and re-capture an aged one. For multisig, execute at the tip (see (Web) Multisig).
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
Module '"@miden-sdk/miden-sdk"' has no exported member 'AccountInputs'. (TS2305) | Type removed | Declare the account instead. |
Property 'prefetched' does not exist on type 'typeof ForeignAccount'. (TS2339) | Factory removed | ForeignAccount.public / private |
Property 'foreignAccountInputs' does not exist on type 'TransactionsResource'. (TS2339) | Method removed | Remove the prefetch step. |
(Web) Multisig requests come from feeAwareTransactionRequestBuilder and execute at the tip
0.17's multisig auth procedure reads three words of auth args (bound block and approval expiration, salt, conversion info) on every chain, fee-free included, and binds the summary to that bound block. A multisig request built with withFeeConversionSalt, or from a bare builder on a fee-free chain, now aborts at execution; on a fee-charging chain a bare builder still fails before execution with FeeConversionInfoRequired, as in 0.16. The full flow is in Transaction Changes.
- Build every request a multisig (including a guarded multisig) executes from
await client.feeAwareTransactionRequestBuilder(account, options), on fee-free chains too. It builds the auth args and declares the bound block. The new options arefeeConversionSalt,boundBlockNumandapprovalExpirationDelta. For any other account it still returns an untouched builder. - Do not call
withFeeConversionSaltorwithAuthArgon the builder it returns for a multisig: each clears the other and discards the auth args. - The
feeConversionSaltoption consumes theWord(it moves across the WASM boundary). Build a freshWordper call: a spent handle is not rejected, it arrives as "no salt" and a random salt is drawn. - Stop passing
anchorfor multisig requests (preview,executeRequest,submit). Sync each party to at least the bound block (Math.max(...request.blockNumbers())) and execute at the tip. TransactionRequest.serialize()now always carries block numbers and a trailing account-code-upgrade field, and embedded scripts are hashless, so bytes from 0.16 or from 0.17.0-rc.4 or earlier fail inTransactionRequest.deserialize(failed to deserialize miden_client::transaction::request::TransactionRequest: ...). A dApp, its wallet and every co-signer exchanging request bytes (for example aCustomTransactionthrough the wallet adapter) must all be on 0.17.0.- New:
TransactionRequestBuilder.withBlockNumbers()andTransactionRequest.blockNumbers().
(Web) Rebuild a bundled native mobile prover
miden-mobile-prover links miden-client, now 0.17.0 on protocol 0.17.0. A prover binary built for 0.16 proves against the 0.16 kernel and fails on 0.17 transactions. Capacitor, iOS and Android apps that embed it must rebuild the library from miden-mobile-prover 0.17.0 (or from the Web SDK release tag) and ship it with the SDK bump. This is not in the changelog.
(Web) Other API changes
| Change | Migration |
|---|---|
BlockHeader.txKernelCommitment() → protocolConfigCommitment() | Rename. TypeScript suggests txCommitment(); that is a different value. |
ForeignAccount.account_id() / storage_slot_requirements() → accountId() / storageSlotRequirements() on the browser build | Rename. The Node build was already camelCase. |
Raw WebClient.createClientWithExternalKeystore inserts feeFaucetId before the three callbacks | Insert the argument (see below), or use MidenClient.create({ keystore: { getKey, insertKey, sign } }). Not in the changelog. |
Wrapper statics WasmWebClient.createClient / createClientWithExternalKeystore gained trailing observability (already accepted at runtime in 0.16.3, newly typed) and feeFaucetId parameters | No change needed. |
AccountVaultDelta.fungible(), FungibleAssetDelta and FungibleAssetDeltaItem removed | Use addedFungibleAssets() / removedFungibleAssets(). See Assets, Vault & Faucet. |
NoteFile.deserialize / AccountFile.deserialize reject bytes serialized by 0.16 | Re-export from a 0.17 client. |
| Network accounts cannot be deployed by an empty transaction, must use the chain's fee faucet, and always allowlist P2ID | See Account Changes. |
Emitting a note to a network account caps the transaction at 20 blocks; createNetworkNote declares the target as a foreign account | See Transaction Changes. |
| New account creation can be gated by an allowlist | See (Node) New accounts may need an invitation code. |
The same ClientOptions.seed draws a different random stream: the seed now feeds a StdRng directly instead of seeding a RandomCoin. Note serial numbers in mint, send and swap requests, the multisig auth salt and Falcon signature randomness differ from 0.16.3; keys from accounts.create do not | Regenerate fixtures and recovery tests that hard-code note IDs or serial numbers from a seeded client. |
Clients sharing one IndexedDB database (a second tab, a wallet and dApp pair, two clients on one storeName) now see each other's account writes. Applying a transaction first checks the stored starting state and rejects the whole apply if another client changed the account: transaction input account commitment does not match state for <id> (or ... persisted state for <id>) from submit, send or apply() | The network may already have the transaction: check its status before resubmitting. |
Node.js: account.storage() returns a StorageView, as the browser entry already did, instead of the raw AccountStorage. getItem(slot) returns a StorageResult, and on a map slot it returns the first entry's value, not the map root | Read a map root with getCommitment(slot) or .raw.getItem(slot). |
// Raw WASM instance method, before (0.16)
await raw.createClientWithExternalKeystore(nodeUrl, noteTransportUrl, seed, storeName, getKey, insertKey, sign);
// After (0.17): TypeScript flags the old call (a function is not assignable to `string`)
await raw.createClientWithExternalKeystore(nodeUrl, noteTransportUrl, seed, storeName, undefined /* feeFaucetId */, getKey, insertKey, sign);
New in 0.17, no migration needed: client.feeFaucetId(), ClientOptions.feeFaucetId, MidenConfig.feeFaucetId, the feeAwareTransactionRequestBuilder options, TransactionRequestBuilder.withBlockNumbers(), TransactionRequest.blockNumbers(), notes.listConsumable(), NoteConsumptionStatus.isConsumableNow(), the exported isConsumableNow(), compile.component({ libraries }), accounts.register() / accounts.isAllowed(), WebClient.registerAccount / isAccountAllowed, RpcClient.registerAccount / isAccountAllowed, AccountVaultDelta.numAssets(), FaucetType, MultisigAuthOptions, RegisterAccountOptions, the test-only NoteInclusionProof.mockAtBlock(blockNum) (accepted only by a mock note transport), and on the Node entry the exports StorageView, StorageResult, wordToBigInt and MidenArrays.
(React) Hook behaviour changes
No hook signature changed: the React typings add only MidenConfig.feeFaucetId?. Behaviour changed in these places:
| Hook | 0.17 behaviour |
|---|---|
MidenProvider | New optional config.feeFaucetId. It only seeds client.feeFaucetId() before the first sync. |
useNotes().consumableNotes / consumableNoteSummaries | Exclude block-locked notes. |
useWaitForNotes().waitForConsumableNotes | Waits only for notes consumable now. |
useSessionAccount funding step | Consumes only notes consumable now, and waits on locked ones. |
useCreateNetworkNote | Declares the target as a foreign account; its transaction must land within 20 blocks. |
usePreview / useTransaction / useChainAnchor | Multisig requests drop the anchor; see the next section. |
(React) Multisig co-signing drops the anchor
Do not capture an anchor with useChainAnchor, and do not pass anchor to usePreview or useTransaction, for a multisig request built by feeAwareTransactionRequestBuilder. usePreview does not sync, so call sync() and check client.getSyncHeight() against Math.max(...request.blockNumbers()) first. The full flow is in Transaction Changes.
// After (0.17): multisig co-signer
const { client, sync } = useMiden();
const { preview } = usePreview();
const verify = async (requestBytes: Uint8Array) => {
const request = TransactionRequest.deserialize(requestBytes);
await sync();
if (!client || (await client.getSyncHeight()) < Math.max(0, ...request.blockNumbers())) {
throw new Error("not synced yet");
}
return preview({ accountId, request }); // no anchor
};
(CLI) Re-create .miden/packages after upgrading
Summary
init writes the bundled component packages into package_directory once and never again (it refuses to run while a config exists). Those 0.16 files are package format 6.0.0; the 0.17 CLI reads only 7.0.0. new-wallet (which loads basic-wallet by name), new-account -p <bundled name> and account --inspect (which loads every .masp in the directory) all fail until the directory is refreshed. This is not in the changelog.
Affected Code
# After upgrading, with the .miden directory created by 0.16
miden-client new-wallet
# Error: account component error: failed to deserialize Package in .../.miden/packages/basic-wallet.masp
# invalid value: unsupported version. Got '[6, 0, 0]', but only '[7, 0, 0]' is supported
# Refresh only the packages of the ACTIVE config (./.miden, else $MIDEN_CLIENT_HOME, else ~/.miden),
# keeping the config and keystore (the store still has to go; see the Quick Fix)
# First run the TOML path-resolution block in Quick Fix and inspect its output.
# Stop if parsing fails; do not replace an empty value with the config directory.
set -eu
: "${pkgs:?Run and verify the Quick Fix path-resolution block first}"
echo "$pkgs" # check before copying
tmp=$(mktemp -d) && (cd "$tmp" && miden-client init --local >/dev/null) \
&& mkdir -p "$pkgs" && cp -R "$tmp/.miden/packages/." "$pkgs/" && rm -rf "$tmp"
Migration Steps
- The simplest path, since the store has to be recreated anyway: move the active
.midendirectory aside (see the Quick Fix for which one the CLI loads) and runmiden-client initagain, with--localif it was a./.midenin your project, with--networkpointing at a 0.17 node (--network devnetalso sets devnet's note transport; a custom URL sets none, so add--note-transport-endpoint). This writes fresh packages. The keystore lives in.miden/keystoreby default, so copy that directory into the new.midenif you want to keep your keys. - To keep the directory, replace only the packages: run
init --localin a temporary directory and copy its.miden/packagesinto the configuredpackage_directory, as above. The copy overwrites the nine bundled packages only; any other6.0.0.maspleft in that directory still breaksaccount --inspect, so rebuild or remove it. - Rebuild any custom
.maspcomponent packages you pass with-p,--extra-packagesor--packagewith a VM 0.35 toolchain; they fail with the sameunsupported versionerror. - The nine bundled package names are unchanged:
basic-wallet.masp,basic-fungible-faucet.masp,basic-non-fungible-faucet.masp,auth/basic-auth.masp,auth/ecdsa-auth.masp,auth/no-auth.masp,auth/multisig-auth.masp,auth/guarded-multisig-auth.masp,auth/network-account-auth.masp.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
account component error: failed to deserialize Package in <path> caused by invalid value: unsupported version. Got '[6, 0, 0]', but only '[7, 0, 0]' is supported | A package written by a 0.16 init, or built with a VM 0.29 toolchain | Refresh .miden/packages; rebuild custom packages. |
configuration file already exists: "miden-client.toml" already exists in the local .miden directory (...), configured with the network "..." | init refuses to overwrite, so it cannot refresh packages in place | Use a temporary directory and copy, or move the old .miden aside. |
(CLI) init still defaults to testnet
miden-client init with no --network still configures https://rpc.testnet.miden.io, and [note_transport] endpoint defaults to https://transport.miden.io. A 0.17.0 CLI needs a stable 0.17 node: a 0.16 node or a 0.17 release-candidate node rejects it at the accept header, so the first sync fails until the CLI is pointed at a stable 0.17 node. A note transport that serves only the 0.16 service does not fail sync at all. Point both at 0.17 services: pass --network <0.17 endpoint> with --note-transport-endpoint <URL>, or edit [rpc] endpoint and [note_transport] endpoint. See (Node) Client, node, remote prover and note transport must all be 0.17.
$ miden-client init --local && miden-client sync
Error: cli::client_error
× client error
├─▶ RPC error
├─▶ accept header validation failed
╰─▶ server rejected request - please check your version and network settings (client version: 0.17.0, genesis commitment: none)
help: The node rejected the request due to a version mismatch. Ensure your client version is compatible with the node version. See docs: https://docs.miden.xyz/builder/tools/clients/rust-client/cli/cli-troubleshooting
(CLI) exec runs compiled packages only; --script-path is removed
Summary
exec no longer compiles MASM. --package / -p is required and takes a compiled library package that exports exactly one @transaction_script procedure. --script-path / -s is gone and not aliased, and an executable (program) package is rejected too.
Affected Code
# Before (0.16)
miden-client exec -a <ACCOUNT_ID> --script-path script.masm --inputs-path inputs.toml
# After (0.17)
midenc script.masm --lib -o script.masp # or `miden build` for a Rust #[tx_script]
miden-client exec -a <ACCOUNT_ID> --package script.masp --inputs-path inputs.toml
miden-client exec -a <ACCOUNT_ID> -p script # bare name: resolved as <package_directory>/script.masp
The MASM itself keeps the 0.16 shape:
@transaction_script
pub proc main
push.1.2 add push.3 mul push.9 assert_eq
end
Migration Steps
- Compile each script to a
.masplibrary package with themidenc0.11 line, which writes package format7.0.0. Amidenc0.10 writes package format6.0.0, which the 0.17 CLI rejects. See Rust Contract SDK & Compiler. - Replace
--script-path <file>.masm/-s <file>.masmwith--package <file>.masp/-p <file>.masp. A path without an extension is looked up in the configuredpackage_directory. --inputs-path,--hex-wordsand-a/--accountare unchanged.- DAP builds (
--features dap):--start-debug-adapternow loads the package's debug info, and a DAP "restart" reloads the.maspfile from disk instead of recompiling source. Rebuild the package before restarting to pick up source edits, and compile with debug info (--debug full) for source stepping.
The changelog says to compile scripts with miden build first. That is the path for a Rust #[tx_script] project; for a MASM script use midenc <file>.masm --lib -o <file>.masp.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error: unexpected argument '--script-path' found (followed by tip: a similar argument exists: '--inputs-path') | Flag removed | Compile the script and pass --package. |
error: unexpected argument '-s' found | Short flag removed | Use -p <PACKAGE>. |
error: the following required arguments were not provided: / --package <PACKAGE> | --package is mandatory | Pass a compiled .masp. |
execute program error: the package at <path> is not a transaction script | The package has no single @transaction_script export, or is an executable package | Build a library package whose one transaction script is marked @transaction_script. |
(CLI) 0.16 .mac and .mno files cannot be imported
Summary
miden-client import of a file written by export in 0.16 fails for both the note and the account decoding attempt. There is no conversion path. The changelog records the Rust side only.
Affected Code
miden-client import account-from-016.mac
# import error: failed to read `account-from-016.mac`
# as a note: failed to decode the note file: failed to decode Protobuf message: invalid tag value: 0: ...
# as an account: failed to decode the account file: failed to decode Protobuf message: invalid tag value: 0: ...
This was verified with a .mac file; a .mno file goes through the same decoding path.
Migration Steps
- Do not rely on 0.16 exports to carry accounts or notes into 0.17.
- Private notes: consume them on 0.16 before upgrading, or have the sender re-send them after both sides run 0.17.
- Accounts: 0.16 secret keys survive (the keystore directory is readable by 0.17), but the account state must come from the chain or be recreated; see (Store) Every 0.16 SQLite store must be recreated.
- Files exported by 0.17 (
export --account,export --note) are the new format and import normally.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
import error: failed to read `<file>` followed by as a note: failed to decode the note file: ... and as an account: failed to decode the account file: ... | A file written by a 0.16 client | Re-export from a 0.17 client, or recreate the account or note. The Protobuf detail after the colon depends on the file bytes. |
(CLI) Bech32 addresses from another network are rejected
Summary
Every account argument that accepts a bech32 address now checks the address's network prefix against the configured network: transfer, mint, consume-notes, call, export, account, the recipient of notes --send, the faucet in <AMOUNT>::<FAUCET_ADDRESS>, and every address in token_symbol_map.toml. In 0.16 the prefix was ignored and the embedded account ID was used. A token symbol map containing one foreign-network address now fails to load, breaking every command that resolves assets. (The changelog lists send; the check applies to transfer, its name since 0.16.)
Affected Code
# CLI configured for testnet (prefix mtst), address encoded for devnet (mdev)
miden-client account -s mdev1ar5czf6r2fl5lsf34ylexzpxzs90mwwf
# 0.16: used the account ID inside the address
# 0.17: input error: Address network `mdev` does not match configured network `mtst`
# token_symbol_map.toml: every entry must use the configured network's prefix
BTC = { address = "mtst1...", decimals = 8 } # on testnet
Migration Steps
- Use addresses with the prefix of the network the CLI is configured for (
mmmainnet,mtsttestnet,mdevdevnet,mlcllocalhost,mcstany other endpoint), or pass the hex account ID, which carries no network. - Re-encode
token_symbol_map.tomlentries if you share one file across networks, or keep one map per.midendirectory. - For your own node of a known network, set
init --network-id <HRP>/[rpc] network_idso the expected prefix is notmcst(next section).
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
input error: Address network `<hrp>` does not match configured network `<hrp>` | Address from another network | Use the configured network's address, or the hex ID. |
config error: Failed to parse `address` for token symbol <SYMBOL> caused by input error: Address network ... does not match configured network ... | Foreign-network entry in the token symbol map | Fix or remove the entry. |
(CLI) Mainnet support and an explicit network ID
Summary
init --network mainnet configures https://rpc.mainnet.miden.io with note transport https://transport.mainnet.miden.io. A new optional [rpc] network_id, set by init --network-id <HRP>, overrides the bech32 prefix derived from the endpoint, which is mcst for any unrecognised endpoint.
Affected Code
miden-client init --network mainnet
miden-client init --network https://my-node.example.com --network-id mm
[rpc]
endpoint = "https://my-node.example.com"
timeout_ms = 10000
network_id = "mm" # new, optional
Migration Steps
- Nothing is required: a 0.16 config without
network_idloads unchanged. - If a 0.16 config already pointed at
https://rpc.mainnet.miden.ioas a custom endpoint, the CLI now renders and expectsmmaddresses there instead ofmcst. - If you run your own node for a public network behind a custom URL, set
network_id. With the new cross-network check,mcstwould otherwise reject that network's real addresses.
(CLI) new-account / new-wallet validate component packages
Summary
Every package passed to new-account -p or new-wallet -e must now be built as an account component and carry account component metadata. In 0.16 a package without the metadata section was silently skipped. A composite storage slot can now be given as one slot-level value in the init-data TOML.
Affected Code
miden-client new-wallet -e ./my-lib.masp
# 0.16: package silently ignored if it had no component metadata
# 0.17: account error: failed to read account component metadata from package <name>
# or: invalid argument: package <name> was built as a `<kind>`, not as an account component
Migration Steps
- Build extra packages as account components (with component metadata), not plain libraries.
- Mark every procedure the account should expose with
@account_procedure, or@auth_scriptfor the auth procedure, and watch stderr forWarning: package <name> has no procedures marked with .... - The multiple-auth-component checks moved to the account builder, so the old CLI message (
Multiple auth components found in packages...) no longer appears; the builder reports the error instead.
The changelog says the commands now reject a package that exports procedures without an @account_procedure or @auth_script attribute. They print a warning on stderr only when no procedure is marked, and continue; unmarked exports alongside marked ones are silently not exposed. The kind and metadata checks are real rejections.
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
invalid argument: package <name> was built as a `<kind>`, not as an account component | A library or executable package passed as a component | Rebuild as an account component. |
account error: failed to read account component metadata from package <name> | The package lacks the account component metadata section | Rebuild with component metadata. |
(CLI) export --account succeeds without keys
Summary
Exporting an account whose keys the keystore does not hold (for example one using an external ECDSA key) now succeeds and writes a keyless .mac. In 0.16 it failed, with keystore error / storage error: account not found <id> for an account the keystore index does not know, or No keys found for account for one indexed without keys. The new --no-keys leaves keys out deliberately. The success line now reports the key count.
Affected Code
miden-client export <ID> --account --no-keys
# Successfully exported account <ID> without secret keys
miden-client export <ID> --account
# Successfully exported account <ID> with 1 secret key(s)
Migration Steps
- Scripts that matched
Successfully exported account <ID>as a whole line must accept the new suffix. - Do not treat a successful export as proof the file can sign; check for
without secret keys.
(CLI) Authentication scheme flags and the keys command
Summary
Additive, with one output change. new-wallet and new-account accept --ecdsa-k256-keccak [PUBLIC_KEY] (alias --ecdsa) or --falcon512-poseidon2 (alias --falcon). With a 0x-prefixed SEC1 public key (33-byte compressed or 65-byte uncompressed) the account commits to that external key and stores no secret. The default is unchanged: a generated Falcon512/Poseidon2 key, unless a package supplies an auth component. A scheme flag combined with a package auth component is an error. The new keys subcommand lists, generates, imports and associates keystore keys, and computes a public-key commitment.
Affected Code
miden-client new-wallet --ecdsa # generate and store an ECDSA key
miden-client new-wallet --ecdsa 0x02... # external key, nothing stored
miden-client keys --list
miden-client keys --commitment 0x04... # 65-byte uncompressed SEC1 accepted
Migration Steps
- Scripts matching
Generated and stored Falcon512 authentication key in keystore.must matchGenerated and stored falcon512-poseidon2 authentication key in keystore.(orecdsa-k256-keccak).
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
error: the argument '--ecdsa-k256-keccak [<PUBLIC_KEY>]' cannot be used with '--falcon512-poseidon2' | Both scheme flags | Pick one. |
invalid argument: the given packages contribute an auth component, which cannot be combined with --ecdsa-k256-keccak or --falcon512-poseidon2 | A scheme flag plus an auth package | Drop one. |
invalid argument: ECDSA public key must use a 0x-prefixed hexadecimal encoding | Missing 0x | Add the prefix. |
(CLI) Other changes
importrequires a path.miden-client importwith no file used to succeed silently; it is now a usage error (error: the following required arguments were not provided:/<FILENAMES>...).- The protocol configuration arrives through
sync. Nothing to configure: the CLI gets the chain's protocol configuration, including the fee asset, from the node onsync. Sync before running transactions on a fresh store. notes --list consumable --account-id <ID>now filters. In 0.16 the filter was ignored and consumable notes for every tracked account were listed.callaccepts bech32 addresses foraccount-idarguments and for the faucet half of anassetargument. A result that does not decode as the declared return type now printsThe result is not a valid value of the procedure's return type: ...plus the raw stack, instead of failing the command.- Bundled packages named by bare name are read with the trusted reader (resolved from
package_directory); a path ending in.maspis still validated. notes --showprefix errors are reported asinput error:instead ofimport error:. This is not in the changelog, and only matters to scripts matching the prefix.- DAP builds pin
miden-debug0.18.0 (0.10 in 0.16.1). Use a matchingmiden-debugto connect or replay. notes --sendneeds the note's inclusion proof. It looks the ID up among output notes first, then input notes, and fails withinput error: note <hex> has no inclusion proof yet; wait for its transaction to be committed, sync and retryuntil the transaction that created the note is committed and the CLI has synced past it. In 0.16 it read input notes only and sent without a proof when there was none. See (Rust) Note transport.- Zero-amount
swap/pswapare rejected when the request is built:swap note assets must be non-zero: a zero offered asset makes the exchange one-sided(orrequested).
Common Errors
| Error Message | Cause | Solution |
|---|---|---|
store is at schema version 2, which is newer than the highest version this client supports (1) (3 for 0.17.0-rc.5) | A SQLite store written by 0.16 or a 0.17 release candidate | Delete the store with its -wal and -shm files and re-sync. |
Failed to create client: storage error: failed to deserialize data from the store: ... (Web) | An IndexedDB store created by a 0.17 release candidate | Delete the database; re-import accounts from backed-up keys. |
server rejected request - please check your version and network settings (client version: ..., genesis commitment: ...) | Client and node differ in major.minor, or only one of them is a release candidate | Run a stable 0.17 node with a stable 0.17 client. |
Failed to ensure genesis in place: ... accept header validation failed (Web) | Same | Same. |
failed to decode the account file / failed to decode the note file | A .mac / .mno file or AccountFile / NoteFile bytes written by 0.16 | Re-export with 0.17, or recreate the account or note. |
invalid value: unsupported version. Got '[6, 0, 0]', but only '[7, 0, 0]' is supported | Stale .miden/packages or a package built with a VM 0.29 toolchain | Refresh .miden/packages; rebuild custom packages. |
protocol configuration <commitment> is not stored; sync the client to get it from the node | Execution or note screening before the first sync | Run sync_state; if it persists, recreate the store. |
account <ID> is not registered on the network allowlist | Creating an account the node's allowlist does not know | Register with an invitation code. |
advice stack read failed (inside the executor error chain) | fee_conversion_salt / withFeeConversionSalt on a multisig account | Build MultisigAuthArgs (Rust) or use feeAwareTransactionRequestBuilder (Web). |
the advice map holds no preimage for the multisig auth args | A multisig request without auth args | Same. |
failed to lookup value in Merkle store | Multisig bound block not added to the request | Add it with block_numbers / withBlockNumbers. |
note with details commitment <hex> is being consumed by pending transaction <id> | The note is held by a pending or discarded transaction | Wait for the pending transaction; if it was discarded, 0.17 has no supported way to release the note. |
Property 'feeFaucetId' does not exist on type 'BlockHeader'. (TS2339) | Accessor removed | await client.feeFaucetId() |
Property 'FungibleFaucet' does not exist on type 'typeof AccountType'. (TS2339) | Stale selector | FaucetType.FungibleFaucet |
error: unexpected argument '--script-path' found | exec takes packages only | Compile the script and pass --package. |
input error: Address network `<hrp>` does not match configured network `<hrp>` | Address from another network | Use the configured network's address, or the hex ID. |
error[E0432]: unresolved import `miden_client::block::ValidatorKeys` | Renamed | ValidatorConfig |
error[E0046]: not all trait items implemented, missing: `register_account`, `is_account_allowed` | New NodeRpcClient methods | Implement or delegate them. |
error[E0046]: not all trait items implemented, missing: `track_account_witness`, ... | New Store methods | Implement the five account-witness methods. |
input error: note <hex> has no inclusion proof yet; wait for its transaction to be committed, sync and retry | notes --send before the commit was synced | Wait for the commit, sync, retry. |
sendPrivate requires inclusionProof: ... / output note has no inclusion proof; sync past the block that committed it (Web) | Relaying a private note without a proof, or before the commit was synced | Pass inclusionProof; sync past the commit first. |
client already tracks maximum number of account tags possible: 128 | A 129th distinct account tag | Remove unused addresses (remove_address), or split accounts across client stores. |
transaction input account commitment does not match state for <id> (Web) | Another client sharing the IndexedDB database changed the account | Check the transaction's status before resubmitting. |