From 9e0c856eb16e6d3ff47922e3cf0045e1b9842c5f Mon Sep 17 00:00:00 2001 From: 0xrouss-miden <312482795+0xrouss-miden@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:14 +0200 Subject: [PATCH 1/6] docs(get-started): refresh Rust/TS guides for v0.16 Update the authored onboarding path, setup instructions, account and note flows, storage examples, first-contract walkthrough, FAQ, and landing-page guidance for the v0.16 APIs and toolchain. --- docs/builder/faq.md | 2 +- docs/builder/get-started/accounts.md | 81 ++---- docs/builder/get-started/index.md | 2 +- docs/builder/get-started/notes.md | 267 ++++++++---------- docs/builder/get-started/read-storage.md | 100 +++---- docs/builder/get-started/setup/cli-basics.md | 55 ++-- .../builder/get-started/setup/installation.md | 16 +- .../your-first-smart-contract/create.md | 25 +- .../your-first-smart-contract/deploy.md | 12 +- .../your-first-smart-contract/test.md | 32 ++- docs/builder/index.md | 2 +- docs/reference/index.md | 18 +- 12 files changed, 296 insertions(+), 316 deletions(-) diff --git a/docs/builder/faq.md b/docs/builder/faq.md index 0028e47c..8418b0fa 100644 --- a/docs/builder/faq.md +++ b/docs/builder/faq.md @@ -100,4 +100,4 @@ faster quote-and-solve flow for test USDC. Both integrations are testnet-only. ## What does the gas fee model of Miden look like? -Miden does not yet have a fully implemented fee model, work in progress. +Miden v0.16 includes a transaction fee mechanism in which the account's authentication procedure creates a public `TX_FEE` note on fee-charging networks. diff --git a/docs/builder/get-started/accounts.md b/docs/builder/get-started/accounts.md index c9dad704..bce7e4ac 100644 --- a/docs/builder/get-started/accounts.md +++ b/docs/builder/get-started/accounts.md @@ -16,7 +16,7 @@ Before diving into account creation, it's essential to understand what makes Mid - **Smart Contract Wallets**: Every account is a programmable smart contract that can hold assets and execute custom logic - **Modular Design**: Accounts are composed of reusable components (authentication, wallet functionality, etc.) -- **Privacy Levels**: Choose between public or private storage modes +- **Privacy Levels**: Choose whether the full account state is public or privately held Miden accounts differ from traditional blockchain addresses in fundamental ways. @@ -27,7 +27,7 @@ Miden accounts differ from traditional blockchain addresses in fundamental ways. - Each account has **storage slots** for custom data - Accounts are composed of **modular components** for different functionalities -**Storage Modes:** +**Account-state visibility:** - **Public**: All state visible onchain (transparent operations) - **Private**: Only commitments onchain, full state held privately @@ -89,17 +89,6 @@ pub enum AccountType { Wallet, faucet, and custom-contract roles come from the account's components and creation options, not from the account ID. -**Storage Modes:** - -```rust -pub enum StorageMode { - /// State stored onchain and publicly readable. - Public, - /// Only a commitment is onchain; full state is held privately by the owner. - Private, -} -``` - ## Set Up Development Environment @@ -134,7 +123,7 @@ If you already created `miden-app` during [installation](./setup/installation#ty ```bash title=">_ Terminal" npm create vite@latest miden-app -- --template vanilla-ts cd miden-app -npm install @miden-sdk/miden-sdk@^0.15.0 +npm install @miden-sdk/miden-sdk@^0.16.0 ``` For each code example, save the TypeScript snippet as `src/demo.ts` (overwriting the previous one as you progress): @@ -173,13 +162,13 @@ use miden_client::{ component::{AuthScheme, AuthSingleSig, BasicWallet}, AccountBuilder, AccountType, }, - auth::AuthSecretKey, + auth::{Approver, AuthSecretKey}, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, - rpc::{Endpoint, GrpcClient}, + rpc::Endpoint, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use rand::RngCore; +use rand::Rng; use std::sync::Arc; #[tokio::main] @@ -187,7 +176,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -200,10 +188,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -216,10 +203,10 @@ async fn main() -> anyhow::Result<()> { let builder = AccountBuilder::new(init_seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( key_pair.public_key().to_commitment(), AuthScheme::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet); let account = builder.build()?; @@ -229,7 +216,7 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&key_pair, account.id()).await?; println!("Account ID: {}", account.id()); - println!("No assets in Vault: {:?}", account.vault().is_empty()); + println!("No Assets in Vault: {:?}", account.vault().is_empty()); Ok(()) } @@ -275,21 +262,19 @@ Before we can work with tokens, we need a source of tokens. Let's create a fungi use miden_client::{ account::{ component::{ - AccessControl, AuthScheme, BurnPolicyConfig, FungibleFaucet, MintPolicyConfig, - PolicyRegistration, TokenName, TokenPolicyManager, TransferPolicy, - create_fungible_faucet, + AuthScheme, AuthSingleSig, BurnPolicy, FungibleFaucet, MintPolicy, TokenName, + TokenPolicyManager, TransferPolicy, create_singlesig_user_fungible_faucet, }, AccountType, }, asset::{AssetAmount, TokenSymbol}, - auth::AuthSecretKey, + auth::{Approver, AuthSecretKey}, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, - rpc::{Endpoint, GrpcClient}, + rpc::Endpoint, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_standards::AuthMethod; -use rand::RngCore; +use rand::Rng; use std::sync::Arc; #[tokio::main] @@ -297,7 +282,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -310,10 +294,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -338,23 +321,22 @@ async fn main() -> anyhow::Result<()> { .decimals(decimals) .max_supply(max_supply) .build()?; - let policies = TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; - let faucet_account = create_fungible_faucet( + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + let auth = AuthSingleSig::new(Approver::new( + key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( init_seed, faucet, - AccountType::Public, - AuthMethod::SingleSig { - approver: ( - key_pair.public_key().to_commitment(), - AuthScheme::Falcon512Poseidon2, - ), - }, - AccessControl::AuthControlled, + auth, policies, + AccountType::Public, )?; client.add_account(&faucet_account, false).await?; @@ -406,11 +388,6 @@ Faucet account ID: 0xde0ba31282f7522046d3d4af40722b - **Public Accounts**: Account state is fully transparent and visible onchain - **Private Accounts**: Only cryptographic commitments are stored onchain, with full state maintained privately -**Storage Modes:** - -- **Public**: Account state is fully transparent and visible onchain -- **Private**: Only cryptographic commitments are stored onchain, with full state maintained privately - **Modular Components:** - **BasicWallet**: Provides asset management functionality diff --git a/docs/builder/get-started/index.md b/docs/builder/get-started/index.md index 120fb9a1..51c7550f 100644 --- a/docs/builder/get-started/index.md +++ b/docs/builder/get-started/index.md @@ -18,7 +18,7 @@ Key concepts you'll encounter: - **Accounts**: smart contracts that hold assets and execute code - **Notes**: messages that exchange data and assets between accounts — also programmable - **Assets**: tokens that can be fungible or non-fungible -- **Privacy**: every transaction, note, and account in Miden is private by default — only the involved parties can view asset amounts or transfer details +- **Privacy**: accounts and notes can be public or private. Private accounts keep their state offchain, while private notes hide their details behind commitments ## Getting started diff --git a/docs/builder/get-started/notes.md b/docs/builder/get-started/notes.md index dd2f34b5..f8ffacf0 100644 --- a/docs/builder/get-started/notes.md +++ b/docs/builder/get-started/notes.md @@ -79,23 +79,22 @@ Let's see this in action: use miden_client::{ account::{ component::{ - AccessControl, AuthScheme, AuthSingleSig, BasicWallet, BurnPolicyConfig, - FungibleFaucet, MintPolicyConfig, PolicyRegistration, TokenName, TokenPolicyManager, - TransferPolicy, create_fungible_faucet, + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, }, AccountBuilder, AccountType, }, - asset::{AssetAmount, AssetCallbackFlag, FungibleAsset, TokenSymbol}, - auth::AuthSecretKey, + asset::{AssetAmount, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, note::NoteType, - rpc::{Endpoint, GrpcClient}, + rpc::Endpoint, transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_standards::AuthMethod; -use rand::RngCore; +use rand::Rng; use std::sync::Arc; #[tokio::main] @@ -103,7 +102,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -116,10 +114,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -147,10 +144,10 @@ async fn main() -> anyhow::Result<()> { // Build the account let account_builder = AccountBuilder::new(alice_seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( alice_key_pair.public_key().to_commitment(), AuthScheme::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet); // Build the faucet @@ -160,29 +157,28 @@ async fn main() -> anyhow::Result<()> { .decimals(decimals) .max_supply(max_supply) .build()?; - let policies = TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); let alice_account = account_builder.build()?; - let faucet_account = create_fungible_faucet( + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( faucet_seed, faucet, - AccountType::Public, - AuthMethod::SingleSig { - approver: ( - faucet_key_pair.public_key().to_commitment(), - AuthScheme::Falcon512Poseidon2, - ), - }, - AccessControl::AuthControlled, + faucet_auth, policies, + AccountType::Public, )?; - println!("Alice's account ID: {:?}", alice_account.id().to_hex()); - println!("Faucet account ID: {:?}", faucet_account.id().to_hex()); + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); // Add accounts to client client.add_account(&alice_account, false).await?; @@ -193,14 +189,13 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - // Enable asset callbacks so the faucet's send/receive transfer policies run - // when this asset moves between accounts. - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)? - .with_callbacks(AssetCallbackFlag::Enabled); + // The faucet account ID encodes callback support for the transfer policies above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; // Build transaction request to mint fungible asset to Alice's account // NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset) // for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault + println!("Minting 1000 tokens to Alice..."); let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( fungible_asset, alice_account.id(), @@ -215,7 +210,7 @@ async fn main() -> anyhow::Result<()> { client.sync_state().await?; println!( - "Mint transaction submitted successfully, ID: {:?}", + "Mint transaction submitted successfully, ID: {}", tx_id.to_hex() ); @@ -299,23 +294,22 @@ This is a complete, self-contained example that includes the setup and minting s use miden_client::{ account::{ component::{ - AccessControl, AuthScheme, AuthSingleSig, BasicWallet, BurnPolicyConfig, - FungibleFaucet, MintPolicyConfig, PolicyRegistration, TokenName, TokenPolicyManager, - TransferPolicy, create_fungible_faucet, + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, }, Account, AccountBuilder, AccountType, }, - asset::{AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol}, - auth::AuthSecretKey, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, note::NoteType, - rpc::{Endpoint, GrpcClient}, + rpc::Endpoint, transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_standards::AuthMethod; -use rand::RngCore; +use rand::Rng; use std::sync::Arc; use tokio::time::Duration; @@ -324,7 +318,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -337,10 +330,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -368,10 +360,10 @@ async fn main() -> anyhow::Result<()> { // Build the account let account_builder = AccountBuilder::new(alice_seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( alice_key_pair.public_key().to_commitment(), AuthScheme::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet); // Build the faucet @@ -381,29 +373,28 @@ async fn main() -> anyhow::Result<()> { .decimals(decimals) .max_supply(max_supply) .build()?; - let policies = TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); let alice_account = account_builder.build()?; - let faucet_account = create_fungible_faucet( + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( faucet_seed, faucet, - AccountType::Public, - AuthMethod::SingleSig { - approver: ( - faucet_key_pair.public_key().to_commitment(), - AuthScheme::Falcon512Poseidon2, - ), - }, - AccessControl::AuthControlled, + faucet_auth, policies, + AccountType::Public, )?; - println!("Alice's account ID: {:?}", alice_account.id().to_hex()); - println!("Faucet account ID: {:?}", faucet_account.id().to_hex()); + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); // Add accounts to client client.add_account(&alice_account, false).await?; @@ -414,14 +405,13 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - // Enable asset callbacks so the faucet's send/receive transfer policies run - // when this asset moves between accounts. - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)? - .with_callbacks(AssetCallbackFlag::Enabled); + // The faucet account ID encodes callback support for the transfer policies above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; // Build transaction request to mint fungible asset to Alice's account // NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset) // for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault + println!("Minting 1000 tokens to Alice..."); let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( fungible_asset, alice_account.id(), @@ -436,7 +426,7 @@ async fn main() -> anyhow::Result<()> { client.sync_state().await?; println!( - "Mint transaction submitted successfully, ID: {:?}", + "Mint transaction submitted successfully, ID: {}", tx_id.to_hex() ); @@ -446,6 +436,7 @@ async fn main() -> anyhow::Result<()> { // Public notes must be committed to a block before they can be consumed. // Poll until the network includes our mint note in a block. + println!("Waiting for note to be consumable..."); loop { // Sync state to get the latest block client.sync_state().await?; @@ -455,7 +446,6 @@ async fn main() -> anyhow::Result<()> { .await?; if consumable_notes.is_empty() { - println!("Waiting for P2ID note to be comitted..."); tokio::time::sleep(Duration::from_secs(2)).await; continue; } @@ -473,7 +463,7 @@ async fn main() -> anyhow::Result<()> { .await?; println!( - "Consume transaction submitted successfully, ID: {:?}", + "Consume transaction submitted successfully, ID: {}", consume_tx_id.to_hex() ); @@ -485,15 +475,10 @@ async fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow::anyhow!("Account not found"))? .try_into()?; let vault = alice_account.vault(); - // The callback flag is part of the vault key, so it must match the flag the - // asset was minted with — otherwise the lookup misses and the balance reads 0. - let balance_key = AssetVaultKey::new_fungible( - faucet_account.id(), - AssetCallbackFlag::Enabled, - ); + let asset_id = AssetId::new_fungible(faucet_account.id()); println!( - "Alice's TEST token balance: {:?}", - vault.get_balance(balance_key) + "Alice's TEST token balance: {}", + vault.get_balance(asset_id)? ); break; // Exit the loop after consuming the note @@ -543,6 +528,7 @@ export async function demo() { ); // List notes available to Alice and consume them — tokens move into her vault. + console.log("Waiting for note to be consumable..."); const notes = await client.notes.listAvailable({ account: alice }); const consumeResult = await client.transactions.consume({ account: alice, @@ -569,13 +555,13 @@ export async function demo() { Expected output ```text -Alice's account ID: "0x5b2840a923dedc102ea67e0c1eba3c" -Faucet account ID: "0x29dd1dc628d2842032e751ed1b5da7" +Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c +Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7 Minting 1000 tokens to Alice... -Mint transaction submitted successfully, ID: "0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06" +Mint transaction submitted successfully, ID: 0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06 Waiting for note to be consumable... -Consume transaction submitted successfully, ID: "0xa75872c498ee71cd6725aef9411d2559094cec1e1e89670dbf99c60bb8843481" -Alice's TEST token balance: Ok(AssetAmount(1000)) +Consume transaction submitted successfully, ID: 0xa75872c498ee71cd6725aef9411d2559094cec1e1e89670dbf99c60bb8843481 +Alice's TEST token balance: 1000 ``` @@ -604,23 +590,22 @@ This is a complete, self-contained example that includes all previous steps. **T use miden_client::{ account::{ component::{ - AccessControl, AuthScheme, AuthSingleSig, BasicWallet, BurnPolicyConfig, - FungibleFaucet, MintPolicyConfig, PolicyRegistration, TokenName, TokenPolicyManager, - TransferPolicy, create_fungible_faucet, + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, }, Account, AccountBuilder, AccountType, }, - asset::{AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol}, - auth::AuthSecretKey, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, - note::{NoteAttachments, NoteType, P2idNote}, - rpc::{Endpoint, GrpcClient}, + note::{NoteType, P2idNote}, + rpc::Endpoint, transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_standards::AuthMethod; -use rand::RngCore; +use rand::Rng; use std::sync::Arc; use tokio::time::Duration; @@ -629,7 +614,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -642,10 +626,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -673,10 +656,10 @@ async fn main() -> anyhow::Result<()> { // Build the account let account_builder = AccountBuilder::new(alice_seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( alice_key_pair.public_key().to_commitment(), AuthScheme::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet); // Build the faucet @@ -686,29 +669,28 @@ async fn main() -> anyhow::Result<()> { .decimals(decimals) .max_supply(max_supply) .build()?; - let policies = TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); let alice_account = account_builder.build()?; - let faucet_account = create_fungible_faucet( + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( faucet_seed, faucet, - AccountType::Public, - AuthMethod::SingleSig { - approver: ( - faucet_key_pair.public_key().to_commitment(), - AuthScheme::Falcon512Poseidon2, - ), - }, - AccessControl::AuthControlled, + faucet_auth, policies, + AccountType::Public, )?; - println!("Alice's account ID: {:?}", alice_account.id().to_hex()); - println!("Faucet account ID: {:?}", faucet_account.id().to_hex()); + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); // Add accounts to client client.add_account(&alice_account, false).await?; @@ -719,14 +701,13 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - // Enable asset callbacks so the faucet's send/receive transfer policies run - // when this asset moves between accounts. - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)? - .with_callbacks(AssetCallbackFlag::Enabled); + // The faucet account ID encodes callback support for the transfer policies above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; // Build transaction request to mint fungible asset to Alice's account // NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset) // for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault + println!("Minting 1000 tokens to Alice..."); let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( fungible_asset, alice_account.id(), @@ -741,7 +722,7 @@ async fn main() -> anyhow::Result<()> { client.sync_state().await?; println!( - "Mint transaction submitted successfully, ID: {:?}", + "Mint transaction submitted successfully, ID: {}", tx_id.to_hex() ); @@ -751,6 +732,7 @@ async fn main() -> anyhow::Result<()> { // Public notes must be committed to a block before they can be consumed. // Poll until the network includes our mint note in a block. + println!("Waiting for note to be consumable..."); loop { // Sync state to get the latest block client.sync_state().await?; @@ -760,7 +742,6 @@ async fn main() -> anyhow::Result<()> { .await?; if consumable_notes.is_empty() { - println!("Waiting for P2ID note to be comitted..."); tokio::time::sleep(Duration::from_secs(2)).await; continue; } @@ -778,7 +759,7 @@ async fn main() -> anyhow::Result<()> { .await?; println!( - "Consume transaction submitted successfully, ID: {:?}", + "Consume transaction submitted successfully, ID: {}", consume_tx_id.to_hex() ); @@ -790,15 +771,10 @@ async fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow::anyhow!("Account not found"))? .try_into()?; let vault = alice_account.vault(); - // The callback flag is part of the vault key, so it must match the flag the - // asset was minted with — otherwise the lookup misses and the balance reads 0. - let balance_key = AssetVaultKey::new_fungible( - faucet_account.id(), - AssetCallbackFlag::Enabled, - ); + let asset_id = AssetId::new_fungible(faucet_account.id()); println!( - "Alice's TEST token balance: {:?}", - vault.get_balance(balance_key) + "Alice's TEST token balance: {}", + vault.get_balance(asset_id)? ); break; // Exit the loop after consuming the note @@ -814,31 +790,31 @@ async fn main() -> anyhow::Result<()> { let bob_key_pair = AuthSecretKey::new_falcon512_poseidon2(); let bob_account = AccountBuilder::new(bob_seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( bob_key_pair.public_key().to_commitment(), AuthScheme::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet) .build()?; client.add_account(&bob_account, false).await?; keystore.add_key(&bob_key_pair, bob_account.id()).await?; - println!("Bob's account ID: {:?}", bob_account.id().to_hex()); + println!("Bob's account ID: {}", bob_account.id().to_hex()); let bob_account_id = bob_account.id(); let send_amount = 100; - let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)? - .with_callbacks(AssetCallbackFlag::Enabled); - - let p2id_note = P2idNote::create( - alice_account.id(), - bob_account_id, - vec![fungible_asset_to_send.into()], - NoteType::Public, - NoteAttachments::empty(), - client.rng(), - )?; + let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)?; + + println!("Sending 100 tokens to Bob..."); + let p2id_note = P2idNote::builder() + .sender(alice_account.id()) + .target(bob_account_id) + .asset(fungible_asset_to_send) + .note_type(NoteType::Public) + .generate_serial_number(client.rng()) + .build()? + .into(); // Create transaction request to send P2ID note to Bob let send_p2id_note_transaction_request = TransactionRequestBuilder::new() @@ -852,7 +828,7 @@ async fn main() -> anyhow::Result<()> { client.sync_state().await?; println!( - "Send 100 tokens to Bob note transaction ID: {:?}", + "Send transaction submitted successfully, ID: {}", send_p2id_note_tx_id.to_hex() ); @@ -898,6 +874,7 @@ export async function demo() { mintResult.txId.toHex(), ); + console.log("Waiting for note to be consumable..."); const notes = await client.notes.listAvailable({ account: alice }); const consumeResult = await client.transactions.consume({ account: alice, @@ -946,10 +923,12 @@ Alice's account ID: 0xd6b8bb0ed10b1610282c513501778a Faucet account ID: 0xe48c43d6ad6496201bcfa585a5a4b6 Minting 1000 tokens to Alice... Mint transaction submitted successfully, ID: 0x948a0eef754068b3126dd3261b6b54214fa5608fb13c5e5953faf59bad79c75f +Waiting for note to be consumable... Consume transaction submitted successfully, ID: 0xc69ab84b784120abe858bb536aebda90bd2067695f11d5da93ab0b704f39ad78 Alice's TEST token balance: 1000 Bob's account ID: 0x103f8a1ad4b983104aec0412ab0b0d -Send 100 tokens to Bob note transaction ID: "0x51ac27474ade3a54adadd50db6c2b9a2ede254c5f9137f93d7a970f0bc7d66d5" +Sending 100 tokens to Bob... +Send transaction submitted successfully, ID: 0x51ac27474ade3a54adadd50db6c2b9a2ede254c5f9137f93d7a970f0bc7d66d5 ``` diff --git a/docs/builder/get-started/read-storage.md b/docs/builder/get-started/read-storage.md index d15baec8..39c20070 100644 --- a/docs/builder/get-started/read-storage.md +++ b/docs/builder/get-started/read-storage.md @@ -37,10 +37,10 @@ Let's interact with a counter contract deployed on the Miden testnet. This contr ```rust title="integration/src/bin/read-count.rs" use integration::helpers::{counter_storage_slot, COUNTER_STORAGE_KEY}; use miden_client::{ - account::{Account, AccountId}, + account::{Account, AccountId, StorageMapKey}, builder::ClientBuilder, keystore::FilesystemKeyStore, - rpc::{Endpoint, GrpcClient}, + rpc::Endpoint, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; use std::sync::Arc; @@ -50,7 +50,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -63,10 +62,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -92,9 +90,12 @@ async fn main() -> anyhow::Result<()> { // name and the map key come from the project's `integration/src/helpers.rs`. let count = counter_account .storage() - .get_map_item(&counter_storage_slot()?, COUNTER_STORAGE_KEY)?; + .get_map_item( + &counter_storage_slot()?, + StorageMapKey::new(COUNTER_STORAGE_KEY), + )?; - println!("Count: {:?}", count); + println!("Count: {}", count[0].as_canonical_u64()); Ok(()) } @@ -130,7 +131,7 @@ export async function demo() { Expected output ```text -Count: Word([1, 0, 0, 0]) +Count: 1 ``` @@ -143,23 +144,22 @@ You can also query the assets (tokens) held by an account: use miden_client::{ account::{ component::{ - AccessControl, AuthScheme, AuthSingleSig, BasicWallet, BurnPolicyConfig, - FungibleFaucet, MintPolicyConfig, PolicyRegistration, TokenName, TokenPolicyManager, - TransferPolicy, create_fungible_faucet, + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, }, Account, AccountBuilder, AccountType, }, - asset::{AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol}, - auth::AuthSecretKey, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, note::NoteType, - rpc::{Endpoint, GrpcClient}, + rpc::Endpoint, transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_standards::AuthMethod; -use rand::RngCore; +use rand::Rng; use std::sync::Arc; use tokio::time::Duration; @@ -168,7 +168,6 @@ async fn main() -> anyhow::Result<()> { // Initialize RPC connection let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); // Initialize keystore let keystore_path = std::path::PathBuf::from("./keystore"); @@ -181,10 +180,9 @@ async fn main() -> anyhow::Result<()> { // NOTE: The client is our entry point to the Miden network. // All interactions with the network go through the client. let mut client = ClientBuilder::new() - .rpc(rpc_client) + .grpc_client(&endpoint, Some(timeout_ms)) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await?; @@ -212,10 +210,10 @@ async fn main() -> anyhow::Result<()> { // Build the account let account_builder = AccountBuilder::new(alice_seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( alice_key_pair.public_key().to_commitment(), AuthScheme::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet); // Build the faucet @@ -225,29 +223,28 @@ async fn main() -> anyhow::Result<()> { .decimals(decimals) .max_supply(max_supply) .build()?; - let policies = TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); let alice_account = account_builder.build()?; - let faucet_account = create_fungible_faucet( + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( faucet_seed, faucet, - AccountType::Public, - AuthMethod::SingleSig { - approver: ( - faucet_key_pair.public_key().to_commitment(), - AuthScheme::Falcon512Poseidon2, - ), - }, - AccessControl::AuthControlled, + faucet_auth, policies, + AccountType::Public, )?; - println!("Alice's account ID: {:?}", alice_account.id().to_hex()); - println!("Faucet account ID: {:?}", faucet_account.id().to_hex()); + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); // Add accounts to client client.add_account(&alice_account, false).await?; @@ -258,10 +255,9 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - // Enable asset callbacks so the faucet's send/receive transfer policies run - // when this asset moves between accounts. - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)? - .with_callbacks(AssetCallbackFlag::Enabled); + // The faucet factory encodes callback support in the faucet account ID because + // transfer policies are configured above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; // Mint the asset to Alice — this creates a P2ID note she can consume. let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( @@ -277,6 +273,7 @@ async fn main() -> anyhow::Result<()> { // Public notes must be committed to a block before they can be consumed. // Poll until the network includes our mint note in a block. + println!("Waiting for note to be consumable..."); loop { client.sync_state().await?; @@ -285,7 +282,6 @@ async fn main() -> anyhow::Result<()> { .await?; if consumable_notes.is_empty() { - println!("Waiting for P2ID note to be comitted..."); tokio::time::sleep(Duration::from_secs(2)).await; continue; } @@ -315,15 +311,10 @@ async fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow::anyhow!("Account not found"))? .try_into()?; - // The callback flag is part of the vault key, so it must match the flag the asset - // was minted with — otherwise the lookup misses and the balance reads 0. - let balance_key = AssetVaultKey::new_fungible( - faucet_account.id(), - AssetCallbackFlag::Enabled, - ); - let balance = alice_account.vault().get_balance(balance_key)?; + let asset_id = AssetId::new_fungible(faucet_account.id()); + let balance = alice_account.vault().get_balance(asset_id)?; - println!("Alice's TEST token balance: {:?}", balance); + println!("Alice's TEST token balance: {}", balance); Ok(()) } @@ -362,6 +353,7 @@ export async function demo() { waitForConfirmation: true, }); + console.log("Waiting for note to be consumable..."); const notes = await client.notes.listAvailable({ account: alice }); await client.transactions.consume({ account: alice, @@ -384,10 +376,10 @@ export async function demo() { Expected output ```text -Alice's account ID: "0x5b2840a923dedc102ea67e0c1eba3c" -Faucet account ID: "0x29dd1dc628d2842032e751ed1b5da7" -Waiting for P2ID note to be comitted... -Alice's TEST token balance: AssetAmount(1000) +Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c +Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7 +Waiting for note to be consumable... +Alice's TEST token balance: 1000 ``` diff --git a/docs/builder/get-started/setup/cli-basics.md b/docs/builder/get-started/setup/cli-basics.md index 838df4d4..8b66b665 100644 --- a/docs/builder/get-started/setup/cli-basics.md +++ b/docs/builder/get-started/setup/cli-basics.md @@ -13,23 +13,26 @@ This guide covers essential Miden CLI commands for creating accounts, minting an Create a new Miden wallet account: ```bash title=">_ Terminal" -miden client new-wallet +miden client sync +miden client new-wallet --deploy ```
Expected output ```text +State synced to block +... +Generated and stored Falcon512 authentication key in keystore. Successfully created new wallet. -To view account details execute miden-client account --show 0x05bd1f642cd368800cc95956b2696a -Config updated successfully +To view account details execute miden client account -s 0x05bd1f642cd368800cc95956b2696a Setting account 0x05bd1f642cd368800cc95956b2696a as the default account ID. -You can unset it with `miden-client account --default none`. +You can unset it with `miden client account --default none`. ```
-This command creates a basic wallet account with **private** storage, giving you full control while keeping your data confidential. +The first command synchronizes the client with the latest network state. The second creates a basic wallet account with **private** storage and deploys it onchain, giving you full control while keeping your data confidential. ### View Your Account @@ -43,9 +46,9 @@ miden client account Expected output ```text -| Account ID | Type | Storage Mode | Nonce | Status | -|------------|------|--------------|-------|--------| -| 0x970e3e4dbcd09b8035532edaa87bc9 | Regular | private | 0 | New | +| Account ID | Kind | Type | Nonce | Status | +|------------|------|------|-------|--------| +| 0x970e3e4dbcd09b8035532edaa87bc9 | Regular | private | 1 | Tracked | ``` @@ -68,12 +71,12 @@ Account Information | Address | mtst1qztsu0jdhngfhqp42vhd42rme9cqzkzy89e | | Account ID (hex) | 0x970e3e4dbcd09b8035532edaa87bc9 | | Account Commitment| 0x404a762b9a19e70bc8752381b17f909bc0bbab02c0b4636d8923d088ac8ebc04 | -| Type | Regular | -| Storage mode | private | +| Kind | Regular | +| Type | private | | Code Commitment | 0x6a11161925930dae89cc24cbddf0d161cead39b0fe88c262d4e790cff35be01d | | Vault Root | 0x3e128c57f6cfa0d44ab1308994171af13cb513422add28d1916b3ff254fef82d | | Storage Root | 0x5f95d38174f10c8ce91a0202763b0813fdcbb2714704cda411af6483ebc8d012 | -| Nonce | 0 | +| Nonce | 1 | Assets: @@ -83,9 +86,11 @@ Assets: Storage: -| Item Slot Index | Item Slot Type | Value/Commitment | -|-----------------|----------------|------------------| -| 0 | Value | 0xa52ef6357625c54a2eaefd11b8cfc2ee3429c37d9f8a827e23886857ea284834 | +| Slot Name | Slot Type | Value/Commitment | +|----------------------------------------------------------|-----------|--------------------------------------------------------------------| +| miden::standards::auth::singlesig::scheme | Value | 0x0200000000000000000000000000000000000000000000000000000000000000 | +| miden::standards::auth::singlesig::pub_key | Value | 0x113697002c3061328fce8c1e26dc433c536e967c8b91f30d81517e47f5980b3c | +| miden::standards::inspection::storage_schema::commitment | Value | 0xb5724e35b8267d3be6bfc7d0ce50bfd6cce52de6da9f9e847ab24ac1bf7770f1 | ``` @@ -110,7 +115,7 @@ miden client account --default ### Deploy Your Account -The `miden client new-wallet` command above already deploys your account onchain automatically. You can verify your account is deployed by syncing and checking its status: +The `miden client new-wallet --deploy` command above deploys your account onchain. You can verify the deployment by syncing and checking that its status is `Tracked`: ```bash title=">_ Terminal" miden client sync @@ -162,7 +167,7 @@ Creates a minimal **Vite example project with Miden integration**, built on the Initialize the client in your working directory when you want to test against a custom network endpoint or use different keys without touching your global config: ```bash title=">_ Terminal" -miden client init --network devnet +miden client init --local --network devnet ``` Available networks: @@ -173,17 +178,21 @@ Available networks: ### Important Files Created -When you manually initialize the Miden client in your working directory, several local files are created: +When you initialize the Miden client with `--local`, a `.miden/` directory is created in your working directory with the following files: -- **`miden-client.toml`**: Configuration file with network settings -- **`store.sqlite3`**: Database storing your account data and transaction history -- **`keystore/`**: Directory containing your private keys (keep secure!) -- **`templates/`**: Pre-built smart contract components +- **`.miden/miden-client.toml`**: Configuration file with network settings +- **`.miden/store.sqlite3`**: Database storing your account data and transaction history +- **`.miden/keystore/`**: Directory containing your private keys (keep secure!) +- **`.miden/packages/`**: Pre-built account component packages :::danger -Private keys in the `keystore/` directory are **not encrypted**. Keep these files secure and never share them. +Private keys in the `.miden/keystore/` directory are **not encrypted**. Keep these files secure and never share them. ::: -To return to your global client configuration, remove the local `miden-client.toml` (and any local store/keystore files you no longer need). +To remove the local configuration and return to your global client configuration, run this command from the same working directory: + +```bash title=">_ Terminal" +miden client clear-config +``` --- diff --git a/docs/builder/get-started/setup/installation.md b/docs/builder/get-started/setup/installation.md index a71584d4..a61d401f 100644 --- a/docs/builder/get-started/setup/installation.md +++ b/docs/builder/get-started/setup/installation.md @@ -32,7 +32,7 @@ rustc --version Expected output ```text -rustc 1.93.0-nightly (fa3155a64 2025-09-30) +rustc 1.96.1 (31fca3adb 2026-06-26) ``` @@ -120,10 +120,10 @@ which miden **Install Miden Toolchain** -Install the latest stable Miden components: +Install the toolchain for the public testnet and make it the default: ```bash title=">_ Terminal" -midenup install stable +midenup install testnet && midenup override testnet ``` :::note @@ -142,7 +142,7 @@ midenup show active-toolchain Expected output ```text -stable +testnet ``` @@ -159,11 +159,11 @@ echo $PATH | tr ':' '\n' | grep cargo **"config error: missing field" when running `miden client` commands** -If you have config files from a previous Miden installation, they may be incompatible with the current version. Delete the old config and database, then re-initialize: +If you have configuration files from a previous Miden installation, they may be incompatible with the current version. Clear the active client configuration, then re-initialize it for testnet: ```bash title=">_ Terminal" -rm -f miden-client.toml store.sqlite3 -miden client init +miden client clear-config +miden client init --network testnet ``` ## Set Up a Project @@ -186,7 +186,7 @@ The TypeScript examples use the [`@miden-sdk/miden-sdk`](https://www.npmjs.com/p ```bash title=">_ Terminal" npm create vite@latest miden-app -- --template vanilla-ts cd miden-app -npm install @miden-sdk/miden-sdk@^0.15.0 +npm install @miden-sdk/miden-sdk@^0.16.0 ``` Open `src/main.ts` and replace its contents with a simple entry point that calls your demo: diff --git a/docs/builder/get-started/your-first-smart-contract/create.md b/docs/builder/get-started/your-first-smart-contract/create.md index 9c764570..ebe0380e 100644 --- a/docs/builder/get-started/your-first-smart-contract/create.md +++ b/docs/builder/get-started/your-first-smart-contract/create.md @@ -41,7 +41,7 @@ The project follows Miden's design philosophy of clean separation: - **`contracts/`**: Your primary working directory for writing Miden smart contract code - **`integration/`**: All onchain interactions, deployment scripts, and tests -Each contract is organized as its own individual crate, providing independent versioning, dependencies, and clear isolation between different contracts. Each contract crate also includes a `miden-project.toml` file next to `Cargo.toml`; the Miden compiler uses it to identify the project kind, WIT namespace, and generated interface dependencies. +Each contract is organized as its own individual crate, providing independent versioning, dependencies, and clear isolation between different contracts. Each contract crate also includes a `miden-project.toml` file next to `Cargo.toml`; the Miden compiler uses it to identify the project kind, WIT namespace, and compiled package dependencies. ### Project Manifests @@ -57,6 +57,7 @@ kind = "account-component" # Full `miden:/@` id. The interface segment is the # kebab-cased component trait name (`CounterContract` -> `counter-contract`). namespace = "miden:counter-account/counter-contract@0.1.0" +path = "src/lib.rs" [dependencies] miden-core = "*" @@ -66,7 +67,8 @@ miden-protocol = "*" supported-types = ["RegularAccountImmutableCode"] ``` -The increment note depends on the counter account's generated WIT so it can call the counter interface: +The increment note depends on the counter account package and the generated WIT +that describes its callable interface: ```toml title="contracts/increment-note/miden-project.toml" [package] @@ -77,6 +79,7 @@ version = "0.1.0" kind = "note" # Notes export a package-derived interface (`miden-`), matching the `#[note]` macro. namespace = "miden:increment-note/miden-increment-note@0.1.0" +path = "src/lib.rs" [dependencies] miden-core = "*" @@ -88,6 +91,12 @@ counter-account = { path = "../counter-account" } counter-account = { wit = "../counter-account/target/generated-wit/" } ``` +Build the contracts with `miden build` in dependency order, as shown below. +Building the account first produces both its `.masp` package and the generated +WIT consumed by the note's `#[account(...)]` wrapper. Plain `cargo check`, +`cargo build`, and IDE analysis do not automatically build or stage those +cross-component dependencies in the published SDK. + ## Building Your Contracts You can build individual contracts by navigating to their directory and running the Miden build command: @@ -132,8 +141,10 @@ struct CounterContractStorage { #[component] trait CounterContract { /// Returns the current counter value stored in the contract's storage map. + #[account_procedure] fn get_count(&self) -> Felt; /// Increments the counter value stored in the contract's storage map by one. + #[account_procedure] fn increment_count(&mut self) -> Felt; } @@ -185,7 +196,7 @@ These imports provide: - **`StorageMap`**: Key-value storage within account storage slots :::note[`felt` vs `Felt`] -`Felt` is the field element type representing values in the Goldilocks prime field (p = 2^64 - 2^32 + 1). `felt!(1)` is a compile-time macro that creates `Felt` values from integer literals with compile-time range validation. Currently `felt!` only accepts values up to 2^32 (compiler limitation); for larger values use `Felt::from_u64_unchecked()`. +`Felt` is the field element type representing values in the Goldilocks prime field (p = 2^64 - 2^32 + 1). `felt!(1)` creates a `Felt` from an integer literal and rejects out-of-range values at compile time. For runtime values, use the fallible `Felt::new(value)` and handle its `Result`. ::: #### Contract Structure Definition @@ -201,15 +212,17 @@ struct CounterContractStorage { #[component] trait CounterContract { /// Returns the current counter value stored in the contract's storage map. + #[account_procedure] fn get_count(&self) -> Felt; /// Increments the counter value stored in the contract's storage map by one. + #[account_procedure] fn increment_count(&mut self) -> Felt; } ``` -The `#[component_storage]` attribute marks the storage struct for this Miden [Account component](/reference/protocol/account), while the `#[component]` trait defines the component's public interface. The `count_map` field is a `StorageMap` stored in a named storage slot of the account. In the v0.15-aligned SDK, storage slots are identified by name rather than explicit index numbers — the slot name is derived automatically from the component's manifest namespace and field name (e.g., `counter_account::counter_contract::count_map`). +The `#[component_storage]` attribute marks the storage struct for this Miden [Account component](/reference/protocol/account), while the `#[component]` trait defines the component's interface. Every callable trait method must carry `#[account_procedure]`; an unmarked method is not exported. The `count_map` field is a `StorageMap` stored in a named storage slot of the account. Storage slots are identified by name rather than explicit index numbers — the slot name is derived automatically from the component's manifest namespace and field name (e.g., `counter_account::counter_contract::count_map`). -**Important**: Storage slots in Miden hold `Word` values, which are composed of four field elements (`Felt`). Each `Felt` is a 64-bit unsigned integer (u64). The `StorageMap` provides a key-value interface within a single storage slot, allowing you to store multiple key-value pairs within the four-element word structure. +**Important**: Miden account storage is organized into named slots. Each slot holds either a single typed value or a key-value map. Here, `StorageMap` provides typed access to a map-backed slot, converting its keys and values to and from `Word`. Each `Word` consists of four field elements (`Felt`), and each `Felt` belongs to the Goldilocks prime field and is represented using 64 bits. #### Contract Implementation @@ -270,7 +283,7 @@ impl IncrementNote { #### No-std Setup -Similar to the account contract, the note script uses `#![no_std]` with the same allocator and panic handler setup. +Like the account contract, the note script uses `#![no_std]` and enables the `alloc_error_handler` language feature. #### Miden Imports diff --git a/docs/builder/get-started/your-first-smart-contract/deploy.md b/docs/builder/get-started/your-first-smart-contract/deploy.md index 68c6c977..c6883466 100644 --- a/docs/builder/get-started/your-first-smart-contract/deploy.md +++ b/docs/builder/get-started/your-first-smart-contract/deploy.md @@ -111,7 +111,7 @@ This process shows how Miden contracts are deployed through state changes rather ## How the Scripts Work -The integration scripts work by connecting to the Miden client and then building contracts from the Miden package files. These package files are generated when you run `miden build` inside each contract directory, but the scripts handle this compilation step automatically - you don't need to manually build the contracts before running the scripts. +The integration scripts connect to the Miden client and compile each contract by invoking `miden build` as a separate process. After each build, the helper loads the generated Miden package into the native client. You don't need to build the contracts manually before running the script. Next, we look into how the scripts convert your Rust contract code into deployable Miden contracts. @@ -148,10 +148,10 @@ let note_package = Arc::new( The `build_project_in_dir()` function: -- Takes the path to your contract's Rust source code -- Compiles the Rust code into a Miden package (`.masp` file) -- Generates a package containing the compiled contract bytecode and metadata -- This is equivalent to manually running `miden build` in each contract directory +- Takes the path to a contract project +- Invokes `miden build` in a separate process, using `--release` when requested +- Resolves the generated `.masp` artifact under the project's `target/miden/` directory +- Reads and deserializes the compiled package for the native client These packages contain all the information needed to deploy and interact with your contracts on the Miden network. @@ -187,7 +187,7 @@ The `create_account_from_package()` function: - Combines it with the provided configuration (storage, settings, etc.) - Creates a deployable Miden account that can be used in transactions -**Important**: Accounts that use storage must have that storage seeded when instantiating the account. In the v0.15-aligned SDK, storage slots are identified by name rather than index. The slot name follows the pattern `::::`, derived from the component's manifest namespace. We seed the storage with: +**Important**: Accounts that use storage must have that storage seeded when instantiating the account. Storage slots are identified by name rather than index. The slot name follows the pattern `::::`, derived from the component's manifest namespace. We seed the storage with: - A named `StorageMap` slot, returned by the `counter_storage_slot()` helper (`counter_account::counter_contract::count_map`) - The counter key `COUNTER_STORAGE_KEY` (`[0, 0, 0, 1]`), mapped to the initial count `0` diff --git a/docs/builder/get-started/your-first-smart-contract/test.md b/docs/builder/get-started/your-first-smart-contract/test.md index 2fa8c5fa..62f386f3 100644 --- a/docs/builder/get-started/your-first-smart-contract/test.md +++ b/docs/builder/get-started/your-first-smart-contract/test.md @@ -65,7 +65,9 @@ use std::{path::Path, sync::Arc}; use anyhow::Context; use integration::helpers::{build_project_in_dir, counter_storage_slot, COUNTER_STORAGE_KEY}; use miden_client::{ - account::{component::InitStorageData, AccountBuilder, AccountComponent, AccountType}, + account::{ + component::InitStorageData, AccountBuilder, AccountComponent, AccountType, StorageMapKey, + }, auth::AuthSchemeId, crypto::RandomCoin, note::NoteScript, @@ -128,13 +130,14 @@ async fn counter_test() -> anyhow::Result<()> { // Build the mock chain let mut mock_chain = builder.build()?; - // Build the transaction context - let tx_context = mock_chain - .build_tx_context(counter_account.clone(), &[counter_note.id()], &[])? + // Build the transaction from the committed account and input note + let transaction = mock_chain + .build_transaction(counter_account.id()) + .authenticated_input_note(counter_note.id()) .build()?; // Execute the transaction - let executed_transaction = tx_context.execute().await?; + let executed_transaction = transaction.execute().await?; // Add the executed transaction to the mockchain mock_chain.add_pending_executed_transaction(&executed_transaction)?; @@ -144,7 +147,10 @@ async fn counter_test() -> anyhow::Result<()> { let count = mock_chain .committed_account(counter_account.id())? .storage() - .get_map_item(&counter_storage_slot, COUNTER_STORAGE_KEY) + .get_map_item( + &counter_storage_slot, + StorageMapKey::new(COUNTER_STORAGE_KEY), + ) .expect("Failed to get counter value from storage slot"); assert_eq!( @@ -251,16 +257,17 @@ The counter account does not need a separate `add_account()` call: `add_account_ ### 5. Creating and Executing the Transaction ```rust -let tx_context = mock_chain - .build_tx_context(counter_account.clone(), &[counter_note.id()], &[])? +let transaction = mock_chain + .build_transaction(counter_account.id()) + .authenticated_input_note(counter_note.id()) .build()?; -let executed_transaction = tx_context.execute().await?; +let executed_transaction = transaction.execute().await?; ``` **What's happening:** -- We **build the transaction context** using the counter account and counter note +- We **build the transaction** against the committed counter account and add the counter note as an authenticated input - We **execute the transaction** - this runs the increment logic locally in the mockchain ### 6. Verifying the Results @@ -274,7 +281,10 @@ mock_chain.prove_next_block()?; let count = mock_chain .committed_account(counter_account.id())? .storage() - .get_map_item(&counter_storage_slot, COUNTER_STORAGE_KEY) + .get_map_item( + &counter_storage_slot, + StorageMapKey::new(COUNTER_STORAGE_KEY), + ) .expect("Failed to get counter value from storage slot"); assert_eq!( diff --git a/docs/builder/index.md b/docs/builder/index.md index 3aeaa549..e2a97710 100644 --- a/docs/builder/index.md +++ b/docs/builder/index.md @@ -45,7 +45,7 @@ Building against the public testnet? Grab free test assets from the [faucet](htt ## Ship - + Breaking changes, renames, and new features across accounts, notes, transactions, MASM, and the client. diff --git a/docs/reference/index.md b/docs/reference/index.md index a9934185..afa23a0b 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -44,7 +44,7 @@ Miden is a zero-knowledge layer 2 that rethinks blockchain architecture. Instead | Principle | How Miden achieves it | |-----------|----------------------| -| **Privacy** | Accounts and notes store only cryptographic commitments onchain; full data remains with users | +| **Privacy** | Public accounts and notes publish their full state or details; private variants keep them offchain behind commitments | | **Parallelism** | Single-account transactions enable concurrent execution without contention | | **Scalability** | Client-side proving offloads computation; proof aggregation reduces onchain verification | | **Programmability** | A Turing-complete VM supports arbitrary smart contract logic in accounts and notes | @@ -71,9 +71,9 @@ The protocol layer defines Miden's data structures, state model, and transaction Accounts are programmable entities that hold assets and execute code: -- **ID** — unique identifier derived from initial code and storage +- **ID** — immutable identifier committed to a seed and the initial code and storage; it also encodes the account type and asset-callback flag - **Code** — smart contract logic defining the account's interface -- **Storage** — key-value store with up to 256 slots for persistent data +- **Storage** — key-value store with up to 255 slots for persistent data - **Vault** — container holding fungible and non-fungible assets - **Nonce** — monotonically increasing counter preventing replay attacks @@ -83,12 +83,12 @@ Account code is composed from **components** — modular building blocks that ad Notes are programmable messages that transfer assets between accounts: -- **Script** — code executed when the note is consumed -- **Inputs** — public data available to the consuming transaction -- **Assets** — tokens transferred to the recipient -- **Metadata** — sender, tag (for discovery), and auxiliary data +- **Assets** — up to 16 fungible or non-fungible assets carried by the note +- **Recipient** — the serial number, script, and storage that define the consumption conditions +- **Metadata** — sender, note type, tag, and attachment headers and commitment; always public +- **Attachments** — optional public auxiliary data associated with the note -Notes can be **public** (all data onchain) or **private** (only a commitment stored). Private notes require offchain communication between sender and recipient. +Public notes publish their metadata, attachments, and full note details. Private notes still publish metadata and attachments, but publish only a commitment to the note details; the consumer must obtain those details separately. ### State model @@ -138,7 +138,7 @@ The Miden VM is a STARK-based virtual machine optimized for zero-knowledge proof Chiplets are co-processors that accelerate common operations: -- **Hash chiplet** — Rescue Prime Optimized hashing, Merkle tree operations +- **Hash chiplet** — Poseidon2 hashing and Merkle tree operations - **Bitwise chiplet** — AND, XOR, and other bitwise operations on 32-bit integers - **Memory chiplet** — efficient random-access memory with read/write tracking - **Kernel ROM** — secure execution of privileged kernel procedures From 9cb5fb65c1905a9d642ea250627db0b5f859261f Mon Sep 17 00:00:00 2001 From: 0xrouss-miden <312482795+0xrouss-miden@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:14 +0200 Subject: [PATCH 2/6] docs(smart-contracts): refresh APIs and examples for v0.16 Align the authored smart-contract reference with the v0.16 account, note, transaction, standards, MASM, and Rust SDK surfaces. --- .../accounts/account-operations.md | 57 ++++----- .../accounts/authentication.md | 21 +-- .../smart-contracts/accounts/components.md | 50 ++++---- .../smart-contracts/accounts/cryptography.md | 5 +- .../smart-contracts/accounts/custom-types.md | 17 ++- .../smart-contracts/accounts/introduction.md | 20 +-- .../accounts/network-accounts.md | 118 ++++++++++++----- .../smart-contracts/accounts/storage.md | 8 +- .../smart-contracts/cross-component-calls.md | 40 +++--- docs/builder/smart-contracts/index.md | 2 - docs/builder/smart-contracts/masm/index.md | 2 - .../smart-contracts/notes/introduction.md | 38 +++--- .../smart-contracts/notes/note-scripts.md | 29 +++-- .../smart-contracts/notes/note-types.md | 120 ++++++++++-------- .../smart-contracts/notes/output-notes.md | 50 +++++--- .../smart-contracts/notes/reading-notes.md | 59 +++++---- docs/builder/smart-contracts/overview.md | 1 + docs/builder/smart-contracts/patterns.md | 9 +- docs/builder/smart-contracts/rust/index.md | 2 - .../standards/account-components.md | 25 ++-- .../standards/faucets-and-policies.md | 53 ++++---- .../smart-contracts/standards/index.md | 2 +- .../standards/standard-notes.md | 41 +++--- .../transactions/introduction.md | 24 ++-- .../transactions/transaction-context.md | 16 +-- .../transactions/transaction-scripts.md | 42 ++++-- docs/builder/smart-contracts/types.md | 29 +++-- 27 files changed, 497 insertions(+), 383 deletions(-) diff --git a/docs/builder/smart-contracts/accounts/account-operations.md b/docs/builder/smart-contracts/accounts/account-operations.md index 9285537e..b324b5f8 100644 --- a/docs/builder/smart-contracts/accounts/account-operations.md +++ b/docs/builder/smart-contracts/accounts/account-operations.md @@ -16,24 +16,24 @@ impl MyAccount for MyAccountStorage { fn check_state(&self) { // Account identity let id: AccountId = self.get_id(); - let nonce: Felt = self.get_nonce(); + let nonce: Nonce = self.get_nonce(); // Vault queries - let balance: Felt = self.get_balance(asset_key); - let initial: Felt = self.get_initial_balance(asset_key); - let has_nft: bool = self.has_non_fungible_asset(asset); + let value: Word = self.get_asset(asset_id); + let initial_value: Word = native_account::get_initial_asset(asset_id); + let has_asset: bool = self.has_asset(asset_id); let root: Word = self.get_vault_root(); - let initial_root: Word = self.get_initial_vault_root(); + let initial_root: Word = native_account::get_initial_vault_root(); // Commitment queries let commitment: Word = self.compute_commitment(); - let initial_commit: Word = self.get_initial_commitment(); + let initial_commit: Word = native_account::get_initial_commitment(); let storage: Word = self.compute_storage_commitment(); - let initial_storage: Word = self.get_initial_storage_commitment(); + let initial_storage: Word = native_account::get_initial_storage_commitment(); let code: Word = self.get_code_commitment(); // Procedure queries - let count: Felt = self.get_num_procedures(); + let count: u32 = self.get_num_procedures(); let proc_root: Word = self.get_procedure_root(0); let exists: bool = self.has_procedure(proc_root); } @@ -46,26 +46,15 @@ impl MyAccount for MyAccountStorage { #[component] impl MyAccount for MyAccountStorage { fn receive_asset(&mut self, asset: Asset) { - // Add an asset to the vault — returns the asset as stored - let stored: Asset = self.add_asset(asset); + // Add an asset to the vault — returns the resulting value word + let stored_value: Word = self.add_asset(asset); } fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) { - // Remove an asset from the vault — returns the removed asset + // Remove an asset from the vault — returns the resulting value word // Proof generation fails if the asset doesn't exist or insufficient balance - let removed: Asset = self.remove_asset(asset); - output_note::add_asset(removed, note_idx); - } - - fn auth(&mut self) { - // Increment the account nonce (replay protection) - let new_nonce: Felt = self.incr_nonce(); - - // Compute commitment of all state changes in this transaction - let delta: Word = self.compute_delta_commitment(); - - // Check if a specific procedure was called during this transaction - let called: bool = self.was_procedure_called(proc_root); + self.remove_asset(asset); + output_note::add_asset(asset, note_idx); } } ``` @@ -81,7 +70,6 @@ Several operations cause proof generation to fail if preconditions aren't met: | Operation | Fails when | |-----------|-----------| | `remove_asset(asset)` | Asset not in vault or insufficient balance | -| `get_balance(asset_key)` | Referenced asset key is non-fungible or invalid | | `get_procedure_root(index)` | Index out of bounds | | Any `assert!()` | Condition is false | | Transaction body (overall) | No state change occurred **and** no notes were consumed | @@ -92,7 +80,7 @@ When proof generation fails: 3. No state changes occur 4. The client receives an error describing the failure -The last row is enforced at end-of-execution by the VM kernel rather than mid-execution: a transaction that mutates no account state (storage, vault, or nonce) **and** consumes no notes is rejected. The Rust client also catches this case before submission as `TransactionRequestError::NoInputNotesNorAccountChange`. See [Empty Transaction](../../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) for the recommended pattern. +The last row is enforced at end-of-execution by the VM kernel rather than mid-execution: a transaction that mutates no account state (storage, vault, or nonce) **and** consumes no notes is rejected. See [Empty Transaction](../../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) for the recommended pattern. ## Example: ManagedWallet @@ -100,16 +88,19 @@ The last row is enforced at end-of-execution by the VM kernel rather than mid-ex #![no_std] #![feature(alloc_error_handler)] -use miden::{component, component_storage, output_note, Asset, Felt, NoteIdx, Word}; +use miden::{component, component_storage, output_note, Asset, NoteIdx, Word}; #[component_storage] struct ManagedWalletStorage; #[component] trait ManagedWallet { + #[account_procedure] fn receive_asset(&mut self, asset: Asset); + #[account_procedure] fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx); - fn balance_of(&self, asset_key: Word) -> Felt; + #[account_procedure] + fn asset_value(&self, asset_id: Word) -> Word; } #[component] @@ -121,13 +112,13 @@ impl ManagedWallet for ManagedWalletStorage { /// Send an asset to an output note, with balance check. fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) { - let removed = self.remove_asset(asset); - output_note::add_asset(removed, note_idx); + self.remove_asset(asset); + output_note::add_asset(asset, note_idx); } - /// Query the balance of a fungible asset. - fn balance_of(&self, asset_key: Word) -> Felt { - self.get_balance(asset_key) + /// Read the value word stored under an asset ID. + fn asset_value(&self, asset_id: Word) -> Word { + self.get_asset(asset_id) } } ``` diff --git a/docs/builder/smart-contracts/accounts/authentication.md b/docs/builder/smart-contracts/accounts/authentication.md index 4be67af3..ea06b11c 100644 --- a/docs/builder/smart-contracts/accounts/authentication.md +++ b/docs/builder/smart-contracts/accounts/authentication.md @@ -8,7 +8,7 @@ description: "Authentication component pattern and nonce management for Miden ac Miden uses digital signatures for transaction authentication. Because transactions execute on the client rather than onchain validators, the system needs a way to prove that a transaction was authorized by the account owner. Without authentication, anyone could construct a valid proof that transfers assets out of an account. The nonce prevents replay attacks — without it, a valid proof could be resubmitted to execute the same state change twice. For details on the cryptographic primitives, see [Cryptography](./cryptography). -v0.15 uses a single scheme-agnostic [`AuthSingleSig`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthSingleSig.html) component for single-signature accounts. It takes an auth scheme identifier such as `Falcon512Poseidon2` or `EcdsaK256Keccak`. The native hash function is Poseidon2, and the Falcon-512 verifier MASM module is `miden::core::crypto::dsa::falcon512_poseidon2`. +The scheme-agnostic [`AuthSingleSig`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthSingleSig.html) component handles single-signature accounts. It takes an `Approver`, which pairs a public-key commitment with an authentication scheme such as `Falcon512Poseidon2` or `EcdsaK256Keccak`. The native hash function is Poseidon2, and the Falcon-512 verifier MASM module is `miden::core::crypto::dsa::falcon512_poseidon2`. ## How authentication works @@ -19,22 +19,23 @@ The standards `AuthSingleSig` component stores two items under well-known names: | Public key | `miden::standards::auth::singlesig::pub_key` | Commitment to the account owner's public key | | Scheme ID | `miden::standards::auth::singlesig::scheme` | Which signature scheme to use (1 = ECDSA K256 Keccak, 2 = Falcon-512 Poseidon2) | -During transaction execution the kernel invokes the `@auth_script`-annotated procedure on the account. For `AuthSingleSig`, that procedure loads both slots and delegates to `miden::standards::auth::signature::authenticate_transaction`, which: +During transaction execution the kernel invokes the `@auth_script`-annotated procedure on the account. For `AuthSingleSig`, that procedure loads both slots and: 1. Increments the account nonce (even if the account state did not change — this is required for replay protection). -2. Computes the transaction summary message: `hash([ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, [0, 0, ref_block_num, final_nonce]])`. -3. Requests the signature from the advice provider and verifies it with the scheme indicated by the stored scheme ID. +2. Pays the transaction fee by creating a public `TX_FEE` note when the chain charges a fee. +3. Computes a transaction summary that binds the account change, input and output notes, reference block commitment, expiration, and user parameters. Because the fee is paid first, the fee note and its vault withdrawal are also covered by the signature. +4. Requests the signature from the advice provider and verifies it with the scheme indicated by the stored scheme ID. If verification fails, proof generation fails and the transaction is rejected before reaching the network. The signature itself isn't passed as a function argument — it's provided through the **advice provider**, a mechanism that supplies auxiliary data to the VM during proof generation. See [Advice Provider](../transactions/advice-provider) for the full API. ## Attaching `AuthSingleSig` to an account -On the client side, attach `AuthSingleSig` via `AccountBuilder::with_auth_component`. `miden-client` re-exports `AuthScheme` as `AuthSchemeId`: +Authentication is an ordinary account component. Attach `AuthSingleSig` with `AccountBuilder::with_component`; the builder recognizes it through its `@auth_script` procedure. `miden-client` re-exports `AuthScheme` as `AuthSchemeId`: ```rust use miden_client::{ account::{AccountBuilder, AccountType, component::BasicWallet}, - auth::{AuthSchemeId, AuthSingleSig}, + auth::{Approver, AuthSchemeId, AuthSingleSig}, }; use miden_protocol::{account::auth::PublicKeyCommitment, Word}; @@ -42,10 +43,10 @@ let public_key = PublicKeyCommitment::from(Word::default()); let account = AccountBuilder::new(seed) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new( + .with_component(AuthSingleSig::new(Approver::new( public_key, AuthSchemeId::Falcon512Poseidon2, - )) + ))) .with_component(BasicWallet) .build()?; ``` @@ -54,13 +55,13 @@ If you import directly from `miden-protocol`, the same enum is called `AuthSchem ## Writing a custom auth component -If you need authentication logic beyond `AuthSingleSig` / `AuthMultisig`, you can write a custom auth component in Rust. Mark exactly one procedure per auth component with `#[auth_script]`. If the procedure returns without panicking, the transaction kernel treats authentication as successful. If it panics (for example via `assert!`), authentication fails. +If you need authentication logic beyond `AuthSingleSig` / `AuthMultisig`, you can write a custom auth component in Rust. Mark exactly one procedure per auth component with `#[auth_script]`. Do not combine `#[auth_script]` with `#[account_procedure]`. If the procedure returns without panicking, the transaction kernel treats authentication as successful. If it panics (for example via `assert!`), authentication fails. On a fee-charging chain, custom authentication must also pay the transaction fee; the standard authentication components handle this automatically. ```rust #![no_std] #![feature(alloc_error_handler)] -use miden::{auth_script, component, component_storage, Word}; +use miden::{component, component_storage, Word}; #[component_storage] struct AuthComponentStorage; diff --git a/docs/builder/smart-contracts/accounts/components.md b/docs/builder/smart-contracts/accounts/components.md index 01c5b9db..31577767 100644 --- a/docs/builder/smart-contracts/accounts/components.md +++ b/docs/builder/smart-contracts/accounts/components.md @@ -23,7 +23,9 @@ struct CounterContractStorage { #[component] trait CounterContract { + #[account_procedure] fn get_count(&self) -> Felt; + #[account_procedure] fn increment_count(&mut self) -> Felt; } @@ -62,28 +64,32 @@ version = "0.1.0" [lib] kind = "account-component" namespace = "miden:counter-contract/counter-contract@0.1.0" +path = "src/lib.rs" [dependencies] miden-core = "*" miden-protocol = "*" ``` -The namespace interface segment must match the kebab-cased `#[component]` trait name. If this component calls another account or exposes generated WIT to a note script, add that dependency to both `[dependencies]` and `[package.metadata.miden.dependencies]`. +The namespace interface segment must match the kebab-cased `#[component]` trait name. For generated WIT dependencies, see [Cross-Component Calls](../cross-component-calls#2-generated-wit-dependency). ## Storage struct The storage struct defines the component's storage layout: ```rust -use miden::{component_storage, StorageMap, StorageValue, Word}; +use miden::{component_storage, AccountId, Felt, StorageMap, StorageValue, Word}; #[component_storage] struct MyContractStorage { #[storage(description = "owner account identifier")] owner: StorageValue, + #[storage(description = "initialization flag")] + initialized: StorageValue, + #[storage(description = "user balances")] - balances: StorageMap, + balances: StorageMap, } ``` @@ -103,11 +109,11 @@ The `description` is optional and becomes part of the generated metadata. Slot I ## Trait and impl block — methods -Declare public methods on the `#[component]` trait, then implement that trait for the storage struct. +Declare callable methods on the `#[component]` trait, mark each one with `#[account_procedure]`, then implement that trait for the storage struct. Unmarked trait methods are not exported from the account interface. Authentication entrypoints use `#[auth_script]` instead; the two attributes cannot be combined. ### Read methods (`&self`) -Methods that take `&self` are **read-only** — they can query storage and account state but cannot modify anything: +Methods that take `&self` can read component storage but cannot mutate it through `self`: ```rust fn get_balance(&self, depositor: AccountId) -> Felt { @@ -117,7 +123,7 @@ fn get_balance(&self, depositor: AccountId) -> Felt { ### Write methods (`&mut self`) -Methods that take `&mut self` can **modify state** — write to storage, add/remove assets, create notes: +Methods that take `&mut self` can update component storage and call mutating methods on `self`: ```rust fn deposit(&mut self, asset: Asset) { @@ -125,10 +131,6 @@ fn deposit(&mut self, asset: Asset) { } ``` -:::info ZK proof implications -Read methods (`&self`) produce proofs that don't include state transitions. Write methods (`&mut self`) produce proofs that do. The distinction is enforced by the compiler and determines which kernel operations are available. -::: - ### Private methods Helpers that are not declared on the `#[component]` trait are private. Define them on the storage struct with a normal inherent impl, then call them from the component trait implementation: @@ -162,19 +164,13 @@ The `#[component]` macro automatically provides methods on `self` for account op ```rust // Add an asset to the account vault -self.add_asset(asset: Asset) -> Asset +self.add_asset(asset: Asset) -> Word // Remove an asset from the account vault -self.remove_asset(asset: Asset) -> Asset +self.remove_asset(asset: Asset) -> Word // Increment the account nonce (replay protection) -self.incr_nonce() -> Felt - -// Compute commitment of account state changes (read-only) -self.compute_delta_commitment() -> Word - -// Check if a procedure was called during this transaction (read-only) -self.was_procedure_called(proc_root: Word) -> bool +self.incr_nonce() -> Nonce ``` ### Read-only methods (`&self`) @@ -184,13 +180,19 @@ self.was_procedure_called(proc_root: Word) -> bool self.get_id() -> AccountId // Get the account nonce -self.get_nonce() -> Felt +self.get_nonce() -> Nonce + +// Get the value word stored under an asset key +self.get_asset(asset_key: Word) -> Word -// Get fungible asset balance for an asset key -self.get_balance(asset_key: Word) -> Felt +// Check fungible or non-fungible asset ownership +self.has_asset(asset_id: Word) -> bool -// Check non-fungible asset ownership -self.has_non_fungible_asset(asset: Asset) -> bool +// Compute commitment of account state changes +self.compute_delta_commitment() -> Word + +// Check if a procedure was called during this transaction +self.was_procedure_called(proc_root: Word) -> bool // Get storage and vault commitments self.get_vault_root() -> Word diff --git a/docs/builder/smart-contracts/accounts/cryptography.md b/docs/builder/smart-contracts/accounts/cryptography.md index e243a36a..412ec54d 100644 --- a/docs/builder/smart-contracts/accounts/cryptography.md +++ b/docs/builder/smart-contracts/accounts/cryptography.md @@ -13,11 +13,12 @@ The Miden SDK exposes cryptographic primitives for signature verification and ha The core function for signature verification: ```rust -use miden::rpo_falcon512_verify; +use miden::{emit_falcon_sig_to_stack, rpo_falcon512_verify}; // Verify a Falcon512 signature // pk: Poseidon2 hash of the public key // msg: Poseidon2 hash of the message +emit_falcon_sig_to_stack(msg, pk); rpo_falcon512_verify(pk, msg); ``` @@ -29,7 +30,7 @@ rpo_falcon512_verify(pk, msg); The function panics (proof generation fails) if the signature is invalid. :::info Where's the signature? -The actual signature data is loaded onto the advice stack by the host. The Rust helper is still named `rpo_falcon512_verify` for compatibility, but the v0.15 verifier uses Falcon-512 over Poseidon2. You don't pass the signature as an argument. +`emit_falcon_sig_to_stack` requests the signature from the host, which loads it onto the advice stack. The Rust verifier is still named `rpo_falcon512_verify` for compatibility, but it uses Falcon-512 over Poseidon2. You don't pass the signature directly to the verifier. ::: ## Hashing diff --git a/docs/builder/smart-contracts/accounts/custom-types.md b/docs/builder/smart-contracts/accounts/custom-types.md index a827fb0e..99c5e41c 100644 --- a/docs/builder/smart-contracts/accounts/custom-types.md +++ b/docs/builder/smart-contracts/accounts/custom-types.md @@ -14,7 +14,7 @@ If you forget `#[export_type]` on a public API type, the compiler will emit an e ## Exporting structs -Struct fields must be public and use types that are either SDK types (`Felt`, `Word`, `Asset`, etc.) or themselves marked with `#[export_type]`: +Struct fields must be named and use types supported by component interfaces, such as Rust primitives, SDK types (`Felt`, `Word`, `Asset`, etc.), or other types marked with `#[export_type]`: ```rust use miden::{component, component_storage, export_type, Asset, Felt, Word}; @@ -36,12 +36,13 @@ struct MyAccountStorage; #[component] trait MyAccount { - fn process(&self, a: StructA, asset: Asset) -> StructB; + #[account_procedure] + fn process(&self, a: StructA) -> StructB; } #[component] impl MyAccount for MyAccountStorage { - fn process(&self, a: StructA, asset: Asset) -> StructB { + fn process(&self, a: StructA) -> StructB { StructB { bar: a.foo[0], baz: a.foo[1], @@ -55,7 +56,7 @@ impl MyAccount for MyAccountStorage { Enums use the same annotation. Enum variants can be unit variants: ```rust -use miden::{export_type, Felt}; +use miden::export_type; #[export_type] pub enum Status { @@ -69,11 +70,13 @@ pub enum Status { Exported types can reference other exported types: ```rust +#[derive(Clone, Copy, Debug)] #[export_type] pub struct Inner { pub value: Felt, } +#[derive(Clone, Copy, Debug)] #[export_type] pub struct Outer { pub nested: Inner, @@ -103,9 +106,9 @@ pub mod my_types { | Rule | Details | |------|---------| | When needed | Any custom type in a public method signature on a `#[component]` trait | -| Struct fields | Must be `pub` | -| Allowed field types | `Felt`, `Word`, `Asset`, `AccountId`, or other `#[export_type]` types | -| Enums | Unit variants supported | +| Structs | Named-field and unit structs are supported; tuple structs are not | +| Allowed field types | Supported primitives, SDK types, `Option`, `Result`, or other `#[export_type]` types | +| Enums | Unit variants and single-field tuple variants are supported | | Modules | Types in submodules work — just apply `#[export_type]` to each | | Order | Declaration order doesn't matter — forward references are resolved | diff --git a/docs/builder/smart-contracts/accounts/introduction.md b/docs/builder/smart-contracts/accounts/introduction.md index 0cacf958..d7983336 100644 --- a/docs/builder/smart-contracts/accounts/introduction.md +++ b/docs/builder/smart-contracts/accounts/introduction.md @@ -1,27 +1,28 @@ --- title: "What are Accounts?" sidebar_position: 0 -description: "Accounts are the primary actors in Miden — they store code, state, and assets, and execute transactions locally with private state." +description: "Accounts are the primary actors in Miden — they store code, state, and assets, and execute provable state transitions." --- # What are Accounts? Accounts are the primary actors in Miden. Every entity on the network — wallets, smart contracts, token faucets — is an account. Unlike traditional blockchains where user wallets and smart contracts are fundamentally different, Miden treats them all as programmable accounts with the same structure. -Each account is an independent state machine that executes transactions locally and generates a zero-knowledge proof of correct execution. This means accounts never share a global execution environment — they run in isolation, which enables parallel execution and privacy by default. +Each account is an independent state machine. Most transactions execute locally on a client, while network accounts can instead be executed and proven by a network transaction builder. In both cases, correct execution is verified through a zero-knowledge proof. Accounts never share a global execution environment — they run in isolation, which enables parallel execution and privacy by default. ## Anatomy of an account -Every account has four parts: +Every account has an immutable identifier and four state elements: | Part | Description | |------|-------------| +| **ID** | An immutable identifier that uniquely identifies the account | | **Code** | One or more [components](./components.md) that define the account's behavior — its public API and internal logic | -| **Storage** | Persistent state — up to 255 typed [slots](./storage.md) of `Value` or `StorageMap` | +| **Storage** | Persistent state — up to 255 typed [slots](./storage.md) of `StorageValue` or `StorageMap` | | **Vault** | The fungible and non-fungible assets the account holds | -| **Nonce** | A counter that increments exactly once per state change, providing replay protection | +| **Nonce** | A counter that increments by one in every state-changing transaction, providing replay protection | -The network doesn't store the full account state. Instead, it stores cryptographic commitments — hashes of the code, storage, and vault (see [account design](/reference/protocol/account/)). Only the account owner (or a public account's observers) sees the actual data. +For public accounts, the network stores the full account state. For private accounts, it stores only a commitment to the account state — computed from the ID, nonce, vault root, storage commitment, and code commitment — while the full state remains offchain and must be maintained by the user (see [account design](/reference/protocol/account/)). ## Components, not contracts @@ -35,6 +36,7 @@ struct MyWalletStorage; #[component] trait MyWallet { + #[account_procedure] fn receive_asset(&mut self, asset: Asset); } @@ -63,8 +65,8 @@ Wallet, contract, and faucet roles are determined by the account's components an | | EVM | Miden | |---|---|---| -| **Execution** | Every validator re-executes every transaction | Account owner executes locally, submits a ZK proof | -| **State visibility** | All state variables are public onchain | State is private by default (only commitments onchain) | +| **Execution** | Every validator re-executes every transaction | A client or network transaction builder executes; the network verifies a ZK proof | +| **State visibility** | All state variables are public onchain | Private accounts expose commitments; public accounts store their full state onchain | | **Code structure** | Monolithic contract deployed to an address | Multiple reusable components composed into one account | | **Identity** | Wallets are EOAs, contracts are separate | Everything is an account — wallets are smart contracts | -| **Failure** | `revert` consumes gas, leaves an onchain trace | Proof cannot be generated — no onchain trace, no cost | +| **Failure** | `revert` consumes gas, leaves an onchain trace | Invalid execution cannot produce a proof, so no failed transaction is submitted onchain | diff --git a/docs/builder/smart-contracts/accounts/network-accounts.md b/docs/builder/smart-contracts/accounts/network-accounts.md index 399c0fc1..c0eae122 100644 --- a/docs/builder/smart-contracts/accounts/network-accounts.md +++ b/docs/builder/smart-contracts/accounts/network-accounts.md @@ -1,7 +1,7 @@ --- title: "Network Accounts" sidebar_position: 7 -description: "What a network account is in Miden v0.15, how to build and deploy one, and how to send network notes to it from Rust and TypeScript." +description: "What a network account is in Miden, including fee policies, deployment, and network notes from Rust and TypeScript." --- # Network Accounts @@ -13,37 +13,41 @@ Two sides have to line up for network execution to happen: - **The account** opts in by carrying the standardized note-allowlist storage slot, added through the [`AuthNetworkAccount`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthNetworkAccount.html) auth component. - **The note** targets the account by carrying a `NetworkAccountTarget` attachment. -If a note's script root is in the account's allowlist, the network consumes it automatically. - -:::info v0.15 change -Before v0.15, network accounts were a storage mode (`AccountStorageMode::Network`). That storage mode was **removed**. An account is now classified only as `AccountType::Public` or `AccountType::Private`, and "network account" is a *property of a public account's storage* — the presence of the allowlist slot — rather than a separate mode. See [Account changes](../../migration/03-account-changes.md). -::: +If a note's script root is allowlisted and its fee can be estimated by the account's active fee policy, the network can consume it automatically. ## What makes an account a network account `AuthNetworkAccount` writes a standardized [`StorageMap`](./storage) slot named `miden::standards::auth::network_account::allowed_note_scripts`. Off-chain services and the node's NTX builder treat the presence of that slot as the signal that an account is a network account. The slot holds a **note allowlist**: the set of note script roots the account is willing to consume. A note whose script root is not in the allowlist is rejected during authentication. -Since **v0.15.2**, the component also holds a second allowlist of permitted **transaction script roots** (`miden::standards::auth::network_account::allowed_tx_scripts`). It is empty by default, and the network auth procedure **rejects any transaction that runs a transaction script whose root is not in this allowlist** — a scriptless transaction has no script and is always accepted. Consuming a note does not need a transaction script, so a note-only network account leaves it empty. But if the account is reached by a *custom transaction script* — for example a scripted deploy, or a scripted interaction — that script's root must be allowlisted too, or the transaction is rejected. (Before v0.15.2 the component banned transaction scripts outright, and its note-allowlist constructor was named `with_allowlist` rather than `with_allowed_notes`.) +The component also holds a second allowlist of permitted **transaction script roots** (`miden::standards::auth::network_account::allowed_tx_scripts`). `AuthNetworkAccount::new` includes the canonical expiration script required by the network transaction builder. Any additional custom transaction script — for example a scripted deploy or interaction — must be allowlisted explicitly. -Both allowlists are **fixed at account creation**. The component deliberately exports no procedure to mutate them, so decide the allowed note scripts before you build the account. +Both allowlists can be updated after deployment through the network-account +configuration note. Those mutations must be protected by an owner- or +RBAC-controlled `Authority`; auth-controlled authority is unsafe because +network authentication is intentionally permissionless for allowlisted inputs. +The account example below installs owner-controlled access for this purpose. ## Prerequisites - The account must be `AccountType::Public`. A private account cannot be a network account. -- The note allowlist must be **non-empty** — `AuthNetworkAccount::with_allowed_notes` returns an error for an empty set, because an account that can consume no notes is useless as a network account. -- You need the **script root of every note type** the account should accept, computed from the compiled note script. +- You need the **script root of every application note type** the account should accept, computed from the compiled note script. Rust's `AuthNetworkAccount::new` accepts an empty application set because it adds the standard configuration and fee-sponsorship scripts. The Web SDK helper requires at least one application `NoteScriptFee`. +- You need a `FeePolicyManager`, the ID of the fungible faucet used for fees, and an active policy that can price every allowed note script. A zero fee is valid but must still be scheduled explicitly by `BasicConstantFeePolicy`. ## Building a network account -Because the allowlists are fixed at creation, compile the note script and read its MAST root **before** building the account. The note-script allowlist is required — its presence is what marks the account as a network account. Compile with the client's code builder: +Compile each application note script and read its MAST root **before** building the account. The standardized allowlist storage installed by `AuthNetworkAccount` is what marks the account as a network account. Compile with the client's code builder: ```rust use std::collections::BTreeSet; use miden_client::account::{ AccountBuilder, AccountType, - component::AuthNetworkAccount, + component::{ + AccessControl, AuthNetworkAccount, BasicConstantFeePolicy, FeePolicyManager, + }, }; +use miden_client::asset::AssetAmount; +use miden_client::note::{FeeSponsorshipNote, NetworkAccountConfigNote}; let note_script = client.code_builder().compile_note_script(note_code)?; let note_script_root = note_script.root(); @@ -51,24 +55,44 @@ let note_script_root = note_script.root(); If the note script calls into the account's own procedures (as the counter example does), link the contract module first so the script compiles — for example `client.code_builder().with_linked_module("external_contract::counter_contract", counter_code)?.compile_note_script(note_code)?`. -Attach `AuthNetworkAccount` as the account's auth component — `miden-client` re-exports it from `miden_client::account::component` — and build a public account: +Build an active fee policy, pass it to `AuthNetworkAccount`, then install every component the auth bundle yields: ```rust -let auth = AuthNetworkAccount::with_allowed_notes(BTreeSet::from([note_script_root]))?; +let fee_policy = BasicConstantFeePolicy::new() + .with_fee(note_script_root, AssetAmount::ZERO) + .with_fee( + NetworkAccountConfigNote::script_root(), + AssetAmount::ZERO, + ) + .with_fee(FeeSponsorshipNote::script_root(), AssetAmount::ZERO); +let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy.into()) + .build(); +let auth = AuthNetworkAccount::new( + BTreeSet::from([note_script_root]), + fee_policy_manager, +)?; let account = AccountBuilder::new(init_seed) - .account_type(AccountType::Public) // network accounts must be public - .with_component(counter_component) // your application logic - .with_auth_component(auth) // AuthNetworkAccount as the auth component + .account_type(AccountType::Public) + .with_component(counter_component) + .with_components(auth) + .with_components(AccessControl::Ownable2Step { owner: owner_id }) .build()?; ``` If the account will be reached by a **custom transaction script** — for example a scripted deploy, or a scripted interaction — you must also allowlist that script's root, or the network auth procedure rejects the transaction: +In that case, replace the earlier `let auth = ...` construction with this one: + ```rust let tx_script = client.code_builder().compile_tx_script(deploy_script_code)?; -let auth = AuthNetworkAccount::with_allowed_notes(BTreeSet::from([note_script_root]))? +let auth = AuthNetworkAccount::new( + BTreeSet::from([note_script_root]), + fee_policy_manager, +)? .with_allowed_tx_scripts(BTreeSet::from([tx_script.root()])); ``` @@ -76,40 +100,35 @@ let auth = AuthNetworkAccount::with_allowed_notes(BTreeSet::from([note_script_ro Building the account and adding it to the client store is **not** enough to register it onchain — an account only exists to the network once a committed transaction has advanced its state (nonce `0` → `1`). Submit a transaction against it to deploy it. -Because `AuthNetworkAccount` bumps the nonce itself, an **empty, scriptless transaction is the simplest way** to register the account — a scriptless transaction needs nothing in the tx-script allowlist. (Deploying with a custom transaction script also works, but then that script's root must be in the tx-script allowlist, as above.) +Because `AuthNetworkAccount` bumps the nonce itself, an **empty, scriptless transaction is the simplest way** to register the account on a zero-fee development chain — a scriptless transaction needs no additional tx-script allowlist entry. Deploying with a custom transaction script also works, but then that script's root must be allowlisted as above. On a fee-charging chain, make sure the account can fund the transaction fee. ```rust use miden_client::transaction::TransactionRequestBuilder; client.add_account(&account, false).await?; -// Scriptless deploy: AuthNetworkAccount bumps the nonce on its own, -// so an empty transaction is enough to register the account onchain. let tx_id = client .submit_new_transaction(account.id(), TransactionRequestBuilder::new().build()?) .await?; -// Sync until the deploy transaction is committed by the node. In tests the -// `wait_for_tx` helper wraps this loop; in application code, sync and check the -// transaction status with `client.get_transactions(...)`. -client.sync_state().await?; +client.sync_state().await?; // repeat until `tx_id` is committed ``` Once the deploy transaction is committed, the network watches the account and will consume any allowlisted note addressed to it. :::note Scriptless vs. scripted deploy -The scriptless deploy above is the minimal path. The [network transactions tutorial](../../tutorials/recipes/rust/network_transactions_tutorial.md) instead deploys with a **custom transaction script** (and therefore allowlists that script's root, as shown above) — that scripted flow is the one verified end-to-end on public testnet. Either works; use the scripted deploy if your contract needs initialization logic to run at deploy time. +The scriptless deploy above is the minimal path. The [network transactions tutorial](../../tutorials/recipes/rust/network_transactions_tutorial.md) instead deploys with a **custom transaction script** and therefore allowlists that script's root, as shown above. Either works; use the scripted deploy if your contract needs initialization logic to run at deploy time. ::: ## Inspecting a network account -`NetworkAccount` is a validation wrapper that confirms an `Account` is public and carries a valid, non-empty allowlist slot. Use it to check an account you fetched or built: +`NetworkAccount` is a validation wrapper that confirms an `Account` is public, carries a valid non-empty note allowlist, and allows the canonical expiration transaction script. Use it to check an account you fetched or built: ```rust use miden_client::account::component::NetworkAccount; -let network_account = NetworkAccount::try_from(account)?; // errors if not public / no allowlist -let allowed = network_account.allowed_notes(); // the note allowlist +let network_account = NetworkAccount::try_from(account)?; +let allowed = network_account.allowed_notes(); ``` ## Sending a note to a network account @@ -134,17 +153,47 @@ const { txId, note } = await client.transactions.createNetworkNote({ Use `buildNetworkNote(...)` if you want the built note without submitting it. -:::note TypeScript cannot create the account -The Web SDK can **send** network notes, but it cannot **create or deploy** a network account — `AuthNetworkAccount`, the allowlist API, and the removed `AccountStorageMode.network()` are not exposed in the Web SDK. Build and deploy the account in Rust; interact with it from either surface. -::: +The Web SDK can also build and deploy the account. Pair every application script with its fee, pass the fee-faucet ID, and install **all** returned components: + +```typescript +import { + AccountBuilder, + AccountComponent, + AccountStorageMode, + NoteScriptFee, + TransactionRequestBuilder, +} from "@miden-sdk/miden-sdk"; + +const networkAuth = AccountComponent.createNetworkAuthComponents( + [new NoteScriptFee(counterNoteScript.root(), 0n)], + feeFaucet.id(), +); + +const builder = new AccountBuilder(seed) + .storageMode(AccountStorageMode.public()) + .withComponent(counterComponent); + +for (const component of networkAuth) { + builder.withComponent(component); +} + +const { account } = builder.build(); +await client.accounts.insert({ account }); +await client.transactions.submit( + account.id(), + new TransactionRequestBuilder().build(), +); +``` + +`createNetworkAuthComponents` returns the auth component plus the components backing its fee policy. Omitting any of them creates an incomplete account. ## Surface support | Flow | Rust | TypeScript | |---|---|---| -| Create + deploy a network account | ✅ `AuthNetworkAccount` + note allowlist | ❌ not exposed | +| Create + deploy a network account | ✅ `AuthNetworkAccount` + fee policy | ✅ `createNetworkAuthComponents` + deployment transaction | | Send a network note to one | ✅ | ✅ `createNetworkNote` / `buildNetworkNote` | -| Inspect (`NetworkAccount`) | ✅ | ❌ | +| Inspect (`NetworkAccount`) | ✅ | ✅ `isNetworkAccount()` / `networkNoteAllowlist()` | :::info API Reference Rust: [`AuthNetworkAccount`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthNetworkAccount.html), [`NetworkAccount`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.NetworkAccount.html), [`NetworkAccountNoteAllowlist`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.NetworkAccountNoteAllowlist.html) @@ -155,5 +204,4 @@ Rust: [`AuthNetworkAccount`](https://docs.rs/miden-standards/latest/miden_standa - [Network transactions tutorial](../../tutorials/recipes/rust/network_transactions_tutorial.md) — end-to-end Rust walkthrough: build, deploy, and drive a network counter contract - [Authentication](./authentication) — the auth component pattern `AuthNetworkAccount` builds on - [Storage](./storage) — how the allowlist `StorageMap` slot is laid out -- [Account changes](../../migration/03-account-changes.md) — the v0.14 → v0.15 removal of `AccountStorageMode::Network` - [Account components](../standards/account-components) — composing wallet, faucet, and access-control components diff --git a/docs/builder/smart-contracts/accounts/storage.md b/docs/builder/smart-contracts/accounts/storage.md index eb261ca6..e9e27e13 100644 --- a/docs/builder/smart-contracts/accounts/storage.md +++ b/docs/builder/smart-contracts/accounts/storage.md @@ -6,7 +6,7 @@ description: "Persistent state management with StorageValue slots and StorageMap # Storage -Miden accounts have persistent storage organized into up to 255 name-addressable slots. Each slot holds either a single typed value (via `StorageValue`) or a key-value map (via `StorageMap`). Slots are identified by `StorageSlotId` values derived from slot names, which in turn are derived from the component package name and the field name. Renaming a field changes the slot ID and is a breaking change for stored data. +Miden accounts have persistent storage organized into up to 255 name-addressable slots. Each slot holds either a single typed value (via `StorageValue`) or a key-value map (via `StorageMap`). Slots are identified by `StorageSlotId` values derived from the component package name, component interface, and field name. Renaming any of these changes the slot ID and is a breaking change for stored data. ## Storage slots @@ -24,7 +24,7 @@ struct MyContractStorage { balances: StorageMap, } ``` -Slot IDs are derived from the component package name and the field name. Ordering does not matter, and `slot(N)` is not supported. +Field ordering does not matter, and `slot(N)` is not supported. ## StorageValue — Single-slot storage @@ -91,7 +91,7 @@ pub fn get_balance(&self, account_id: AccountId) -> Felt { } ``` -Scalar `Felt` map values are encoded in the low word limb (`[value, 0, 0, 0]`) in v0.15. This is handled by the typed `StorageMap` conversion. For a full `Word` value, declare the map value type as `Word`: +Scalar `Felt` map values are encoded in the low word limb (`[value, 0, 0, 0]`). This is handled by the typed `StorageMap` conversion. For a full `Word` value, declare the map value type as `Word`: ```rust // Get the full Word value @@ -174,7 +174,7 @@ let initial: Word = storage::get_initial_item(slot_id); let initial: Word = storage::get_initial_map_item(slot_id, &key); ``` -These functions return values from before any modifications in the current transaction. +`get_item` and `get_map_item` read the current values. `get_initial_item` and `get_initial_map_item` read the values from the start of the transaction, while `set_item` and `set_map_item` return the values immediately before the write. For Felt and Word conversion details, see [Types](../types). To export your own types for public APIs, see [Custom Types](./custom-types). For common storage patterns like access control and rate limiting, see [Patterns](../patterns). diff --git a/docs/builder/smart-contracts/cross-component-calls.md b/docs/builder/smart-contracts/cross-component-calls.md index 1a49e8be..4e4efaf8 100644 --- a/docs/builder/smart-contracts/cross-component-calls.md +++ b/docs/builder/smart-contracts/cross-component-calls.md @@ -6,15 +6,18 @@ description: "Call methods across account components and from note scripts." # Cross-Component Calls -Miden [components](./accounts/components) can call each other's methods. Since accounts can have multiple components (e.g., wallet + auth + custom logic), those components need to communicate. [Note scripts](./notes/note-scripts) also need to call methods on the account's components to transfer assets. +Miden [components](./accounts/components) can call each other's methods. Since accounts can have multiple components (e.g., wallet + auth + custom logic), those components need to communicate. [Note scripts](./notes/note-scripts) can also call methods on the account's components. ## How it works -When you build a component with `miden build`, the compiler generates an interface describing its public methods. Other projects can import this interface to call those methods. +When you build a component with `miden build`, the compiler writes its compiled +package and generates WIT describing the methods marked with +`#[account_procedure]`. Other projects use the package plus that WIT to call +those methods. ``` -counter-contract (component) - → generates interface +counter-account (package) + → exports counter-contract (component interface) → counter-note imports the interface → calls account.get_count() ``` @@ -24,29 +27,26 @@ counter-contract (component) The simplest way to make cross-component calls from note scripts is through the `#[note]` macro with an `Account` parameter: ```rust -use miden::{account, active_note, note, Word}; +use miden::{account, note, Word}; -#[account(basic_wallet::BasicWallet)] -pub struct Wallet; +#[account(counter_account::CounterContract)] +pub struct CounterAccount; #[note] -struct P2idNote; +struct CounterNote; #[note] -impl P2idNote { +impl CounterNote { #[note_script] - pub fn run(self, _arg: Word, account: &mut Wallet) { - // Iterate over the note's assets and transfer each to the account - for asset in active_note::get_assets() { - account.receive_asset(asset); - } + pub fn run(self, _arg: Word, account: &mut CounterAccount) { + account.increment_count(); } } ``` -The `_arg: Word` parameter is the note's first input Word, passed automatically when the note is consumed. It's unused in this example (prefixed with `_`), but note scripts can use it for recipient-specific data like expected account IDs or amounts. +The `_arg: Word` parameter contains the note argument (`NOTE_ARGS`) supplied by the transaction when the note is consumed. It's unused in this example (prefixed with `_`), but note scripts can use it for transaction-specific data like expected account IDs or amounts. -The `#[account(...)]` wrapper declares which generated WIT interface the script will call. Its methods correspond to the referenced component's public methods. +The `#[account(...)]` wrapper declares which package interface the script will call. Its methods correspond to the referenced component's account procedures. ## Calling foreign accounts @@ -65,7 +65,7 @@ fn read_foreign_count(counter_account_id: AccountId) -> Felt { ``` Key points: -- The `#[account(package::Interface)]` path names the exported WIT interface, not just the package. +- The `#[account(package::Interface)]` path names the interface exported by the dependency package and described by its generated WIT, not just the package. - An account parameter in a note or transaction script refers to the transaction's native account. - `AccountWrapper::new(account_id)` creates a foreign account caller routed through FPI. @@ -102,7 +102,8 @@ version = "0.1.0" [lib] kind = "note" -namespace = "miden:counter-note/counter-note@0.1.0" +namespace = "miden:counter-note/miden-counter-note@0.1.0" +path = "src/lib.rs" [dependencies] miden-core = "*" @@ -114,7 +115,8 @@ counter-account = { wit = "../counter-account/target/generated-wit/" } ``` :::info Build order matters -The dependent component must be built first so its interface files exist. Build `counter-contract` before building `counter-note`. +Build `counter-account` before `counter-note` so both its compiled package and +`target/generated-wit/` interface exist when the consumer is compiled. ::: ## Example: Counter note calling counter contract diff --git a/docs/builder/smart-contracts/index.md b/docs/builder/smart-contracts/index.md index 6167899b..3ae40897 100644 --- a/docs/builder/smart-contracts/index.md +++ b/docs/builder/smart-contracts/index.md @@ -8,8 +8,6 @@ pagination_prev: null This section covers the developer-facing paths for building smart contracts on Miden: an authoring guide for **Miden Assembly (MASM)** (the supported path for mainnet production today) and **Rust** (in active development as the long-term direction), plus the [Miden Standards](./standards/) library of reusable components callable from either. -These pages track protocol v0.15.3, Miden VM / Assembly v0.23, miden-crypto v0.25, and the Rust smart-contract SDK macro surface from Miden SDK v0.13. - :::tip Building for mainnet? Miden mainnet supports smart contracts authored in **Miden Assembly (MASM)** today. The Rust SDK is in active development and will become the default authoring path once it ships v1. For production deployments now, see [MASM Smart Contracts](./masm/). ::: diff --git a/docs/builder/smart-contracts/masm/index.md b/docs/builder/smart-contracts/masm/index.md index f241f973..f2409bd3 100644 --- a/docs/builder/smart-contracts/masm/index.md +++ b/docs/builder/smart-contracts/masm/index.md @@ -8,8 +8,6 @@ sidebar_position: 0 This section is the practical guide to authoring Miden smart contracts directly in **Miden Assembly (MASM)** — the path Miden mainnet supports for production deployments today. The Rust SDK is in active development and will become the default authoring path once it ships v1; until then, MASM is what you ship with. -The examples in this section assume protocol v0.15.3 and Miden Assembly v0.23. - :::info Audience You're here because you want to deploy a contract to Miden mainnet. MASM is a small, stack-based assembly language — closer to assembly than Rust or Solidity, but it gives you direct, predictable control over the VM and is what the mainnet kernel verifies. The [Reference → Miden VM → Assembly](/reference/miden-vm/user_docs/assembly/) section is the full language reference; this section is the Builder-side cookbook for using it. ::: diff --git a/docs/builder/smart-contracts/notes/introduction.md b/docs/builder/smart-contracts/notes/introduction.md index b5c11329..2fab8aa5 100644 --- a/docs/builder/smart-contracts/notes/introduction.md +++ b/docs/builder/smart-contracts/notes/introduction.md @@ -6,51 +6,51 @@ description: "Miden's cross-account communication mechanism — programmable UTX # What are Notes? -Notes are Miden's primary mechanism for cross-account communication — they carry assets, execute programmable logic, and trigger state changes on the consuming account. Like UTXOs, notes are created and consumed atomically. Unlike Bitcoin's UTXOs, each Miden note carries an arbitrary executable script — written in Rust — that runs when the note is consumed, enabling programmable conditions far beyond simple locking scripts. +Notes are Miden's primary mechanism for cross-account communication — they carry assets, execute programmable logic, and trigger state changes on the consuming account. Like UTXOs, notes are created and consumed atomically. Unlike Bitcoin's UTXOs, each Miden note carries an arbitrary executable script that runs when the note is consumed, enabling programmable conditions far beyond simple locking scripts. While asset transfers are the most common use of notes, notes are how accounts communicate with one another in general: a note can trigger a counter increment, initiate a swap, delegate an operation, or carry arbitrary data to be acted on by the recipient's logic. -Assets never transfer directly between accounts. Instead, they always move through notes. This indirection is what makes Miden private: the network sees notes being created and consumed, but it can't link sender and recipient accounts because those operations happen in separate transactions. +Assets never transfer directly between accounts. Instead, they always move through notes. With private notes, creation and consumption can be unlinkable to observers because the full note details are not published. Public notes expose those details. ## Anatomy of a note -Every note has four parts: +Every note has four main parts: | Part | Description | |------|-------------| -| **Assets** | The fungible or non-fungible tokens the note carries | -| **Script** | Code that executes when the note is consumed — determines who can claim it and what side effects occur | -| **Storage** | Custom data stored with the note that the script can read at consumption time (e.g., a target account ID, an expiration block) | -| **Metadata** | Sender ID, note tag (for discovery routing), and auxiliary data | +| **Assets** | The fungible or non-fungible tokens the note carries. | +| **Recipient** | The serial number, script, and storage that define the conditions under which the note can be consumed. | +| **Metadata** | Sender ID, note type, note tag, and attachment headers and commitment. Metadata is always public. | +| **Attachments** | Optional public auxiliary data associated with the note. | -The **recipient** is a Poseidon2 hash that encodes who can consume the note. When creating notes programmatically (via [`output_note::create`](./output-notes#create-a-note)), you compute a `Recipient` from the note's serial number, script root, and storage commitment: +The **recipient** is not necessarily an account address. It is a Poseidon2 commitment to the note's serial number, script, and storage, which together define the conditions under which the note can be consumed: ``` recipient = hash(hash(hash(serial_num, [0;4]), script_root), storage_commitment) ``` -Only someone who knows these values can construct a valid consumption proof. See [Computing a Recipient](./output-notes#computing-a-recipient) for the protocol helpers. +To consume the note, a transaction must provide the values that open this commitment and execute the script successfully. See [Computing a Recipient](./output-notes#computing-a-recipient) for the protocol helpers. ## The two-transaction model Unlike Ethereum where a transfer is a single atomic call, Miden transfers happen across two separate transactions: ``` -Transaction 1 (Sender) Transaction 2 (Recipient) +Transaction 1 (Sender) Transaction 2 (Consumer) ┌─────────────────────────┐ ┌─────────────────────────┐ │ 1. Create note │ │ 1. Discover note │ │ 2. Attach assets │ │ 2. Consume note │ │ 3. Note published │──────────▶│ 3. Script runs │ -│ (onchain or private) │ │ 4. Assets move to vault │ +│ (details/commitment) │ │ 4. Assets move to vault │ │ │ │ 5. Note nullified │ └─────────────────────────┘ └─────────────────────────┘ ``` -**Transaction 1**: The sender's account creates an output note, attaches assets to it, and the note is published (either onchain or kept private). +**Transaction 1**: The sender's account creates an output note, attaches assets to it, and publishes the note's public representation. -**Transaction 2**: The recipient discovers the note, consumes it in their own transaction, the note script runs and verifies the consumer is authorized, and assets transfer into the recipient's vault. A **nullifier** is recorded to prevent the same note from being consumed again (see [note design](/reference/protocol/note)). +**Transaction 2**: A consuming account discovers the note and consumes it in its own transaction. The note script runs, its conditions must succeed, and any assets handled by the script can be added to the consumer's vault. A **nullifier** is recorded to prevent the same note from being consumed again (see [note design](/reference/protocol/note)). -This separation is what enables privacy and parallelism — the two transactions are independent and unlinkable from the network's perspective. +This separation lets notes be processed independently and allows private-note creation and consumption to remain unlinkable to observers who do not know the note details. ## Public vs. private notes @@ -58,10 +58,8 @@ Notes come in two visibility modes: | Mode | Description | |------|-------------| -| **Public** | The note's full data (assets, script, storage) is stored by the Miden network and visible onchain. Anyone can discover and attempt to consume it. | -| **Private** | Only a commitment (hash) is stored onchain. The actual note data must be communicated offchain between sender and recipient. | - -Private notes provide stronger privacy guarantees — the network can't even see what assets a note carries — but they require the sender and recipient to have a communication channel outside the protocol. +| **Public** | Metadata, attachments, and full note details (assets, serial number, script, and storage) are published. Anyone can discover and attempt to consume the note. | +| **Private** | Metadata and attachments remain public, but only a commitment to the note details is published. The consumer must obtain the full details separately, for example through a private channel or an encrypted public attachment. | Miden provides built-in note patterns (P2ID, P2IDE, SWAP) for common transfer scenarios — see [Standard Note Types](./note-types). You can also write fully custom note scripts for arbitrary consumption logic. @@ -70,7 +68,7 @@ Miden provides built-in note patterns (P2ID, P2IDE, SWAP) for common transfer sc | | EVM | Miden | |---|---|---| | **Transfer model** | Single `transfer()` call on a token contract | Two transactions: create note, then consume note | -| **Privacy** | Sender, recipient, and amount are public | Transactions are unlinkable; private notes hide all data | +| **Privacy** | Sender, recipient, and amount are public | Private notes can hide their details and unlink creation from consumption; metadata and attachments remain public | | **Programmability** | Token contracts control transfer logic | Each note carries its own script with custom conditions | | **Failure** | Revert onchain, gas consumed | Proof can't be generated — no onchain trace | -| **Parallelism** | Transfers contend for contract state | Notes are independent — unlimited parallel creation | +| **Parallelism** | Transfers contend for contract state | Notes can be created and consumed independently | diff --git a/docs/builder/smart-contracts/notes/note-scripts.md b/docs/builder/smart-contracts/notes/note-scripts.md index 159dc862..5f602b0a 100644 --- a/docs/builder/smart-contracts/notes/note-scripts.md +++ b/docs/builder/smart-contracts/notes/note-scripts.md @@ -13,7 +13,7 @@ Note scripts define the logic that executes when a note is consumed. They determ A note script consists of a struct (holding note storage fields) and an impl block with a `#[note_script]` method: ```rust -use miden::{account, AccountId, Word, active_note, note}; +use miden::{account, note, AccountId, Word}; #[account(basic_wallet::BasicWallet)] pub struct Wallet; @@ -33,8 +33,9 @@ impl MyNote { ``` The `#[note]` macro: -1. Deserializes note storage into struct fields -2. Exports the `run` function as the note's entry point + +1. Deserializes note storage into the struct fields. +2. Exports the method marked with `#[note_script]` as the note entry point. ## Struct fields as note storage @@ -49,7 +50,7 @@ struct MyNote { The compiler maps struct fields to note storage values based on their order and type. Supported field types include `AccountId`, `Felt`, `Word`, and other SDK types. -If you don't need inputs, use a unit struct: +If the note has no storage fields, use a unit struct: ```rust #[note] @@ -82,26 +83,25 @@ pub fn run(self, account: &mut Wallet, _arg: Word) { ... } When you include `&mut Wallet` (or `&Wallet`), the note script can call methods on the account's components: ```rust +#[account(counter_account::CounterContract)] +pub struct CounterAccount; + #[note_script] -pub fn run(self, _arg: Word, account: &mut Wallet) { - let assets = active_note::get_assets(); - for asset in assets { - account.receive_asset(asset); // Cross-component call - } +pub fn run(self, _arg: Word, account: &mut CounterAccount) { + account.increment_count(); } ``` -Declare the account wrapper with `#[account(package::Interface)]` and point `miden-project.toml` at the dependency's generated WIT — see [Cross-Component Calls](../cross-component-calls). +Declare the account wrapper with `#[account(package::Interface)]` and configure its package and generated-WIT dependencies in `miden-project.toml` — see [Cross-Component Calls](../cross-component-calls). ### Without account access -Use this pattern for **trigger or command notes** that carry no assets and only execute logic. If your note transfers assets or calls account methods, include the relevant `&mut AccountWrapper`. +Use this pattern when the script does not call account-component methods. Add `&AccountWrapper` for read-only calls or `&mut AccountWrapper` for state-changing calls. For asset-moving scripts, see [Reading Notes](./reading-notes#assets). ```rust #[note_script] pub fn run(self, _arg: Word) { - // For logic-only notes that carry no assets. - // Cannot call account methods — see the counter note example below. + // Logic that does not require account-component methods. } ``` @@ -148,7 +148,8 @@ version = "0.1.0" [lib] kind = "note" -namespace = "miden:counter-note/counter-note@0.1.0" +namespace = "miden:counter-note/miden-counter-note@0.1.0" +path = "src/lib.rs" [dependencies] miden-core = "*" diff --git a/docs/builder/smart-contracts/notes/note-types.md b/docs/builder/smart-contracts/notes/note-types.md index ceb06199..f19a98ff 100644 --- a/docs/builder/smart-contracts/notes/note-types.md +++ b/docs/builder/smart-contracts/notes/note-types.md @@ -17,7 +17,7 @@ The most common pattern — a note that can only be consumed by a specific accou Use P2ID for standard asset transfers where only the intended recipient should be able to consume the note. This is the most common note type. :::info -P2ID notes use `P2idNote::create` from the `miden-standards` crate (`miden_standards::note::P2idNote`). The script is pre-compiled MASM — use the builder API to create P2ID notes in client code. +P2ID notes use the typed `P2idNote` builder from `miden_standards::note`. The script is pre-compiled MASM; build the typed note and convert it into a protocol `Note` with `.into()`. ::: ### How it works @@ -35,16 +35,17 @@ P2ID notes use `P2idNote::create` from the `miden-standards` crate (`miden_stand ### Builder API ```rust +use miden_protocol::note::Note; use miden_standards::note::P2idNote; -P2idNote::create( - sender, // AccountId: who sends the note - target, // AccountId: the only account that can consume this note - assets, // Vec: assets to attach - note_type, // NoteType: Public or Private - attachments, // NoteAttachments: auxiliary data - rng, // &mut impl FeltRng -) -> Result +let note: Note = P2idNote::builder() + .sender(sender) + .target(target) + .assets(assets) + .note_type(note_type) + .generate_serial_number(rng) + .build()? + .into(); ``` | Parameter | Type | Description | @@ -53,62 +54,68 @@ P2idNote::create( | `target` | `AccountId` | The only account that can consume this note | | `assets` | `Vec` | Assets to attach to the note | | `note_type` | `NoteType` | `Public` or `Private` | -| `attachments` | `NoteAttachments` | Auxiliary data for the note | -| `rng` | `&mut impl FeltRng` | Random number generator | +| `attachment` / `attachments` | `NoteAttachment` / iterator | Optional auxiliary data | +| `generate_serial_number` | `&mut impl FeltRng` | Generates the required serial number | ## P2IDE (Pay to ID with Expiration) -P2IDE extends P2ID with a timelock and a reclaim window. The note can't be consumed before `timelock_height`, and if the target hasn't consumed it by `reclaim_height`, the creator can reclaim the assets. +P2IDE extends P2ID with optional timelock and reclaim conditions. A configured timelock prevents any account from consuming the note before the specified height. If reclaim is enabled, the configured reclaimer can also consume the note once `reclaim_height` has been reached and any configured timelock has expired; the target remains authorized. ### When to use Use P2IDE when the sender wants the option to reclaim assets if the recipient doesn't consume the note within a time window. :::info -P2IDE notes use `P2ideNote::create` from the `miden-standards` crate (`miden_standards::note::P2ideNote`). The script is pre-compiled MASM — use the builder API to create P2IDE notes in client code. +P2IDE notes use the typed `P2ideNote` builder from `miden_standards::note`. Its reclaimer and block-height constraints are optional builder fields. ::: ### How it works -1. Creator creates a P2IDE note with the target account ID, a timelock height, and a reclaim height as note storage items -2. **Target consumes after `timelock_height`** — assets transfer to the target account -3. **Creator reclaims after `reclaim_height`** — assets return to the creator -4. **Before timelock or between timelock and reclaim by a non-target** — any consumption attempt fails (proof generation fails) +1. The sender creates a P2IDE note with the target account ID and optional timelock, reclaim height, and reclaimer +2. If a timelock is configured, no account can consume the note before it expires +3. The target can consume the note once the timelock condition is satisfied +4. If reclaim is enabled, the reclaimer can also consume the note once `reclaim_height` has been reached and any configured timelock has expired; the sender is the default reclaimer +5. All other consumption attempts fail (proof generation fails) ### Note storage | Item | Type | Description | |------|------|-------------| -| `target_account_id_prefix` | `Felt` | Target account ID prefix | -| `target_account_id_suffix` | `Felt` | Target account ID suffix | -| `reclaim_height` | `Felt` | Block height after which the creator can reclaim | -| `timelock_height` | `Felt` | Block height before which the note can't be consumed | +| `reclaimer` | `AccountId` | Account allowed to reclaim; defaults to the sender | +| `target` | `AccountId` | Account allowed to receive the note | +| `reclaim_height` | `Option` | Block height after which the reclaimer can consume the note, subject to the timelock | +| `timelock_height` | `Option` | Block height before which no account can consume the note | ### Builder API ```rust -use miden_standards::note::{P2ideNote, P2ideNoteStorage}; - -P2ideNote::create( - sender, // AccountId: who sends the note - P2ideNoteStorage::new(target, reclaim_height, timelock_height), - assets, // Vec: assets to attach - note_type, // NoteType: Public or Private - attachments, // NoteAttachments: auxiliary data - rng, // &mut impl FeltRng -) -> Result +use miden_protocol::note::Note; +use miden_standards::note::P2ideNote; + +let note: Note = P2ideNote::builder() + .sender(sender) + .target(target) + .reclaimer(reclaimer) + .reclaim_height(reclaim_height) + .timelock_height(timelock_height) + .assets(assets) + .note_type(note_type) + .generate_serial_number(rng) + .build()? + .into(); ``` | Parameter | Type | Description | |-----------|------|-------------| | `sender` | `AccountId` | Account sending the note | -| `target` | `AccountId` | The only account that can consume this note (set on `P2ideNoteStorage`) | -| `reclaim_height` | `Option` | Block height after which sender can reclaim; `None` = no reclaim (set on `P2ideNoteStorage`) | -| `timelock_height` | `Option` | Block height before which note can't be consumed; `None` = no timelock (set on `P2ideNoteStorage`) | +| `target` | `AccountId` | The account that can receive the note | +| `reclaimer` | `AccountId` | Optional reclaiming account; defaults to `sender` | +| `reclaim_height` | `BlockNumber` | Optional block height after which the reclaimer can consume the note, subject to the timelock | +| `timelock_height` | `BlockNumber` | Optional block height before which no account can consume the note | | `assets` | `Vec` | Assets to attach to the note | | `note_type` | `NoteType` | `Public` or `Private` | -| `attachments` | `NoteAttachments` | Auxiliary data for the note | -| `rng` | `&mut impl FeltRng` | Random number generator | +| `attachment` / `attachments` | `NoteAttachment` / iterator | Optional auxiliary data | +| `generate_serial_number` | `&mut impl FeltRng` | Generates the required serial number | ## SWAP (Atomic Exchange) @@ -119,30 +126,33 @@ SWAP enables atomic asset exchange. The creator offers one asset; any consumer w Use SWAP for trustless atomic exchanges where two parties trade assets without intermediaries. :::info -SWAP notes use `SwapNote::create` from the `miden-standards` crate (`miden_standards::note::SwapNote`). The script is pre-compiled MASM — use the builder API to create SWAP notes in client code. +SWAP notes use the typed `SwapNote` builder from `miden_standards::note`. Read the expected payback details from the typed value before converting it into a protocol `Note`. ::: ### How it works -1. Creator creates a SWAP note containing the offered asset and metadata describing the requested asset + payback recipient -2. Consumer's transaction processes the note — the script creates a P2ID payback note targeted at the original creator containing the requested asset -3. Consumer receives the offered asset into their vault -4. Both transfers happen atomically in one transaction +1. The creator creates a SWAP note containing the offered asset and storage describing the requested asset and payback configuration +2. The consumer's transaction moves the requested asset from their vault into a P2ID payback note targeted at the creator +3. The transaction moves the offered asset from the SWAP note into the consumer's vault +4. The payback note creation and offered asset transfer happen atomically in the same transaction ### Builder API ```rust +use miden_protocol::note::Note; use miden_standards::note::SwapNote; -SwapNote::create( - sender, - offered_asset, - requested_asset, - swap_note_type, - swap_note_attachments, - payback_note_type, - rng, -) -> Result<(Note, NoteDetails), NoteError> +let swap = SwapNote::builder() + .sender(sender) + .offered_asset(offered_asset) + .requested_asset(requested_asset) + .note_type(swap_note_type) + .payback_note_type(payback_note_type) + .generate_serial_number(rng) + .build()?; + +let payback_note_details = swap.payback_note_details(); +let note: Note = swap.into(); ``` | Parameter | Type | Description | @@ -151,14 +161,14 @@ SwapNote::create( | `offered_asset` | `Asset` | Asset the note carries (what the consumer receives) | | `requested_asset` | `Asset` | Asset the consumer must provide in return | | `swap_note_type` | `NoteType` | `Public` or `Private` for the SWAP note | -| `swap_note_attachments` | `NoteAttachments` | Auxiliary data for the SWAP note | +| `attachment` / `attachments` | `NoteAttachment` / iterator | Optional auxiliary data for the SWAP note | | `payback_note_type` | `NoteType` | `Public` or `Private` for the P2ID payback note | -| `rng` | `&mut impl FeltRng` | Random number generator | +| `generate_serial_number` | `&mut impl FeltRng` | Generates the required serial number | -Returns a tuple of `(Note, NoteDetails)` — the SWAP note to submit and the expected payback note details (for tracking). +The builder returns a typed `SwapNote`. Call `payback_note_details()` before converting it into the `Note` to submit. -`NoteAttachments` is defined in `miden-protocol`. Use `NoteAttachments::empty()` when the note does not need auxiliary data — see [note attachments](./output-notes#note-attachments) for the underlying SDK API. +Attachments are optional. Use `.attachment(value)` or `.attachments(values)` only when needed; see [note attachments](./output-notes#note-attachments) for the underlying SDK API. ## More note types -For writing custom note scripts, see [Note Scripts](./note-scripts). For the transaction context and `#[tx_script]`, see [Transaction Context](../transactions/transaction-context). +For PSWAP, MINT, BURN, and other standard notes, see [Standard Notes](../standards/standard-notes). For writing custom note scripts, see [Note Scripts](./note-scripts). For the transaction context and `#[tx_script]`, see [Transaction Context](../transactions/transaction-context). diff --git a/docs/builder/smart-contracts/notes/output-notes.md b/docs/builder/smart-contracts/notes/output-notes.md index 36f40112..b3a68096 100644 --- a/docs/builder/smart-contracts/notes/output-notes.md +++ b/docs/builder/smart-contracts/notes/output-notes.md @@ -6,7 +6,7 @@ description: "Create output notes, attach assets, add attachments, and compute r # Output Notes -The `output_note` module creates notes from inside account component code and transaction scripts. Use it to send assets to other accounts by creating notes that carry assets and a recipient hash. +The `output_note` module creates and updates notes during a transaction. ```rust use miden::{output_note, Asset, NoteIdx, Tag, NoteType, Recipient}; @@ -18,10 +18,7 @@ use miden::{output_note, Asset, NoteIdx, Tag, NoteType, Recipient}; let note_idx: NoteIdx = output_note::create(tag, note_type, recipient); ``` - -To construct a tag targeting a specific account, use `NoteTag::with_account_target(account_id)` from `miden_protocol::note`. - -Returns a `NoteIdx` used to reference this note in subsequent operations within the same transaction. +`create` returns a `NoteIdx` used by subsequent operations in the same transaction. It may only be called from an account component procedure. Transaction and note scripts must call an account procedure such as `BasicWallet::create_note` and use the returned index. ## Add assets to a note @@ -29,6 +26,8 @@ Returns a `NoteIdx` used to reference this note in subsequent operations within output_note::add_asset(asset, note_idx); ``` +`add_asset` only adds the asset to the output note; it does not remove it from the native account's vault. When funding a note from that vault, remove the asset first or use `BasicWallet::move_asset_to_note`. + Call `add_asset` multiple times with the same `note_idx` to attach several assets to one note. A note can carry both fungible and non-fungible assets. ## Query output note state @@ -44,17 +43,17 @@ let assets: Vec = output_note::get_assets(note_idx); let recipient: Recipient = output_note::get_recipient(note_idx); ``` -`OutputNoteAssetsInfo` contains `commitment: Word` and `num_assets: Felt`. +`OutputNoteAssetsInfo` contains `commitment: Word` and `num_assets: u32`. ### Note metadata -Returns note metadata: +`get_metadata()` returns the encoded metadata header: ```rust let metadata: NoteMetadata = output_note::get_metadata(note_idx); ``` -On the v0.15 protocol side, `NoteMetadata` combines `PartialNoteMetadata` (sender, note type, tag) with attachment headers and the attachments commitment. See [Reading Notes — Note metadata](./reading-notes#note-metadata) for details. +The SDK's `NoteMetadata` contains a single `header: Word`. Attachment content and its commitment are queried separately. On the protocol side, the full `NoteMetadata` combines `PartialNoteMetadata` (sender, note type, tag) with attachment headers and the attachments commitment. See [Reading Notes — Note metadata](./reading-notes#note-metadata) for details. ## Note attachments @@ -65,12 +64,11 @@ Notes can carry auxiliary data as attachments. The attachment API uses a `Felt`- output_note::add_word_attachment(note_idx, attachment_scheme, word_data); ``` - Use `add_attachment` when you already have an attachment commitment and the raw data is present in the advice map. Use `add_attachment_from_memory` for multi-word data that should be hashed and inserted from memory. Attachments are committed into note metadata, and the consumer must have access to the corresponding advice map entries to read the full data. ## Computing a Recipient -When creating notes programmatically, you need a `Recipient` to pass to `output_note::create`. The `Recipient` is a hash that encodes the note script and storage commitment, ensuring only someone who knows these values can consume the note. +When creating notes programmatically, an account component needs a `Recipient` to pass to `output_note::create`. The `Recipient` is a commitment to the note's serial number, script, and storage, which together define the conditions under which the note can be consumed. The protocol computation is: @@ -85,14 +83,34 @@ recipient = hash(hash(hash(serial_num, [0;4]), script_root), storage_commitment) A complete flow for creating a note inside an account component: ```rust -use miden::{output_note, Asset, NoteType, Recipient, Tag}; +use miden::{ + component, component_storage, felt, native_account, output_note, Asset, NoteIdx, NoteType, + Recipient, Tag, +}; + +#[component_storage] +struct NoteSenderStorage; + +#[component] +trait NoteSender { + #[account_procedure] + fn send_asset(&mut self, recipient: Recipient, asset: Asset, tag: Tag) -> NoteIdx; +} + +#[component] +impl NoteSender for NoteSenderStorage { + fn send_asset(&mut self, recipient: Recipient, asset: Asset, tag: Tag) -> NoteIdx { + // 1. Create the note + let note_idx = output_note::create(tag, NoteType::from(felt!(1)), recipient); + + // 2. Remove the asset from the native account's vault + let _ = native_account::remove_asset(asset); -pub fn send_assets(recipient: Recipient, asset: Asset, tag: Tag) { - // 1. Create the note - let note_idx = output_note::create(tag, NoteType::Public, recipient); + // 3. Add the asset to the note + output_note::add_asset(asset, note_idx); - // 2. Attach assets - output_note::add_asset(asset, note_idx); + note_idx + } } ``` diff --git a/docs/builder/smart-contracts/notes/reading-notes.md b/docs/builder/smart-contracts/notes/reading-notes.md index 657fbda7..5b93fbe8 100644 --- a/docs/builder/smart-contracts/notes/reading-notes.md +++ b/docs/builder/smart-contracts/notes/reading-notes.md @@ -13,7 +13,7 @@ Miden provides two modules for reading note data, each for a different execution ## `active_note` — the executing note -When a note script runs, `active_note` provides access to the current note's storage, assets, and metadata: +When a note script runs, `active_note` provides access to the current note's storage, creation-time assets, and metadata: ```rust use miden::active_note; @@ -39,9 +39,15 @@ let storage: Vec = active_note::get_storage(); ### Assets ```rust -let assets: Vec = active_note::get_assets(); +let assets: Vec = active_note::get_initial_assets(); ``` +The name makes the semantics explicit: these are the assets the note carried when it was created, before any in-transaction movement. This is an inspection API; iterating over this vector does not remove assets from the note's current state. + +:::warning Rust SDK limitation +The protocol exposes stateful `active_note::remove_asset` and `active_note::remove_all_assets` procedures in MASM, but the Rust SDK does not yet bind them. Do not implement asset consumption by passing values from `get_initial_assets()` directly to an account. Until the bindings land, use a standard note such as P2ID/P2IDE or write the removal flow in MASM. +::: + ### Identity and metadata ```rust @@ -53,13 +59,15 @@ let serial_num: Word = active_note::get_serial_number(); ### Note metadata -`get_metadata()` returns note metadata: +`get_metadata()` returns the encoded metadata header: ```rust let metadata: NoteMetadata = active_note::get_metadata(); ``` -On the v0.15 protocol side, user-facing note metadata is `PartialNoteMetadata`: +In the onchain Rust SDK, `NoteMetadata` contains a single `header: Word`; attachments and their commitment are queried separately. + +In `miden-protocol`, the user-facing metadata used to construct a note is `PartialNoteMetadata`: ```rust pub struct PartialNoteMetadata { @@ -69,7 +77,7 @@ pub struct PartialNoteMetadata { } ``` -The full `NoteMetadata` wraps that partial metadata together with attachment headers and an attachments commitment. Its encoded metadata word has four felts: sender suffix plus type/version, sender prefix, tag, and attachment schemes. +The protocol's full `NoteMetadata` wraps that partial metadata together with attachment headers and an attachments commitment. Its `to_metadata_word()` method produces the same four-felt header returned by the onchain SDK: sender suffix plus type/version, sender prefix, tag, and attachment schemes. ## `input_note` — querying notes by index @@ -82,11 +90,11 @@ use miden::input_note; ### Assets ```rust -let info: InputNoteAssetsInfo = input_note::get_assets_info(note_idx); -let assets: Vec = input_note::get_assets(note_idx); +let info: input_note::InputNoteAssetsInfo = input_note::get_initial_assets_info(note_idx); +let assets: Vec = input_note::get_initial_assets(note_idx); ``` -`InputNoteAssetsInfo` contains `commitment: Word` and `num_assets: Felt`. +`InputNoteAssetsInfo` contains `commitment: Word` and `num_assets: u32`. ### Identity and metadata @@ -100,14 +108,14 @@ let serial_num: Word = input_note::get_serial_number(note_idx); ### Storage ```rust -let storage_info: InputNoteStorageInfo = input_note::get_storage_info(note_idx); +let storage_info: input_note::InputNoteStorageInfo = input_note::get_storage_info(note_idx); ``` :::note -Unlike `active_note::get_storage()` which returns the full `Vec` of storage values, `input_note` only exposes the storage commitment and count — not the actual values. The transaction kernel only has commitments for input notes that are not currently executing. To read actual storage values, use `active_note::get_storage()` inside the note script itself. +Unlike `active_note::get_storage()`, the `input_note` API only exposes the storage commitment and item count. To read the storage values, use `active_note::get_storage()` while that note is executing. ::: -`InputNoteStorageInfo` contains `commitment: Word` and `num_storage_items: Felt`. +`InputNoteStorageInfo` contains `commitment: Word` and `num_storage_items: u32`. ### Note metadata @@ -119,28 +127,29 @@ let metadata: NoteMetadata = input_note::get_metadata(note_idx); ## Examples -### Reading storage in a note script +### Reading storage and inspecting initial assets -A note script that reads the target account ID from storage and verifies the consumer: +A note script that reads the target account ID from storage, verifies the consumer, and inspects the creation-time asset list: ```rust -use miden::{AccountId, Word, active_note, note}; +use miden::{AccountId, Word, account, active_note, note}; + +#[account(basic_wallet::BasicWallet)] +pub struct Wallet; #[note] -struct P2idNote { +struct InspectionNote { target_account_id: AccountId, } #[note] -impl P2idNote { +impl InspectionNote { #[note_script] - pub fn run(self, _arg: Word, account: &mut Account) { + pub fn run(self, _arg: Word, account: &mut Wallet) { assert_eq!(account.get_id(), self.target_account_id); - let assets = active_note::get_assets(); - for asset in assets { - account.receive_asset(asset); - } + // Inspection only: this does not remove assets from the active note. + let _initial_assets = active_note::get_initial_assets(); } } ``` @@ -153,14 +162,14 @@ A transaction script that reads data from a consumed input note: use miden::*; #[tx_script] -pub fn run(arg: Word) { +pub fn run(_arg: Word) { // Query the first input note (index 0) let idx = NoteIdx { inner: felt!(0) }; - let assets = input_note::get_assets(idx); - let sender = input_note::get_sender(idx); + let _assets = input_note::get_initial_assets(idx); + let _sender = input_note::get_sender(idx); } ``` :::info API Reference -Full API docs on docs.rs: [`miden::active_note`](https://docs.rs/miden/latest/miden/active_note/), [`miden::input_note`](https://docs.rs/miden/latest/miden/input_note/) +Full API docs on docs.rs: [`miden::active_note`](https://docs.rs/miden/0.14.0-rc.1/miden/active_note/), [`miden::input_note`](https://docs.rs/miden/0.14.0-rc.1/miden/input_note/) ::: diff --git a/docs/builder/smart-contracts/overview.md b/docs/builder/smart-contracts/overview.md index 32a745d4..50a3feaa 100644 --- a/docs/builder/smart-contracts/overview.md +++ b/docs/builder/smart-contracts/overview.md @@ -70,6 +70,7 @@ struct MyWalletStorage; #[component] trait MyWallet { + #[account_procedure] fn receive_asset(&mut self, asset: Asset); } diff --git a/docs/builder/smart-contracts/patterns.md b/docs/builder/smart-contracts/patterns.md index c68240d1..287d9d1d 100644 --- a/docs/builder/smart-contracts/patterns.md +++ b/docs/builder/smart-contracts/patterns.md @@ -18,11 +18,11 @@ Unlike Solidity, account component procedures cannot check "who is calling me." For account-level access control, Miden uses **authentication components** rather than manual sender checks. The transaction kernel calls the account's `auth` procedure automatically during the transaction epilogue — if the signature is invalid, the entire transaction fails. See [Authentication](./accounts/authentication) for the full pattern. -For note-level access control, note scripts can check who created the note using `active_note::get_sender()`. The protocol-level `ownable` standard (`miden-standards/asm/standards/access/ownable.masm`) provides `verify_owner`, `get_owner`, `transfer_ownership`, and `renounce_ownership` procedures. +For note-level access control, note scripts can check who created the note using `active_note::get_sender()`. The protocol-level `ownable2step` standard (`miden-standards/asm/standards/access/ownable2step.masm`) provides `get_owner`, `get_nominated_owner`, `is_sender_owner`, `assert_sender_is_owner`, `transfer_ownership`, `accept_ownership`, and `renounce_ownership` procedures. ## Rate limiting {#rate-limiting} -Use `tx::get_block_number()` to enforce cooldown periods between actions. Store the last action block number in a `Value` storage slot, then compare against the current block number before allowing the next action. +Use `tx::get_block_number()` to enforce cooldown periods between actions. It returns a typed `BlockNumber`; convert it with `.as_u32()` before using integer arithmetic. Store the last action block number in a `Value` storage slot, then compare it with the current block number before allowing the next action. See [Transaction Context](./transactions/transaction-context) for the available block and transaction info functions. @@ -48,6 +48,8 @@ Every state-changing transaction must increment the nonce. The auth component ha Use `saturating_sub` to prevent underflow: ```rust +let current_block = tx::get_block_number().as_u32(); + // Good — won't underflow let elapsed = current_block.saturating_sub(last_block); @@ -72,5 +74,6 @@ All Miden contracts run without the standard library: | `std::collections::HashMap` | Use `BTreeMap` from `alloc`, or `StorageMap` for persistent account storage | | `std::string::String` | Use `alloc::string::String` | | `std::vec::Vec` | Use `alloc::vec::Vec` | -| `println!()` / `eprintln!()` | No direct equivalent — run the transaction under the Mockchain and inspect outputs, or use the external debugger | +| `println!()` | Use `miden::println!()` or `miden::debug::println()`; formatting arguments are not supported | +| `eprintln!()` | No direct equivalent — run the transaction under the Mockchain and inspect outputs, or use the external debugger | | Error strings in `assert!()` | Use `assert!(condition)` without messages | diff --git a/docs/builder/smart-contracts/rust/index.md b/docs/builder/smart-contracts/rust/index.md index 53b140c7..42c5da51 100644 --- a/docs/builder/smart-contracts/rust/index.md +++ b/docs/builder/smart-contracts/rust/index.md @@ -7,8 +7,6 @@ description: "Author Miden smart contracts in Rust — the long-term direction f The Rust SDK is the long-term direction for Miden smart-contract development: define account components, note scripts, and transaction scripts in idiomatic `#![no_std]` Rust with typed storage, attribute macros, and client-side proving. The SDK compiles to Miden Assembly (MASM) under the hood, so the same execution model and standards library apply. -The examples in this section assume protocol v0.15.3, Miden VM / Assembly v0.23, miden-crypto v0.25, and the Miden SDK v0.13 macro surface. - :::caution Currently in active development The Rust SDK is being actively developed and is **not yet production-ready for mainnet**. For production deployments today, write contracts in [Miden Assembly (MASM)](../masm/) — the supported path Miden mainnet verifies. Use the Rust SDK for prototyping, experimentation, and exploration of the long-term direction. ::: diff --git a/docs/builder/smart-contracts/standards/account-components.md b/docs/builder/smart-contracts/standards/account-components.md index 463786c3..5e4fa250 100644 --- a/docs/builder/smart-contracts/standards/account-components.md +++ b/docs/builder/smart-contracts/standards/account-components.md @@ -1,6 +1,6 @@ --- title: "Account Components" -description: "Use standard account components for wallets, authentication, access control, faucets, and metadata." +description: "Use standard account components for wallets, authentication, access control, faucets, and account inspection." --- # Account Components @@ -16,13 +16,12 @@ Use these components from Rust when you build accounts with the SDK, or import t | `BasicWallet` | Holding assets, receiving assets from standard notes, and moving assets into output notes. | `miden_standards::account::wallets` | | `FungibleFaucet` | Minting, sending, receiving, and burning fungible assets from faucet accounts. | `miden_standards::account::faucets` | | `AuthSingleSig` | Single-signature authentication of transactions. | `miden_standards::account::auth` | -| `AuthSingleSigAcl` | Single-signature authentication with an access-control list. | `miden_standards::account::auth` | | `AuthMultisig` / `AuthMultisigSmart` | Threshold or policy-aware multisig authentication. | `miden_standards::account::auth` | | `AuthGuardedMultisig` | Multisig guarded by a guardian configuration. | `miden_standards::account::auth` | -| `AuthNetworkAccount` | Authentication through note allowlists for network accounts. | `miden_standards::account::auth` | +| `AuthNetworkAccount` | Authentication through note- and transaction-script allowlists for network accounts. | `miden_standards::account::auth` | | `Ownable2Step` | Access control for account owners. | `miden_standards::account::access` | -| `RoleBasedAccessControl` | Role-based authorization for token policy management. | `miden_standards::account::access` | -| `Authority` | Shared authority component used by policy-management standards. | `miden_standards::account::access` | +| `RoleBasedAccessControl` | Role-based authorization for protected account procedures. | `miden_standards::account::access` | +| `Authority` | Shared authority component for protecting administrative changes. | `miden_standards::account::access` | | `TokenPolicyManager` | Registering and updating mint, burn, send, and receive token policies. | `miden_standards::account::policies` | | `BasicBlocklist` | Blocking specific native accounts in send and receive transfer-policy checks. | `miden_standards::account::policies` | | `BasicAllowlist` | Allowing only specific native accounts in send and receive transfer-policy checks. | `miden_standards::account::policies` | @@ -41,7 +40,7 @@ Most regular accounts need: ```rust title="Compose a regular account with standard auth and wallet components" use miden_client::{ account::{AccountBuilder, AccountType, component::BasicWallet}, - auth::{AuthSchemeId, AuthSingleSig}, + auth::{Approver, AuthSchemeId, AuthSingleSig}, }; use miden_protocol::{account::auth::PublicKeyCommitment, Word}; @@ -50,11 +49,14 @@ fn build_wallet_account() -> Result<(), Box> { let account = AccountBuilder::new([1; 32]) .account_type(AccountType::Public) - .with_auth_component(AuthSingleSig::new(public_key, AuthSchemeId::Falcon512Poseidon2)) + .with_component(AuthSingleSig::new(Approver::new( + public_key, + AuthSchemeId::Falcon512Poseidon2, + ))) .with_component(BasicWallet) .build()?; - assert_eq!(account.account_type(), AccountType::Public); + assert!(account.is_public()); Ok(()) } ``` @@ -68,9 +70,9 @@ Standard notes assume the consuming account exposes the procedures they need. Fo At the builder level, the practical rule is: - Add `BasicWallet` to accounts that should receive standard asset-transfer notes. -- Add `FungibleFaucet` to faucet accounts that should mint or burn fungible assets. +- Add `FungibleFaucet` together with `TokenPolicyManager` to faucet accounts that should mint or burn fungible assets. - For local or user accounts, add an auth component to reject unauthorized transactions. -- For network accounts, add an access-control component to gate the account procedures notes can call. +- For network accounts, use `AuthNetworkAccount` to restrict the allowed note and transaction scripts. Prefer building on top of `BasicWallet`: compose it with a custom extension component for application-specific methods. If you replace the wallet interface entirely, test consumption of the relevant standard notes deliberately. @@ -84,7 +86,8 @@ Rust APIs are the usual entry point for account composition. MASM modules are av | Authentication | `miden_standards::account::auth` | `miden::standards::auth::*` | | Access control | `miden_standards::account::access` | `miden::standards::access::*` | | Faucets | `miden_standards::account::faucets` | `miden::standards::faucets::*` | -| Metadata | `miden_standards::account::metadata` | `miden::standards::metadata::*` | +| Policies | `miden_standards::account::policies` | `miden::standards::faucets::policies::*` | +| Inspection | `miden_standards::account::inspection` | `miden::standards::inspection::*` | Reach for MASM directly when you are implementing low-level behavior, integrating a custom component with a standard procedure, or verifying exact stack effects. diff --git a/docs/builder/smart-contracts/standards/faucets-and-policies.md b/docs/builder/smart-contracts/standards/faucets-and-policies.md index 21841e93..6ac9fe6a 100644 --- a/docs/builder/smart-contracts/standards/faucets-and-policies.md +++ b/docs/builder/smart-contracts/standards/faucets-and-policies.md @@ -16,7 +16,7 @@ The current standard fungible faucet component is `FungibleFaucet`. | Surface | Entry point | |---------|-------------| | Rust component | `miden_standards::account::faucets::FungibleFaucet` | -| Rust builder/helper | `FungibleFaucetBuilder`, `create_fungible_faucet` | +| Rust builder/helper | `FungibleFaucetBuilder`, `create_singlesig_user_fungible_faucet` | | MASM component | `miden::standards::faucets::fungible` | | Account role | Faucet account whose account ID identifies the issuer. | @@ -27,26 +27,24 @@ use miden_client::{ account::{ AccountType, component::{ - AccessControl, - BurnPolicyConfig, + AuthSingleSig, + BurnPolicy, FungibleFaucet, - MintPolicyConfig, - PolicyRegistration, + MintPolicy, TokenName, TokenPolicyManager, TransferPolicy, - create_fungible_faucet, + create_singlesig_user_fungible_faucet, }, }, asset::TokenSymbol, + auth::Approver, }; use miden_protocol::{ account::auth::{AuthScheme, PublicKeyCommitment}, asset::AssetAmount, Word, }; -use miden_standards::AuthMethod; - fn create_faucet_account() -> Result<(), Box> { let public_key = PublicKeyCommitment::from(Word::from([1, 2, 3, 4u32])); @@ -57,24 +55,27 @@ fn create_faucet_account() -> Result<(), Box> { .max_supply(AssetAmount::from(1_000_000u32)) .build()?; - let policies = TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)? - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + + let auth = AuthSingleSig::new(Approver::new( + public_key, + AuthScheme::Falcon512Poseidon2, + )); - let account = create_fungible_faucet( + let account = create_singlesig_user_fungible_faucet( [9; 32], faucet, - AccountType::Public, - AuthMethod::SingleSig { - approver: (public_key, AuthScheme::Falcon512Poseidon2), - }, - AccessControl::AuthControlled, + auth, policies, + AccountType::Public, )?; - assert_eq!(account.account_type(), AccountType::Public); + assert!(account.is_public()); Ok(()) } ``` @@ -92,7 +93,7 @@ A fungible asset is tied to its faucet account ID. The faucet's metadata describ | Optional metadata | Optional display fields such as description, logo URI, and external link. | | Faucet account ID | The issuer ID used when constructing fungible assets and checking balances. | -When an account checks its balance for a fungible token at the protocol/client layer, it queries by the asset's `AssetVaultKey`, which is derived from the faucet account ID and callback flag. +When an account checks its balance for a fungible token at the protocol/client layer, it queries by the asset's `AssetId`, which is derived from the faucet account ID. Whether the asset invokes callbacks is encoded in the faucet account ID at construction time. ## Choose policy modules @@ -100,10 +101,12 @@ Policy modules decide which operations are allowed for a token faucet. | Policy area | Current standard examples | Use it for | |-------------|---------------------------|------------| -| Mint | `MintPolicyConfig::AllowAll`, `MintPolicyConfig::OwnerOnly` | Gate mint operations. | -| Burn | `BurnPolicyConfig::AllowAll`, `BurnPolicyConfig::OwnerOnly` | Gate burn operations. | -| Send | `TransferPolicy::AllowAll`, `BasicBlocklist`, `BlocklistOwnerControlled` | Gate assets leaving accounts through notes. | -| Receive | `TransferPolicy::AllowAll`, `BasicBlocklist`, `BlocklistOwnerControlled` | Gate assets entering account vaults. | +| Mint | `MintPolicy::allow_all()`, `MintPolicy::owner_only()` | Gate mint operations. | +| Burn | `BurnPolicy::allow_all()`, `BurnPolicy::owner_only()` | Gate burn operations. | +| Send | `TransferPolicy::allow_all()`, `TransferPolicy::empty_basic_blocklist()`, `TransferPolicy::with_basic_blocklist(...)` | Gate assets leaving accounts through notes. | +| Receive | `TransferPolicy::allow_all()`, `TransferPolicy::empty_basic_blocklist()`, `TransferPolicy::with_basic_blocklist(...)` | Gate assets entering account vaults. | + +Use `BlocklistManager` alongside a basic blocklist when its entries must be updated at runtime. `TokenPolicyManager` owns the active policy roots and validates policy changes. Authority for changing policies comes from the account's access-control setup, such as owner-controlled or role-based authority. diff --git a/docs/builder/smart-contracts/standards/index.md b/docs/builder/smart-contracts/standards/index.md index d5264be3..54b1d8d0 100644 --- a/docs/builder/smart-contracts/standards/index.md +++ b/docs/builder/smart-contracts/standards/index.md @@ -10,7 +10,7 @@ Miden Standards are reusable building blocks for common smart-contract behavior: Use them when you want your account, note, or transaction flow to interoperate with the rest of the Miden ecosystem instead of defining every interface from scratch. :::caution Versioned APIs -These pages track the v0.15 standards surface. Use the version selector if you are building against an older protocol release. +These pages track the v0.16 standards surface. Use the version selector if you are building against an older protocol release. ::: This section is a builder guide, not the canonical standards specification. It explains which standard to reach for, how it fits into the smart-contract model, and where to switch to reference docs when you need exact procedure names, storage schemas, or script roots. diff --git a/docs/builder/smart-contracts/standards/standard-notes.md b/docs/builder/smart-contracts/standards/standard-notes.md index be246913..d584c230 100644 --- a/docs/builder/smart-contracts/standards/standard-notes.md +++ b/docs/builder/smart-contracts/standards/standard-notes.md @@ -17,17 +17,19 @@ Use the Rust APIs to construct standard notes in client or transaction-building | P2IDE | You are sending to a specific account ID with a timelock and/or reclaim path. | `P2ideNote` | `miden::standards::notes::p2ide` | | SWAP | You are offering one asset and requiring a specific asset in return. | `SwapNote` | `miden::standards::notes::swap` | | PSWAP | You need a partially fillable swap note. | `PswapNote` | `miden::standards::notes::pswap` | -| MINT | A faucet is minting fungible tokens into a note. | `MintNote` | `miden::standards::notes::mint` | -| BURN | A faucet is burning fungible tokens returned through a note. | `BurnNote` | `miden::standards::notes::burn` | +| MINT | A faucet is minting an asset into a note. | `MintNote` | `miden::standards::notes::mint` | +| BURN | A faucet is burning an asset returned through a note. | `BurnNote` | `miden::standards::notes::burn` | For the note model itself, start with [What are Notes?](../notes/introduction). This page focuses on how the standards fit into builder workflows. ```rust title="Create a public P2ID note" use miden_protocol::Word; -use miden_protocol::account::{AccountId, AccountIdVersion, AccountType}; +use miden_protocol::account::{ + AccountId, AccountIdVersion, AccountType, AssetCallbackFlag, +}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::RandomCoin; -use miden_protocol::note::{NoteAttachments, NoteType}; +use miden_protocol::note::{Note, NoteType}; use miden_standards::note::P2idNote; fn dummy_account(byte: u8, account_type: AccountType) -> AccountId { @@ -37,6 +39,7 @@ fn dummy_account(byte: u8, account_type: AccountType) -> AccountId { bytes, AccountIdVersion::Version1, account_type, + AssetCallbackFlag::Disabled, ) } @@ -47,30 +50,34 @@ fn create_p2id_note() -> Result<(), Box> { let asset: Asset = FungibleAsset::new(faucet_id, 100)?.into(); let mut rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); - let note = P2idNote::create( - sender, - target, - vec![asset], - NoteType::Public, - NoteAttachments::empty(), - &mut rng, - )?; + let note: Note = P2idNote::builder() + .sender(sender) + .target(target) + .asset(asset) + .note_type(NoteType::Public) + .generate_serial_number(&mut rng) + .build()? + .into(); assert_eq!(note.metadata().sender(), sender); Ok(()) } ``` +`AccountId::dummy` is available with the protocol crate's `testing` feature +and keeps this example self-contained. Production code should use account IDs +created or retrieved through the client. + ## Account requirements Standard notes assume the consuming account exposes the procedures the note script calls. | Note | Consuming account needs | |------|-------------------------| -| P2ID / P2IDE | A wallet-compatible receive procedure, usually from `BasicWallet`. | -| SWAP / PSWAP | Wallet-compatible receive and asset-to-note procedures. | -| MINT | A compatible faucet/account flow for mint authorization and recipient delivery. | -| BURN | A compatible faucet burn procedure. | +| P2ID / P2IDE | `BasicWallet`, exposing `receive_asset`. | +| SWAP / PSWAP | `BasicWallet` and `NoteCreator`, exposing `receive_asset`, `move_asset_to_note`, and `create_note`. | +| MINT | A network faucet exposing `CodeInspection::has_procedure` and a fungible or non-fungible `mint_and_send` procedure. | +| BURN | The issuing faucet exposing `CodeInspection::has_procedure` and a fungible or non-fungible `receive_and_burn` procedure. | If you write a custom wallet or faucet component, test it against the standard notes you expect it to consume. @@ -80,7 +87,7 @@ Standard notes can use attachments and execution hints to help clients and index | Helper | Use it for | |--------|------------| -| `StandardNoteAttachment` | Standard attachment schemes for note metadata. | +| `StandardNoteAttachment` | Identifiers for standard attachment schemes. | | `NetworkAccountTarget` | Attaching network-account targeting data to notes. | | `AccountTargetNetworkNote` | Wrapping notes known to target network accounts. | | `NetworkNoteExt` | Convenience helpers for network-targeted notes. | diff --git a/docs/builder/smart-contracts/transactions/introduction.md b/docs/builder/smart-contracts/transactions/introduction.md index 61796f40..61c0753d 100644 --- a/docs/builder/smart-contracts/transactions/introduction.md +++ b/docs/builder/smart-contracts/transactions/introduction.md @@ -8,7 +8,7 @@ description: "Transactions are Miden's execution unit — they consume input not Transactions are the execution unit in Miden. Every state change — transferring assets, updating storage, minting tokens — happens inside a transaction. Each transaction runs against a single account, consumes zero or more input notes, and produces zero or more output notes. -The critical difference from other blockchains: transactions execute locally on the user's machine, not on a shared VM. After execution, the Miden VM generates a zero-knowledge proof that the transaction was valid (see [transaction design](/reference/protocol/transaction)). Only this proof and the resulting state commitments are submitted to the network. The network never sees the transaction inputs, the account's private state, or the logic that ran. +The critical difference from other blockchains: transactions execute locally on the user's machine, not on a shared VM. After execution, the Miden VM generates a zero-knowledge proof that the transaction was valid and the client seals the transaction inputs (see [transaction design](/reference/protocol/transaction)). The proof, resulting state commitments, and sealed inputs are submitted to the network. The network does not receive private inputs in plaintext or see the account's private state and execution trace. ## Anatomy of a transaction @@ -17,7 +17,7 @@ Every transaction has these elements: | Element | Description | |---------|-------------| | **Account** | The single account this transaction mutates — its storage, vault, and nonce | -| **Input notes** | Zero or more notes being consumed — their scripts run and assets transfer to the account | +| **Input notes** | Zero or more notes being consumed — their scripts run and explicitly remove any assets they move | | **Output notes** | Zero or more notes being created — carrying assets and scripts for future consumption | | **Transaction script** | Optional entry-point logic that runs in addition to note scripts and component code | | **Block reference** | The chain state the transaction executes against — provides block number, timestamp, and commitments | @@ -28,18 +28,18 @@ A transaction can only modify one account. Cross-account interactions happen thr ``` ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Build │────▶│ Execute │────▶│ Prove │────▶│ Submit │────▶│ Verify │ +│ Build │────▶│ Execute │────▶│ Prove │────▶│ Submit │────▶│ Verify │ │ │ │ │ │ │ │ │ │ │ │ Assemble │ │ VM runs │ │ ZK proof │ │ Proof + │ │ Network │ -│ tx inputs│ │ locally │ │ generated│ │ state │ │ updates │ -│ │ │ │ │ │ │ sent │ │ state │ +│ tx inputs│ │ locally │ │ generated│ │ sealed │ │ updates │ +│ │ │ │ │ │ │ inputs │ │ state │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ``` 1. **Build**: The client assembles the transaction — which account, which notes to consume, what methods to call. 2. **Execute**: The Miden VM runs the transaction locally. Note scripts execute, component code runs, storage is mutated, output notes are created. 3. **Prove**: The VM produces a zero-knowledge proof of correct execution. If any assertion fails (e.g., insufficient balance, unauthorized caller), the proof cannot be generated — the transaction is rejected before it ever reaches the network. -4. **Submit**: The proof and public state updates (new note commitments, updated account commitment, nullifiers for consumed notes) are submitted to the network. +4. **Submit**: The proof, sealed transaction inputs, and public state updates (new note commitments, updated account commitment, and nullifiers for consumed notes) are submitted to the network. 5. **Verify**: The network verifies the proof, records the state changes, and includes the transaction in a batch and eventually a block. ## The transaction context @@ -47,11 +47,11 @@ A transaction can only modify one account. Cross-account interactions happen thr During execution, your code runs inside a **transaction context** that provides access to: - **Block data** — current block number, timestamp, and commitments via the `tx` module -- **Input notes** — the notes being consumed, their assets, inputs, and metadata +- **Input notes** — the notes being consumed, their assets, recipient storage, and metadata - **Account state** — the executing account's storage, vault, and nonce - **Output notes** — the ability to create new notes and attach assets -The transaction context is what connects your component code to the chain state. For example, you can implement time-based logic by comparing `tx::get_block_number()` against a stored value, or read note inputs to determine what action to take. +The transaction context is what connects your component code to the chain state. For example, you can implement time-based logic by comparing `tx::get_block_number()` against a stored value, or read note storage to determine what action to take. ## What happens when execution fails @@ -70,15 +70,15 @@ The ZK circuit **cannot produce a valid proof**. This means: This is fundamentally different from Ethereum's `revert`, where the failed transaction still lands onchain, consumes gas, and is visible to everyone. -A separate failure mode is the **empty transaction**: a transaction that runs to completion but mutates no account state (storage, vault, or nonce) and consumes no input notes. Both the Rust client (which raises `TransactionRequestError::NoInputNotesNorAccountChange` before submission) and the VM kernel reject it. This typically catches transaction scripts whose conditional logic takes a no-op branch — see [Empty Transaction](../../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) in the pitfalls guide for the recommended pattern. +A separate failure mode is the **empty transaction**: a transaction that runs to completion but mutates no account state (storage, vault, or nonce) and consumes no input notes. The VM kernel rejects it during execution. This typically catches transaction scripts whose conditional logic takes a no-op branch — see [Empty Transaction](../../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) in the pitfalls guide for the recommended pattern. ## How transactions differ from EVM transactions | | EVM | Miden | |---|---|---| -| **Execution** | Every validator re-executes the transaction | Client executes locally, submits only the proof | +| **Execution** | Every validator re-executes the transaction | Client executes locally, then submits the proof and sealed inputs | | **Scope** | Can call multiple contracts in one tx | One transaction mutates one account; cross-account via notes | -| **Privacy** | All inputs, state reads, and call traces are public | Network sees only the proof and state commitments | +| **Privacy** | All inputs, state reads, and call traces are public | Private inputs are sealed before submission; the network verifies the proof and state commitments | | **Failure** | Onchain revert, gas consumed, visible trace | Proof can't be generated — no onchain trace, no cost | | **Parallelism** | Transactions touching same state must serialize | Single-account scope enables parallel execution | -| **Authentication** | `msg.sender` set by protocol | Falcon-512 Poseidon2 signatures verified inside the transaction | +| **Authentication** | `msg.sender` set by protocol | The account authentication procedure verifies the configured scheme, such as Falcon-512 Poseidon2 or ECDSA K256 Keccak | diff --git a/docs/builder/smart-contracts/transactions/transaction-context.md b/docs/builder/smart-contracts/transactions/transaction-context.md index 9414f6b2..a03dbf50 100644 --- a/docs/builder/smart-contracts/transactions/transaction-context.md +++ b/docs/builder/smart-contracts/transactions/transaction-context.md @@ -11,20 +11,20 @@ A Miden transaction is a local operation that consumes zero or more input notes ## The `tx` module ```rust -use miden::tx; +use miden::{BlockNumber, Word, tx}; ``` ### Block information ```rust // Current block number -let block_num: Felt = tx::get_block_number(); +let block_num: BlockNumber = tx::get_block_number(); // Block commitment (hash of block header) let commitment: Word = tx::get_block_commitment(); // Block timestamp (seconds since epoch) -let timestamp: Felt = tx::get_block_timestamp(); +let timestamp: u32 = tx::get_block_timestamp(); ``` ### Note commitments @@ -37,8 +37,8 @@ let input_commit: Word = tx::get_input_notes_commitment(); let output_commit: Word = tx::get_output_notes_commitment(); // Number of input/output notes -let num_inputs: Felt = tx::get_num_input_notes(); -let num_outputs: Felt = tx::get_num_output_notes(); +let num_inputs: u32 = tx::get_num_input_notes(); +let num_outputs: u32 = tx::get_num_output_notes(); ``` ### Transaction expiration @@ -47,13 +47,13 @@ Control how long a transaction remains valid: ```rust // Get current expiration delta (in blocks) -let delta: Felt = tx::get_expiration_block_delta(); +let delta: u16 = tx::get_expiration_block_delta(); // Set a new expiration delta -tx::update_expiration_block_delta(felt!(100)); +tx::update_expiration_block_delta(100); ``` -The expiration delta determines how many blocks after creation the transaction remains valid. If the transaction isn't included within this window, it expires. +The expiration delta is measured from the transaction's reference block. A value of `0` means no expiration has been set; updates must be between `1` and `u16::MAX` and can only tighten an existing expiration limit. ## Transaction scripts diff --git a/docs/builder/smart-contracts/transactions/transaction-scripts.md b/docs/builder/smart-contracts/transactions/transaction-scripts.md index 0585e0d7..15a15ddb 100644 --- a/docs/builder/smart-contracts/transactions/transaction-scripts.md +++ b/docs/builder/smart-contracts/transactions/transaction-scripts.md @@ -38,7 +38,8 @@ version = "0.1.0" [lib] kind = "tx-script" -namespace = "miden:basic-wallet-tx-script/basic-wallet-tx-script@0.1.0" +namespace = "miden:base/transaction-script@1.0.0" +path = "src/lib.rs" [dependencies] miden-core = "*" @@ -51,7 +52,9 @@ basic-wallet = { wit = "../basic-wallet/target/generated-wit/" } ## Example: basic-wallet-tx-script -This example reads note parameters from the advice map and creates an output note: +This example decodes structured input from the advice map and asks the account's +wallet component to create the output note. `output_note::create` is restricted +to account-component context, so a transaction script cannot call it directly. ```rust // Do not link against libstd (i.e. anything defined in `std::`) @@ -76,35 +79,48 @@ const NOTE_TYPE_INDEX: usize = 1; const RECIPIENT_START: usize = 2; const RECIPIENT_END: usize = 6; const ASSET_START: usize = 6; -const ASSET_END: usize = 10; +const ASSET_END: usize = 14; #[tx_script] fn run(arg: Word, account: &mut Wallet) { let num_felts = adv_push_mapvaln(arg.clone()); let num_felts_u64 = num_felts.as_canonical_u64(); assert_eq!(Felt::from_u32((num_felts_u64 % 4) as u32), felt!(0)); + let num_words = Felt::new(num_felts_u64 / 4).unwrap(); let commitment = arg; let input = adv_load_preimage(num_words, commitment); + let tag = input[TAG_INDEX]; let note_type = input[NOTE_TYPE_INDEX]; - let recipient: [Felt; 4] = input[RECIPIENT_START..RECIPIENT_END].try_into().unwrap(); - let note_idx = output_note::create(tag.into(), note_type.into(), recipient.into()); - let asset: [Felt; 4] = input[ASSET_START..ASSET_END].try_into().unwrap(); - account.move_asset_to_note(asset.into(), note_idx); + let recipient: [Felt; 4] = + input[RECIPIENT_START..RECIPIENT_END].try_into().unwrap(); + + let note_idx = + account.create_note(tag.into(), note_type.into(), recipient.into()); + + // Contract-side assets contain an ID word followed by a value word. + let asset: [Felt; 8] = input[ASSET_START..ASSET_END].try_into().unwrap(); + let asset_key: [Felt; 4] = asset[..4].try_into().unwrap(); + let asset_value: [Felt; 4] = asset[4..].try_into().unwrap(); + let asset = Asset::new(asset_key, asset_value); + + account.move_asset_to_note(asset, note_idx); } ``` ### Walkthrough -1. **`arg: Word`** is a map key used to look up the transaction data in the advice map -2. **`adv_push_mapvaln(arg)`** reads the number of felts stored at that key -3. **`adv_load_preimage(num_words, commitment)`** retrieves the actual data (tag, note_type, recipient, asset) from the advice map -4. **`output_note::create(tag, note_type, recipient)`** creates the output note -5. **`account.move_asset_to_note(asset, note_idx)`** moves the asset from the account vault into the newly created note +1. **`arg: Word`** is the commitment used to look up the structured input in the advice map. +2. **`adv_push_mapvaln(arg)`** loads the preimage length, and `adv_load_preimage(...)` retrieves the tag, note type, recipient, and two-word asset. +3. **`account.create_note(...)`** crosses into the installed wallet component, where note creation is permitted. +4. **`account.move_asset_to_note(...)`** removes the asset from the account vault and attaches it to the new note. :::note -This script uses the advice map to pass structured input data. The caller encodes the note parameters (tag, note_type, recipient, asset) as a preimage and passes the commitment hash as the `arg` Word. +Host code must insert a 16-felt, word-aligned preimage into the advice map: +tag (1), note type (1), recipient (4), asset (8), and two zero padding felts. +Hash all 16 felts and pass that commitment as the transaction-script argument. +Keep the host and guest field order in sync. ::: :::tip diff --git a/docs/builder/smart-contracts/types.md b/docs/builder/smart-contracts/types.md index 5a4da414..fd7ecda9 100644 --- a/docs/builder/smart-contracts/types.md +++ b/docs/builder/smart-contracts/types.md @@ -25,7 +25,7 @@ $$ ```rust use miden::{felt, Felt}; -// Compile-time literal (validated at compile time) +// Literal construction (validated when evaluated) let zero = felt!(0); let one = felt!(1); let answer = felt!(42); @@ -42,7 +42,10 @@ let o = Felt::ONE; ``` :::info `felt!()` range limitation -The `felt!()` macro currently only accepts values up to `u32::MAX` (4,294,967,295). For larger values, use `Felt::new(...).unwrap()` or handle the error. This limitation may be lifted in a future release. +The `felt!()` macro accepts integer literals representable as `u64` and +validates them through `Felt::new(...).unwrap()`. An out-of-field literal +panics when evaluated; it is not currently rejected by `cargo check`. For +runtime values, use the fallible `Felt::new(...)` and handle the error. ::: ### Arithmetic @@ -113,8 +116,8 @@ let inv = f.inv(); // Panics if f == felt!(0) // Exponentiation: base^exponent mod p let result = f.exp(felt!(3)); // 7^3 mod p = 343 -// Power of 2: computes 2^self -let power = felt!(10).pow2(); // 2^10 = 1024 (panics if self > 63) +// Squaring: f^2 +let square = f.square(); // 7^2 mod p = 49 ``` ## Word — Four field elements @@ -184,7 +187,7 @@ let cooldown = config.b.as_canonical_u64(); ## Asset -`Asset` represents either a fungible or non-fungible asset. In v0.15 contract code it is **two words** — a `key` (an asset vault key identifying the asset class and composition) and a `value` (encoding the fungible amount or non-fungible data). +`Asset` represents either a fungible or non-fungible asset. In contract code it is **two words** — a `key` (the asset ID used by the vault) and a `value` (encoding the fungible amount or non-fungible data). ```rust pub struct Asset { @@ -216,9 +219,9 @@ pub struct Asset { | `key` | `b` | Data hash element 1 | | `key` | `c` | Faucet ID suffix plus metadata byte | | `key` | `d` | Faucet ID prefix | -| `value` | `a..d`| Data payload (implementation-defined) | +| `value` | `a..d`| Data hash elements 0–3 | -The low metadata byte in `key.c` encodes `AssetComposition` in bits 0-1 and the callback flag in bit 2. Use the protocol helpers instead of hand-decoding this byte. +The low metadata byte in `key.c` encodes `AssetComposition` in bits 0-1. Whether assets invoke callbacks is encoded in the faucet account ID when that account is built. Use protocol helpers instead of hand-decoding the metadata. ### Working with assets @@ -236,16 +239,14 @@ let asset = Asset::new( // Read the amount (fungible): first limb of `value`. let amount: u64 = asset.value.a.as_canonical_u64(); -// Build a fungible asset from faucet ID + amount via the SDK helper. -use miden::asset; -let asset = asset::create_fungible_asset(faucet_id, felt!(1000), false); - -// Build a non-fungible asset. -let nft = asset::create_non_fungible_asset(faucet_id, data_hash, false); +// Assets passed into contract procedures are already constructed by the host. +// Read the ID word when querying the active account vault. +let asset_id: Word = asset.key; +let is_present = miden::active_account::has_asset(asset_id); ``` :::note Asset on the host side -On the client / host side, `Asset` is an enum (`Asset::Fungible(_) | Asset::NonFungible(_)`) exposed from `miden-protocol`, with `to_key_word()` / `to_value_word()` / `from_key_value_words()` helpers. Inside a Rust contract the SDK exposes the two-word `Asset` struct shown above. +On the client / host side, `Asset` is an enum (`Asset::Fungible(_) | Asset::NonFungible(_)`) exposed from `miden-protocol`, with `id()` / `to_id_word()` / `to_value_word()` / `from_id_and_value_words()` helpers. Inside a Rust contract the SDK exposes the two-word `Asset` struct shown above. ::: ## AccountId From 3ab901f1ef734d31ddf673b9cb56ad6c793f8ded Mon Sep 17 00:00:00 2001 From: 0xrouss-miden <312482795+0xrouss-miden@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:14 +0200 Subject: [PATCH 3/6] docs(web-client,react-sdk): update SDK docs for v0.16 Refresh the locally authored Web and React SDK guides, signer and lifecycle patterns, transaction flows, setup guidance, and local-node integration for the v0.16 SDK line. --- .../tools/clients/local-node-testing.md | 51 ++++---- .../react-sdk/account-state-and-balances.md | 18 +-- .../tools/clients/react-sdk/advanced.md | 81 ++++++++++++- docs/builder/tools/clients/react-sdk/index.md | 4 +- .../tools/clients/react-sdk/query-hooks.md | 17 ++- .../tools/clients/react-sdk/recipes.md | 51 ++++---- docs/builder/tools/clients/react-sdk/setup.md | 25 ++-- .../tools/clients/react-sdk/signers.md | 36 ++++-- .../tools/clients/web-client/accounts.md | 15 +-- .../tools/clients/web-client/compile.md | 42 ++++--- .../builder/tools/clients/web-client/index.md | 14 +-- .../builder/tools/clients/web-client/notes.md | 37 +++--- .../builder/tools/clients/web-client/setup.md | 21 ++-- docs/builder/tools/clients/web-client/sync.md | 2 +- .../tools/clients/web-client/testing.md | 39 +++++-- .../tools/clients/web-client/transactions.md | 110 +++++++++++++----- 16 files changed, 376 insertions(+), 187 deletions(-) diff --git a/docs/builder/tools/clients/local-node-testing.md b/docs/builder/tools/clients/local-node-testing.md index 4ace31d0..f908ec61 100644 --- a/docs/builder/tools/clients/local-node-testing.md +++ b/docs/builder/tools/clients/local-node-testing.md @@ -14,10 +14,10 @@ Use a local node when a test needs real node state: public accounts, block commi | --- | --- | | Browser or app testing against a local network | The node repo Docker Compose stack | | Rust client integration tests | `TEST_MIDEN_NETWORK=localhost` against a running local node | -| Private note delivery | A separate Miden Note Transport node | +| Private note delivery | The node Compose stack with the `note-transport` profile enabled | | Future one-command local dev | Track [node#1874](https://github.com/0xMiden/node/issues/1874) and [midenup#180](https://github.com/0xMiden/midenup/issues/180) | -Docker Compose is the supported default path for running the current local node stack. The miden-client repo also has a `make start-node` helper for its own integration tests, but that helper runs the test node directly with Cargo and is not the operator-facing Docker workflow. +Docker Compose is the supported default path for running the current local node stack. The rust-sdk repo also has a `make start-node` helper for its own integration tests, but that helper runs the test node directly with Cargo and is not the operator-facing Docker workflow. ## Prerequisites @@ -29,19 +29,17 @@ On Linux, make sure your user can run Docker commands without `sudo`, or prefix ## Start a local node -Clone the node repo into a directory named `miden-node`. The account export command below assumes this Compose project name, which gives the genesis volume the name `miden-node_node-data`. +Clone the compatible node release into a directory named `miden-node`. The account export command below assumes this Compose project name, which gives the genesis volume the name `miden-node_node-data`. ```bash -git clone https://github.com/0xMiden/node.git miden-node +git clone --branch v0.16.0 --depth 1 https://github.com/0xMiden/node.git miden-node cd miden-node -make docker-build-node -make docker-build-monitor -make compose-genesis -make compose-up +make local-network-build +make local-network-up ``` -The stack starts the store, validator, block producer, RPC component, network transaction builder, telemetry, and network monitor. The RPC endpoint is: +The stack starts the sequencer, three validators, transaction prover, network transaction builder, telemetry services, and network monitor. The RPC endpoint is: ```text http://localhost:57291 @@ -50,47 +48,47 @@ http://localhost:57291 Check the containers: ```bash -docker compose -f docker-compose.yml -f compose/telemetry.yml -f compose/monitor.yml ps +docker compose --profile telemetry --profile monitor ps ``` Follow node logs: ```bash -make compose-logs +make local-network-logs ``` Stop the node without deleting chain data: ```bash -make compose-down +make local-network-down ``` Reset the chain to a fresh genesis: ```bash -make compose-genesis -make compose-up +make local-network-delete +make local-network-up ``` For the full node operator workflow, see the [local network development guide](../../../reference/node/local-network-development). ## Export the genesis account -The local genesis process writes account files into the Compose volume. Copy the default genesis account into the repo root when you need to import it into a client: +The local genesis process writes account files into the Compose volume. Copy the faucet operator account into the repo root when you need an existing local account in a client: ```bash docker run --rm \ -v miden-node_node-data:/data:ro \ -v "$PWD":/out \ alpine:3.20 \ - cp /data/accounts/account.mac /out/account.mac + cp /data/accounts/faucet_operator.mac /out/faucet_operator.mac ``` Then configure the CLI for localhost and import the account: ```bash miden-client init --local --network localhost -miden-client import account.mac +miden-client import faucet_operator.mac miden-client sync miden-client account --list ``` @@ -114,13 +112,19 @@ const client = await MidenClient.create({ await client.sync(); ``` -For private note delivery, run a Miden Note Transport node separately and pass its raw URL. The Web SDK has `testnet` and `devnet` shorthands for note transport, but no `localhost` shorthand. +For private note delivery, enable the optional Note Transport service included in the node Compose stack: + +```bash +docker compose --profile note-transport up -d +``` + +Then pass its browser-facing gRPC-Web URL. The Web SDK has `testnet` and `devnet` shorthands for note transport, but no `localhost` shorthand. ```typescript const client = await MidenClient.create({ rpcUrl: "localhost", proverUrl: "local", - noteTransportUrl: "http://localhost:57292", + noteTransportUrl: "http://ntl.localhost", autoSync: false, storeName: "miden-local-dev", }); @@ -140,7 +144,7 @@ export function LocalMidenApp({ children }: { children: ReactNode }) { config={{ rpcUrl: "localhost", prover: "local", - noteTransportUrl: "http://localhost:57292", + noteTransportUrl: "http://ntl.localhost", autoSyncInterval: 15_000, }} > @@ -157,8 +161,8 @@ If the frontend itself runs inside Docker, `localhost` is the frontend container The miden-client integration test binary uses the same local network preset: ```bash -git clone https://github.com/0xMiden/miden-client.git -cd miden-client +git clone --branch v0.16.0 --depth 1 https://github.com/0xMiden/rust-sdk.git miden-rust-sdk +cd miden-rust-sdk TEST_MIDEN_NETWORK=localhost \ cargo run --package miden-client-integration-tests --release --locked -- \ @@ -172,7 +176,6 @@ For broader local runs, use the same `TEST_MIDEN_NETWORK=localhost` environment - The node RPC server enables gRPC-web and CORS, so browser clients can call `http://localhost:57291` directly. - Do not proxy RPC as JSON. If your dev server or reverse proxy sits between the app and node, preserve gRPC-web requests and response headers. -- Private note transport is not part of the node Compose stack. Run the note transport service separately or import private notes out of band. - When switching between testnet, devnet, and localhost, use a different `storeName` or clear the browser IndexedDB database used by the SDK. ## Debug local failures @@ -180,7 +183,7 @@ For broader local runs, use the same `TEST_MIDEN_NETWORK=localhost` environment Start with the local logs: ```bash -make compose-logs +make local-network-logs ``` Then sync the client and inspect local transaction state: diff --git a/docs/builder/tools/clients/react-sdk/account-state-and-balances.md b/docs/builder/tools/clients/react-sdk/account-state-and-balances.md index f38b4b4f..d743d549 100644 --- a/docs/builder/tools/clients/react-sdk/account-state-and-balances.md +++ b/docs/builder/tools/clients/react-sdk/account-state-and-balances.md @@ -22,7 +22,7 @@ The important boundary is: - **`useExecuteProgram()`** runs locally and does not prove, submit, or change state. For the full package README and source-level examples, see -[`miden-client/packages/react-sdk/README.md`](https://github.com/0xMiden/miden-client/blob/v0.15.0/packages/react-sdk/README.md). +[`web-sdk/packages/react-sdk/README.md`](https://github.com/0xMiden/web-sdk/blob/v0.16.0/packages/react-sdk/README.md). ## Provider setup @@ -37,7 +37,7 @@ export function App() { config={{ rpcUrl: "testnet", prover: "testnet", - noteTransportUrl: "testnet", + noteTransportUrl: "https://transport.miden.io", autoSyncInterval: 15_000, }} loadingComponent={

Loading Miden...

} @@ -55,7 +55,7 @@ function WalletHome() { ## Resolve the active account -When an external signer is connected, `useMiden()` exposes `signerAccountId`. In local-keystore flows, pick the account from `useAccounts()` instead, usually from a user selection or the first wallet in the local store. +When an external signer is connected, `useMiden()` exposes `signerAccountId`. In local-keystore flows, pick the account from `useAccounts()` instead, usually from a user selection or the first account in the local store. ```tsx import { useMemo } from "react"; @@ -63,16 +63,16 @@ import { useAccounts, useMiden } from "@miden-sdk/react"; export function useActiveAccountId(selectedAccountId?: string): string | undefined { const { signerAccountId } = useMiden(); - const { wallets } = useAccounts(); + const { accounts } = useAccounts(); return useMemo( - () => selectedAccountId ?? signerAccountId ?? wallets[0]?.id().toString(), - [selectedAccountId, signerAccountId, wallets] + () => selectedAccountId ?? signerAccountId ?? accounts[0]?.id().toString(), + [selectedAccountId, signerAccountId, accounts] ); } ``` -If this returns `undefined`, the app has no connected signer and no local wallet yet. Render a connect/create-account state before calling transaction hooks. +If this returns `undefined`, the app has no connected signer and no local account yet. Render a connect/create-account state before calling transaction hooks. ## Render all fungible balances @@ -102,7 +102,7 @@ export function BalancePanel({ await refetch(); }; - if (!accountId) return

Connect or create a wallet to see balances.

; + if (!accountId) return

Connect or create an account to see balances.

; if (isLoading) return

Loading balances...

; if (error) return

{error.message}

; if (!account) return

Account not found in the local store.

; @@ -328,7 +328,7 @@ export function CounterRead({ ## Checklist - Wrap app code in `MidenProvider` before calling hooks. -- Use `signerAccountId` for external signer apps and `useAccounts()` for local wallet selection. +- Use `signerAccountId` for external signer apps and `useAccounts()` for local account selection. - Call `sync()` before user-visible reads that need fresh network state. - Read balances from `useAccount(accountId).assets` or `getBalance(faucetId)`. - After submitted transactions, wait for commit and sync again. diff --git a/docs/builder/tools/clients/react-sdk/advanced.md b/docs/builder/tools/clients/react-sdk/advanced.md index d450c3cd..cd75456a 100644 --- a/docs/builder/tools/clients/react-sdk/advanced.md +++ b/docs/builder/tools/clients/react-sdk/advanced.md @@ -5,7 +5,7 @@ sidebar_position: 5 # Advanced hooks -Hooks beyond the core send / mint / consume trio: custom scripts, MASM compilation, session wallets, store backup, note serialization, and sync control. +Hooks beyond the core send / mint / consume trio: custom scripts, anchored transaction previews, MASM compilation, session wallets, store backup, note serialization, and sync control. ## `useTransaction` @@ -43,9 +43,79 @@ await execute({ | `request` | `TransactionRequest` or `(client: WebClient) => TransactionRequest \| Promise` | | `skipSync` | Skip pre-send auto-sync (default `false`) | | `privateNoteTarget` | Deliver private output notes to this account after commit (any `AccountRef` form) | +| `anchor` | Execute against a reference block captured with `useChainAnchor` | The `privateNoteTarget` field is the 4-step pipeline shortcut: execute the tx, commit onchain, then auto-deliver the private note through the note transport to the target. Useful for "send private note" UIs where the recipient already has the React SDK running. +## `useChainAnchor` and `usePreview` + +Use these hooks when a transaction summary is proposed on one client and authorized or executed on another, such as multisig and offline co-signing flows. `useChainAnchor` pins the request to one reference block; `usePreview` derives the summary awaiting authorization at that block. + +Capture and preview in separate UI steps. `anchoredRequest` is React state, so it becomes available on the render after `captureAnchor()` completes: + +```tsx +import { useChainAnchor, usePreview, useTransaction } from "@miden-sdk/react"; +import type { TransactionRequest } from "@miden-sdk/miden-sdk"; + +type MultisigProposalProps = { + accountId: string; + buildRequest: () => TransactionRequest | Promise; + sendProposal: (anchor: Uint8Array, summary: Uint8Array) => Promise; +}; + +function MultisigProposal({ + accountId, + buildRequest, + sendProposal, +}: MultisigProposalProps) { + const { captureAnchor, anchor, anchoredRequest, isCapturing } = useChainAnchor(); + const { preview, isPreviewing } = usePreview(); + const { execute, isLoading } = useTransaction(); + + const capture = async () => { + await captureAnchor({ request: buildRequest }); + }; + + const previewAndShare = async () => { + if (!anchor || !anchoredRequest) return; + + const summary = await preview({ + accountId, + request: anchoredRequest, + anchor, + }); + await sendProposal(anchor.serialize(), summary.serialize()); + }; + + const executeAnchored = async () => { + if (!anchor || !anchoredRequest) return; + await execute({ accountId, request: anchoredRequest, anchor }); + }; + + return ( + <> + + + + + ); +} +``` + +`preview()` rejects with `TRANSACTION_ALREADY_AUTHORIZED` when the request needs no additional authorization; execute it directly in that case. A `ChainAnchor` owns a WASM allocation, so call `anchor.free()` when the proposal workflow no longer needs it. + ## `useExecuteProgram` View call — executes a transaction script locally and returns the stack output. No prove, no submit, no state change. Think of it as Miden's `eth_call`. @@ -83,6 +153,7 @@ const { component, txScript, noteScript, isReady } = useCompile(); // Account component const counterComponent = await component({ code: counterContractCode, + namespace: "external_contract::counter_contract", slots: [StorageSlot.emptyValue("miden::tutorials::counter")], }); @@ -90,13 +161,13 @@ const counterComponent = await component({ const script = await txScript({ code: ` use external_contract::counter_contract - begin + + @transaction_script + pub proc main call.counter_contract::increment_count end `, - libraries: [ - { namespace: "external_contract::counter_contract", code: counterContractCode }, - ], + libraries: [{ component: counterComponent }], }); // Note script — use the @note_script attribute on a library proc diff --git a/docs/builder/tools/clients/react-sdk/index.md b/docs/builder/tools/clients/react-sdk/index.md index 8359aacc..6a25cf78 100644 --- a/docs/builder/tools/clients/react-sdk/index.md +++ b/docs/builder/tools/clients/react-sdk/index.md @@ -29,11 +29,11 @@ You can always reach the underlying WASM client from any hook via `useMidenClien | [Query hooks](./query-hooks.md) | `useAccount(s)`, `useNotes`, `useNoteStream`, `useTransactionHistory`, `useSyncState`, `useAssetMetadata` | | [Account state and balances](./account-state-and-balances.md) | Active account selection, sync boundaries, balance rendering, and refresh-after-transaction patterns | | [Mutation hooks](./mutation-hooks.md) | `useCreateWallet`, `useCreateFaucet`, `useImportAccount`, `useSend`, `useMultiSend`, `useMint`, `useConsume`, `useSwap` | -| [Advanced hooks](./advanced.md) | `useTransaction`, `useExecuteProgram`, `useCompile`, `useSessionAccount`, `useExportStore`, `useImportStore`, `useImportNote`, `useExportNote`, `useSyncControl`, `useWaitForCommit`, `useWaitForNotes` | +| [Advanced hooks](./advanced.md) | `useTransaction`, `useChainAnchor`, `usePreview`, `useExecuteProgram`, `useCompile`, `useSessionAccount`, store and note import/export, sync control | | [External signers](./signers.md) | `MultiSignerProvider`, `SignerContext`, `useSigner`, `useMultiSigner` — pluggable wallet integrations (Para, Turnkey, MidenFi, custom) | | Utilities | `formatAssetAmount`, `parseAssetAmount`, `getNoteSummary`, `toBech32AccountId`, `createNoteAttachment` / `readNoteAttachment`, … | -Each hook exports its own result interface — not a generic `{ data, isLoading, error }` wrapper. Data lives in named fields (e.g. `accounts`, `wallets`, `records`, `wallet`, `faucet`). Transaction-producing mutations additionally expose a `stage` field that advances through `idle → executing → proving → submitting → complete`. See [setup](./setup.md#hook-result-conventions) for per-family details. +Each hook exports its own result interface — not a generic `{ data, isLoading, error }` wrapper. Data lives in named fields (e.g. `accounts`, `records`, `wallet`, `faucet`). Transaction-producing mutations additionally expose a `stage` field that advances through `idle → executing → proving → submitting → complete`. See [setup](./setup.md#hook-result-conventions) for per-family details. ## Where to go next diff --git a/docs/builder/tools/clients/react-sdk/query-hooks.md b/docs/builder/tools/clients/react-sdk/query-hooks.md index 2474f6b6..a72dccb9 100644 --- a/docs/builder/tools/clients/react-sdk/query-hooks.md +++ b/docs/builder/tools/clients/react-sdk/query-hooks.md @@ -9,24 +9,23 @@ Query hooks read from the local store (and trigger a fetch when the cache is col ## `useAccounts` -Lists every account tracked by the client, pre-categorised into wallets and faucets. +Lists every account header tracked by the client. Since protocol 0.15, an account ID/header no longer identifies whether the account is a wallet or faucet; inspect the full account's components when you need that distinction. ```tsx import { useAccounts } from "@miden-sdk/react"; function AccountList() { - const { accounts, wallets, faucets, isLoading, error } = useAccounts(); + const { accounts, isLoading, error } = useAccounts(); if (isLoading) return

Loading…

; if (error) return

{error.message}

; return ( <> -

Wallets ({wallets.length})

- {wallets.map((w) =>
{w.id().toString()}
)} - -

Faucets ({faucets.length})

- {faucets.map((f) =>
{f.id().toString()}
)} +

Accounts ({accounts.length})

+ {accounts.map((account) => ( +
{account.id().toString()}
+ ))} ); } @@ -37,8 +36,8 @@ Return type (`AccountsResult`): ```ts { accounts: AccountHeader[]; // every tracked account - wallets: AccountHeader[]; // regular accounts - faucets: AccountHeader[]; // token faucets + wallets: AccountHeader[]; // deprecated alias that mirrors accounts + faucets: AccountHeader[]; // deprecated; always empty isLoading: boolean; error: Error | null; refetch: () => Promise; diff --git a/docs/builder/tools/clients/react-sdk/recipes.md b/docs/builder/tools/clients/react-sdk/recipes.md index c6801a96..b679ed13 100644 --- a/docs/builder/tools/clients/react-sdk/recipes.md +++ b/docs/builder/tools/clients/react-sdk/recipes.md @@ -9,7 +9,7 @@ Short patterns covering the common cases. For longer walkthroughs — building a ## Show transaction progress -Every mutation hook exposes `isLoading` and `stage`; use them for optimistic UI: +`useSend()` exposes `isLoading` and `stage`; use them for optimistic UI: ```tsx import { useSend } from "@miden-sdk/react"; @@ -54,7 +54,7 @@ const amount = parseAssetAmount("0.01", 8); import { getNoteSummary, formatNoteSummary } from "@miden-sdk/react"; const summary = getNoteSummary(note); -const text = formatNoteSummary(summary); // "1.5 USDC" +const text = summary ? formatNoteSummary(summary) : "Unknown note"; ``` `noteSummaries` from `useNotes()` already runs `getNoteSummary` for you — these helpers are for ad-hoc formatting elsewhere. @@ -76,18 +76,23 @@ await waitForCommit(result.txId); ```tsx import { useMidenClient } from "@miden-sdk/react"; -function BlockHeaderPeek() { +function SyncHeightPeek() { const client = useMidenClient(); - const header = await client.getBlockHeaderByNumber(100); - // ... whatever the hooks don't expose + + const showSyncHeight = async () => { + const height = await client.getSyncHeight(); + console.log("Sync height:", height); + }; + + return ; } ``` -`useMidenClient()` throws if the provider isn't ready — guard with `useMiden().isReady` when you render before init. +`useMidenClient()` throws if the provider isn't ready. Render the component only after `useMiden().isReady`, or provide `MidenProvider`'s `loadingComponent`. -## Prevent race conditions +## Serialize a custom raw-client flow -Two user actions can fire in quick succession — a double-click on "Send", or a hook plus a manual button both wanting to sign. The React SDK exposes a lock: +When several raw-client calls must run as one provider-serialized flow, use `runExclusive`. Prevent repeated calls to a mutation hook with that hook's loading state instead. ```tsx import { useMiden, useMidenClient } from "@miden-sdk/react"; @@ -98,38 +103,36 @@ function CompoundFlow() { const run = () => runExclusive(async () => { - // Multiple client calls that must not interleave with other hooks' - // WASM work run here — the lock serialises them across the whole app. - await client.sync(); - // ... + await client.syncState(); + // ...other raw-client calls in the same flow }); return ; } ``` -`runExclusive(fn)` takes a zero-argument async function; reach for the client via `useMidenClient()` inside it. Built-in mutations already use this lock internally; `runExclusive` is the escape hatch for your own compound flows. +`runExclusive(fn)` takes a zero-argument async function. Built-in transaction hooks coordinate their own client calls, so don't wrap a hook such as `send()` in `runExclusive`; use it only for your own raw-client flow. -## Isolated clients for multi-wallet apps +## Separate stores for multiple signers `MidenProvider`'s config does not accept a `storeName` directly. Per-user isolation flows through the active signer: each `SignerContext.Provider` supplies its own `storeName` field, and `MidenProvider` reads that when initialising the underlying client. See the [Signers](./signers.md#custom-signer-providers) guide for a custom signer that picks a unique store name per connected user (typically the wallet address or a hash of it). -If you just need two wallets side-by-side in a dev environment and don't want to wire a signer, mount two separate `MidenProvider`s in isolated subtrees backed by different signer contexts. +For apps that switch between several signers, use [`MultiSignerProvider` and `SignerSlot`](./signers.md#multisignerprovider). `MidenProvider` switches to the store associated with the active signer. Don't mount multiple `MidenProvider`s expecting independent clients: the React SDK state store is shared. ## Account IDs — hex and bech32 interchangeably -Every hook accepts either: +Account ID parameters accept either format: ```tsx -// Both are valid -useAccount("0x1234567890abcdef"); -useAccount("mtst1qy35..."); +import { toBech32AccountId, useAccount } from "@miden-sdk/react"; -// Convert for display -account.bech32id(); // "mtst1qy35..." +// Both formats are accepted. +const byHex = useAccount(hexAccountId); +const byBech32 = useAccount(bech32Address); -import { toBech32AccountId } from "@miden-sdk/react"; -toBech32AccountId(someHexId); // "mtst1qy35..." +// Convert for display +byHex.account?.bech32id(); +toBech32AccountId(hexAccountId); ``` ## Troubleshooting @@ -141,7 +144,7 @@ toBech32AccountId(someHexId); // "mtst1qy35..." | Notes not appearing after mint | Call `sync()` from `useSyncState()` or verify `autoSyncInterval` isn't `0`. | | Bech32 address has wrong prefix | `rpcUrl` doesn't match the network you intended. `"testnet"` → `mtst1...`, `"devnet"` → `mdev1...`. | | WASM init fails in dev | Ensure your bundler serves `.wasm` with the `application/wasm` MIME type. Vite does this automatically; some custom setups don't. | -| `"A send is already in progress"` | Two `useSend` mutations fired simultaneously. Either `await` the previous call before starting the next, or use `runExclusive` to coordinate. | +| `"A send is already in progress"` | The same `useSend` instance received another call before the previous one completed. Disable the trigger with `isLoading` and `await` the previous call. | ## Next diff --git a/docs/builder/tools/clients/react-sdk/setup.md b/docs/builder/tools/clients/react-sdk/setup.md index df95b16e..8878063d 100644 --- a/docs/builder/tools/clients/react-sdk/setup.md +++ b/docs/builder/tools/clients/react-sdk/setup.md @@ -45,7 +45,7 @@ Every hook in the rest of this section assumes a `MidenProvider` is mounted some rpcUrl: "testnet", // "devnet" | "testnet" | "localhost" | custom URL prover: "testnet", // "local" | "devnet" | "testnet" | custom URL autoSyncInterval: 15_000, // ms; set to 0 to disable auto-sync - noteTransportUrl: "testnet", // optional; required for private notes + noteTransportUrl: "https://transport.miden.io", // optional; required for private notes }} loadingComponent={} // rendered while WASM boots errorComponent={} // rendered if init fails @@ -61,7 +61,7 @@ Every hook in the rest of this section assumes a `MidenProvider` is mounted some | `rpcUrl` | `"devnet" \| "testnet" \| "localhost" \| string` | Node RPC endpoint. Shorthands expand to hosted Miden endpoints; any other string is treated as a raw URL. | | `prover` | `"local" \| "devnet" \| "testnet" \| string \| ProverConfig` | Default prover. `"local"` runs in-browser. `ProverConfig` supports a `primary` + `fallback` pair if you want automatic fallback. | | `autoSyncInterval` | `number` | Milliseconds between automatic sync pulls. `0` disables the loop (you can still call `sync()` manually). Default: 15000. | -| `noteTransportUrl` | `"devnet" \| "testnet" \| string` | Note transport service. Required for `sendPrivate` / `fetchPrivate`. | +| `noteTransportUrl` | `string` | Full note transport service URL. Required for `sendPrivate` / `fetchPrivate`. | | `proverTimeoutMs` | `number` | Per-transaction prover timeout. | | `seed` | `Uint8Array` | 32-byte RNG seed for deterministic account-ID derivation in tests. | @@ -73,6 +73,8 @@ Every hook in the rest of this section assumes a `MidenProvider` is mounted some | `testnet` | Pre-production testing against the hosted Miden testnet | | `localhost` | Local node at `http://localhost:57291` | +`MidenProvider` expands the `rpcUrl` network shorthands but not `noteTransportUrl`. Pass the full transport URL (`https://transport.miden.io` for testnet or `https://transport.devnet.miden.io` for devnet). + ### `loadingComponent` and `errorComponent` - `loadingComponent` is rendered during the brief WASM load phase (first render only). @@ -101,17 +103,22 @@ function Status() { - `isInitializing` — `true` during the first load. - `error` — non-null if init failed. - `sync()` — trigger a manual sync pass outside the auto-sync loop. -- `runExclusive(fn: () => Promise): Promise` — serialize a block of async work under the internal lock. `fn` takes no arguments; reach for the client via `useMidenClient()` if you need one inside. See [race conditions](./recipes.md#prevent-race-conditions). +- `runExclusive(fn: () => Promise): Promise` — serialize a block of async work under the internal lock. `fn` takes no arguments; reach for the client via `useMidenClient()` if you need one inside. See [serialized raw-client flows](./recipes.md#serialize-a-custom-raw-client-flow). `useMidenClient()` is a shortcut that returns the ready `WebClient` directly, throwing if the provider isn't ready yet: ```tsx import { useMidenClient } from "@miden-sdk/react"; -function AdvancedCall() { +function LoadBlockHeaderButton() { const client = useMidenClient(); - const header = await client.getBlockHeaderByNumber(100); - // ... + + const loadHeader = async () => { + const header = await client.getBlockHeaderByNumber(100); + console.log("Block:", header.blockNum()); + }; + + return ; } ``` @@ -119,7 +126,7 @@ Use it for APIs the React SDK hooks don't expose. ## Hook result conventions -Each hook exports its own result interface — `UseSendResult`, `AccountsResult`, `NotesResult`, and so on — rather than a generic `QueryResult` wrapper. Data lives in named fields (e.g. `accounts`, `wallets`, `faucets`) not inside a common `data` key. The shared machinery is narrower than that: +Each hook exports its own result interface — `UseSendResult`, `AccountsResult`, `NotesResult`, and so on — rather than a generic `QueryResult` wrapper. Data lives in named fields (e.g. `accounts` and `records`) not inside a common `data` key. The shared machinery is narrower than that: ### Query hooks @@ -136,11 +143,11 @@ Every query hook exposes at least: Plus the hook-specific data fields. For example: ```tsx -const { wallets, faucets, isLoading, error, refetch } = useAccounts(); +const { accounts, isLoading, error, refetch } = useAccounts(); if (isLoading) return ; if (error) return

{error.message}

; -return ; +return ; ``` ### Mutation hooks diff --git a/docs/builder/tools/clients/react-sdk/signers.md b/docs/builder/tools/clients/react-sdk/signers.md index cf5072c9..f9ec134f 100644 --- a/docs/builder/tools/clients/react-sdk/signers.md +++ b/docs/builder/tools/clients/react-sdk/signers.md @@ -12,7 +12,7 @@ The React SDK treats signing as a pluggable contract: `MidenProvider` accepts an ### Para (EVM wallets) ```tsx -import { ParaSignerProvider } from "@miden-sdk/para"; +import { ParaSignerProvider } from "@miden-sdk/use-miden-para-react"; import { MidenProvider } from "@miden-sdk/react"; function App() { @@ -29,7 +29,7 @@ function App() { Expose Para-specific data inside your app: ```tsx -import { useParaSigner } from "@miden-sdk/para"; +import { useParaSigner } from "@miden-sdk/use-miden-para-react"; const { para, wallet, isConnected } = useParaSigner(); ``` @@ -38,23 +38,26 @@ const { para, wallet, isConnected } = useParaSigner(); ```tsx import { TurnkeySignerProvider } from "@miden-sdk/miden-turnkey-react"; +import { MidenProvider } from "@miden-sdk/react"; -// Config is optional — defaults to https://api.turnkey.com and reads -// VITE_TURNKEY_ORG_ID from the environment. - +// defaultOrganizationId is required. The API URL defaults to +// https://api.turnkey.com. + -// Or with explicit config: +// Or with an explicit API URL: - ... + + + ``` @@ -84,9 +87,10 @@ function ConnectButton() { ### MidenFi wallet adapter ```tsx -import { MidenFiSignerProvider } from "@miden-sdk/wallet-adapter-react"; +import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react"; +import { MidenProvider } from "@miden-sdk/react"; - + @@ -116,7 +120,11 @@ function Header() { For a signing service that doesn't have a prebuilt provider — internal HSM, hardware wallet, or experimental integration — wire `SignerContext` directly: ```tsx -import { SignerContext, type SignerContextValue } from "@miden-sdk/react"; +import { + MidenProvider, + SignerContext, + type SignerContextValue, +} from "@miden-sdk/react"; import { AccountStorageMode } from "@miden-sdk/miden-sdk"; const signer: SignerContextValue = { @@ -180,14 +188,18 @@ For apps that need to swap between multiple signer providers at runtime (e.g. "c ```tsx import { MultiSignerProvider, SignerSlot, MidenProvider } from "@miden-sdk/react"; +import { ParaSignerProvider } from "@miden-sdk/use-miden-para-react"; +import { TurnkeySignerProvider } from "@miden-sdk/miden-turnkey-react"; function App() { return ( - + - + diff --git a/docs/builder/tools/clients/web-client/accounts.md b/docs/builder/tools/clients/web-client/accounts.md index 094b6257..fb6b7364 100644 --- a/docs/builder/tools/clients/web-client/accounts.md +++ b/docs/builder/tools/clients/web-client/accounts.md @@ -16,9 +16,9 @@ Account creation uses a few small option values. Wallets are the default shape; | faucet `type` field | `0` \| `1` | Fungible or non-fungible faucet selector | | `auth` field | `"falcon"` \| `"ecdsa"` | Signing scheme — Falcon is the default | | `storage` field | `"public"` \| `"private"` | Account visibility mode | -| low-level `AccountType` | `AccountType.Public` \| `AccountType.Private` | WASM builder visibility flag | +| low-level `AccountStorageMode` | `AccountStorageMode.public()` \| `AccountStorageMode.private()` | WASM builder visibility flag | -The v0.15 protocol no longer encodes wallet/faucet/contract role or mutability in the low-level `AccountType`; role comes from the create options and attached components. +The protocol does not encode wallet/faucet/contract role or mutability in the account ID; role comes from the create options and attached components. ## Create @@ -95,11 +95,13 @@ const counterCode = ` const COUNTER_SLOT = word("miden::tutorials::counter") + @account_procedure pub proc get_count push.COUNTER_SLOT[0..2] exec.active_account::get_item exec.sys::truncate_stack end + @account_procedure pub proc increment_count push.COUNTER_SLOT[0..2] exec.active_account::get_item add.1 @@ -143,7 +145,7 @@ import { MidenClient, AccountBuilder, AccountComponent, - AccountType, + AccountStorageMode, } from "@miden-sdk/miden-sdk"; const client = await MidenClient.createTestnet(); @@ -155,7 +157,7 @@ const account = new AccountBuilder(seed) .withAuthComponent( AccountComponent.createAuthComponentFromCommitment(commitment, 1), ) - .accountType(AccountType.Public) + .storageMode(AccountStorageMode.public()) .withBasicWalletComponent() .build().account; @@ -240,7 +242,7 @@ await client.accounts.addAddress("0xACCOUNT...", "mtst1address..."); await client.accounts.removeAddress("0xACCOUNT...", "mtst1address..."); ``` -Associates one or more bech32 addresses with an account. Useful when your UI lets users alias accounts by a human-readable string. +Associates valid Miden bech32 addresses with an account. The address is a protocol value, not an arbitrary UI alias. ## Import @@ -279,8 +281,7 @@ const accountFile = await client.accounts.export("0x1234..."); ## Error behaviour - `get()` returns `null` when the account is not in the local store. -- `getDetails()`, `getBalance()`, and `export()` throw `"Account not found: 0x..."` when the account is missing. -- `import({ seed, ... })` on a private account throws at resolve time — private state isn't recoverable from a seed. +- `getDetails()`, `getBalance()`, and `export()` throw when the account is missing. ## Next diff --git a/docs/builder/tools/clients/web-client/compile.md b/docs/builder/tools/clients/web-client/compile.md index 3e96b866..953117e8 100644 --- a/docs/builder/tools/clients/web-client/compile.md +++ b/docs/builder/tools/clients/web-client/compile.md @@ -9,7 +9,7 @@ sidebar_position: 6 | Method | Produces | Used by | | --- | --- | --- | -| `client.compile.component({ code, slots?, supportAllTypes? })` | `AccountComponent` | [`accounts.create({ components: [...] })`](./accounts.md#contract) | +| `client.compile.component({ code, namespace?, slots?, supportAllTypes? })` | `AccountComponent` | [`accounts.create({ components: [...] })`](./accounts.md#contract) | | `client.compile.txScript({ code, libraries? })` | `TransactionScript` | [`transactions.execute({ script })`](./transactions.md#custom-transaction-scripts-execute) | | `client.compile.noteScript({ code, libraries? })` | `NoteScript` | `Note` construction utilities | @@ -30,11 +30,13 @@ const contractCode = ` const COUNTER_SLOT = word("miden::tutorials::counter") + @account_procedure pub proc get_count push.COUNTER_SLOT[0..2] exec.active_account::get_item exec.sys::truncate_stack end + @account_procedure pub proc increment_count push.COUNTER_SLOT[0..2] exec.active_account::get_item add.1 @@ -45,6 +47,7 @@ const contractCode = ` const component = await client.compile.component({ code: contractCode, + namespace: "external_contract::counter_contract", slots: [StorageSlot.emptyValue("miden::tutorials::counter")], }); @@ -56,8 +59,9 @@ console.log("get_count hash:", getCountHash); Options: - `code` — the MASM source for the component. +- `namespace` — module path used to derive procedure identities. Reuse it when rebuilding the source as an inline library; linking `{ component }` preserves the exact compiled identity. - `slots` — initial storage slots. Use the `StorageSlot` helpers (`emptyValue`, etc.). -- `supportAllTypes` — defaults to `true`. When `true`, the compiler auto-injects an auth-kernel invocation so the component accepts the standard set of input types for authenticated transactions. Set to `false` if your component already invokes an auth kernel procedure itself, or intentionally omits one. +- `supportAllTypes` — defaults to `true` and calls `withSupportsAllTypes()` for compatibility. In 0.16, components already apply to every account type; this option does not inject an auth-kernel invocation. ## Transaction scripts @@ -69,7 +73,9 @@ A script with no `libraries` entry can only reference procedures that exist in t const script = await client.compile.txScript({ code: ` use miden::core::sys - begin + + @transaction_script + pub proc main push.0 exec.sys::truncate_stack end @@ -77,7 +83,7 @@ const script = await client.compile.txScript({ }); ``` -If your script needs to call into an external contract (as in the FPI section below), you must pass that contract's code through `libraries` — the compiler only links what you explicitly provide. +If your script needs to call into an external contract (as in the FPI section below), pass either the exact compiled component or its source through `libraries` — the compiler only links what you explicitly provide. ### With inline libraries @@ -88,7 +94,9 @@ const script = await client.compile.txScript({ code: ` use external_contract::my_contract use miden::core::sys - begin + + @transaction_script + pub proc main call.my_contract::do_something exec.sys::truncate_stack end @@ -103,7 +111,7 @@ const script = await client.compile.txScript({ }); ``` -Each library takes: +Each inline library takes: | Field | Required | Description | | --- | --- | --- | @@ -111,6 +119,8 @@ Each library takes: | `code` | yes | MASM source. | | `linking` | no | `Linking.Dynamic` (default) or `Linking.Static`. `"dynamic"` / `"static"` string literals are also accepted. | +`libraries` also accepts `{ component, linking? }`, which links the exact code installed by an `AccountComponent`, or a pre-built `Library`. Prefer the component form when a script calls a component installed on an account. + ### Linking modes | Value | Behaviour | When to use | @@ -128,7 +138,8 @@ const noteScript = await client.compile.noteScript({ use miden::protocol::active_note use miden::core::sys - begin + @note_script + pub proc main # Runs when the consuming account redeems this note. # Real note scripts inspect note storage, assets, and account state # using procedures from miden::protocol::active_note. @@ -138,7 +149,7 @@ const noteScript = await client.compile.noteScript({ }); ``` -Libraries follow the same `{ namespace, code, linking? }` shape as transaction scripts. +Libraries accept the same inline `{ namespace, code, linking? }`, compiled `{ component, linking? }`, and pre-built `Library` forms as transaction scripts. ## Procedure hashes (for FPI) @@ -147,6 +158,7 @@ Foreign procedure invocation requires the **hash** of the target procedure. Extr ```typescript const component = await client.compile.component({ code: counterContractCode, + namespace: "external_contract::counter_contract", slots: [StorageSlot.emptyValue("miden::tutorials::counter")], }); @@ -156,7 +168,9 @@ const script = await client.compile.txScript({ code: ` use external_contract::count_reader_contract use miden::core::sys - begin + + @transaction_script + pub proc main push.${getCountHash} push.${counterAccountId.suffix()} push.${counterAccountId.prefix()} @@ -185,6 +199,7 @@ await client.sync(); // 1. Compile the contract component const component = await client.compile.component({ code: counterCode, + namespace: "external_contract::counter_contract", slots: [StorageSlot.emptyValue("miden::tutorials::counter")], }); @@ -204,13 +219,14 @@ await client.sync(); const script = await client.compile.txScript({ code: ` use external_contract::counter_contract - begin + + @transaction_script + pub proc main call.counter_contract::increment_count end `, - libraries: [ - { namespace: "external_contract::counter_contract", code: counterCode }, - ], + // Link the exact component installed on the account so procedure identities match. + libraries: [{ component }], }); // 4. Execute diff --git a/docs/builder/tools/clients/web-client/index.md b/docs/builder/tools/clients/web-client/index.md index 0d0b2229..5646de8a 100644 --- a/docs/builder/tools/clients/web-client/index.md +++ b/docs/builder/tools/clients/web-client/index.md @@ -5,7 +5,7 @@ sidebar_position: 1 # Web SDK (@miden-sdk/miden-sdk) -The Web SDK is the browser-focused toolkit for the Miden network. It wraps the Rust client, compiles to WebAssembly, and exposes a typed JavaScript API through the `MidenClient` class. Use it from web apps, wallets, dApps, service workers, Node servers — any JavaScript runtime that supports Web Workers and WebAssembly. +The Web SDK is the JavaScript toolkit for the Miden network. In browsers, it wraps the Rust client as WebAssembly and exposes a typed API through the `MidenClient` class for web apps, wallets, dApps, and worker contexts. The same package provides a native Node.js entry backed by N-API and SQLite. ## Capabilities @@ -30,16 +30,16 @@ The Web SDK is the browser-focused toolkit for the Miden network. It wraps the R │ │ │ │ └─ wraps WasmWebClient (Rust → WASM) │ │ │ -│ Runs prove / execute on a dedicated │ -│ Web Worker to keep the main thread responsive │ +│ Browser default: prove / execute on a │ +│ dedicated Web Worker │ └────────────────────────────────────────────────┘ ``` -The SDK is built from the `web-client` Rust crate in [0xMiden/miden-client](https://github.com/0xMiden/miden-client), compiled with `wasm-bindgen`, and bundled with the WASM module, JavaScript bindings, and a dedicated Web Worker script. +The browser build comes from the `web-client` Rust crate in [0xMiden/web-sdk](https://github.com/0xMiden/web-sdk), compiled with `wasm-bindgen`, and bundled with the WASM module, JavaScript bindings, and a dedicated Web Worker script. Under Node.js, the package selects its native N-API binding and SQLite storage instead. ## Resource management -Each `MidenClient` instance holds a dedicated Web Worker thread. When you no longer need a client — for example in a multi-wallet app that creates one client per active network — call `client.terminate()` to release the worker. +In browsers, each `MidenClient` created with the default `useWorker: true` setting holds a dedicated Web Worker thread. When you no longer need a client — for example in a multi-wallet app that creates one client per active network — call `client.terminate()` to release its underlying resources. Node.js clients and browser clients created with `useWorker: false` do not allocate this worker, but should still be terminated when finished. ```typescript import { MidenClient } from "@miden-sdk/miden-sdk"; @@ -48,7 +48,7 @@ const client = await MidenClient.createTestnet(); // ... use the client ... -// Free the Web Worker when you are done +// Release client resources when you are done client.terminate(); ``` @@ -61,7 +61,7 @@ In environments that support the TC39 [explicit resource management](https://git } ``` -After `terminate()`, every subsequent method call throws `Error("Client terminated")`. +After `terminate()`, subsequent client operations throw `Error("Client terminated")`. ## Where to go next diff --git a/docs/builder/tools/clients/web-client/notes.md b/docs/builder/tools/clients/web-client/notes.md index 6c1279bf..045d3e5d 100644 --- a/docs/builder/tools/clients/web-client/notes.md +++ b/docs/builder/tools/clients/web-client/notes.md @@ -80,8 +80,8 @@ import { MidenClient, NoteExportFormat } from "@miden-sdk/miden-sdk"; const client = await MidenClient.createTestnet(); // Import from a previously exported NoteFile -const noteId = await client.notes.import(noteFile); -console.log("Imported:", noteId); +const importedRef = await client.notes.import(noteFile); +console.log("Imported:", importedRef); // Export — formats differ in completeness const idOnly = await client.notes.export("0xnote...", { format: NoteExportFormat.Id }); @@ -89,34 +89,45 @@ const full = await client.notes.export("0xnote...", { format: NoteExportForma const details = await client.notes.export("0xnote...", { format: NoteExportFormat.Details }); ``` +`import()` returns a note ID as a hex string when the file includes one, or the details commitment for a `Details` file. + `NoteExportFormat`: -- **`Id`** — just the note ID. Only works for public notes. +- **`Id`** — just the note ID. A recipient can import it only for a public note. - **`Full`** — complete note data plus inclusion proof. Requires the note to have an onchain inclusion proof. -- **`Details`** — note ID, metadata, and creation block. +- **`Details`** — assets and recipient plus a sync hint containing the tag and after-block number. Metadata and attachments are recovered from the chain. ## Note transport (private notes) Private notes are delivered through the Miden note transport service. The sender emits a note with `type: "private"`; the recipient fetches it from the transport network. ```typescript -// Send a private note +// Relay an arbitrary private note. You can also pass an input note ID or +// record tracked by this client. await client.notes.sendPrivate({ - note: "0xnote...", // NoteInput - to: "mtst1recipient...", // recipient AccountRef + note: privateNote, + to: "mtst1recipient...", + scanAfterBlockNum, // chain tip recorded when the transaction was submitted }); -// Fetch — default is incremental (paginated) -await client.notes.fetchPrivate(); +// For an applied output note created by this client, let the SDK derive the +// scan-start block from its stored expected height. +await client.notes.sendPrivateOutput({ + noteId: "0xnote...", + to: "mtst1recipient...", +}); -// Or fetch everything at once (initial-setup scenarios) -await client.notes.fetchPrivate({ mode: "all" }); +// On the client that tracks the recipient, fetch incrementally from the +// stored transport cursor. +await recipientClient.notes.fetchPrivate(); // Now inspect the inbox -const notes = await client.notes.list(); -console.log(`Fetched ${notes.length} notes`); +const notes = await recipientClient.notes.list(); +console.log(`Tracked ${notes.length} notes`); ``` +`scanAfterBlockNum` must be at or below the note's commitment block. A value above it is never scanned backward and can silently prevent delivery. `sendPrivateOutput()` avoids that footgun for applied output notes created by the same client. Newly tracked tags are backfilled by `client.sync()`; `fetchPrivate({ mode: "all" })` is no longer available. + You need a note transport endpoint configured on the client — set `noteTransportUrl` in `ClientOptions`, or use a network factory (`createTestnet`, `createDevnet`) that preconfigures it. ## Tags diff --git a/docs/builder/tools/clients/web-client/setup.md b/docs/builder/tools/clients/web-client/setup.md index 4f1f3364..bdc1f41b 100644 --- a/docs/builder/tools/clients/web-client/setup.md +++ b/docs/builder/tools/clients/web-client/setup.md @@ -17,7 +17,7 @@ yarn add @miden-sdk/miden-sdk pnpm add @miden-sdk/miden-sdk ``` -The SDK targets modern browsers (Chrome, Firefox, Safari, Edge) with WebAssembly and Web Worker support. It also runs under Node 20+ when the host provides those primitives. +The SDK targets modern browsers (Chrome, Firefox, Safari, Edge). The browser build uses WebAssembly and a Web Worker when available. Under Node 20+, the package automatically selects its native N-API binding with SQLite-backed storage. ## Create a client @@ -27,7 +27,7 @@ Every operation goes through a `MidenClient` instance. Four factories cover the | --- | --- | | `MidenClient.createTestnet()` | Miden testnet — RPC, prover, and note transport preconfigured | | `MidenClient.createDevnet()` | Miden devnet — same shape, devnet endpoints | -| `MidenClient.createMock()` | Deterministic in-memory chain for tests — no network | +| `MidenClient.createMock()` | Deterministic local mock chain for tests — no network | | `MidenClient.create({ ... })` | Custom endpoints (localhost, self-hosted node, or any shorthand) | ```typescript @@ -48,11 +48,11 @@ const custom = await MidenClient.create({ const mock = await MidenClient.createMock(); ``` -All factories are async — the SDK has to load its WebAssembly module and spin up a Web Worker before the client is usable. +All factories are async because they initialize the platform runtime and client storage before the client is usable. ## `ClientOptions` reference -All four factories accept the same `ClientOptions` shape. The differences are in what each factory pre-fills before the options are applied. +`createTestnet()`, `createDevnet()`, and `create()` accept the same `ClientOptions` shape. The differences are in what each factory pre-fills before the options are applied. `createMock()` accepts a separate `MockOptions` shape for configuring its local mock chain. ### Field reference @@ -60,11 +60,12 @@ All four factories accept the same `ClientOptions` shape. The differences are in | --- | --- | --- | | `rpcUrl` | `"testnet" \| "devnet" \| "localhost" \| "local" \| string` | Node RPC endpoint. Shorthands expand to the hosted Miden endpoints; any other string is treated as a raw URL. | | `noteTransportUrl` | `"testnet" \| "devnet" \| string` | Note transport service endpoint. Required for private-note `sendPrivate` / `fetchPrivate`. | -| `proverUrl` | `"local" \| "devnet" \| "testnet" \| string` | Default prover for transactions. `"local"` runs in the browser; remote shorthands and URLs route to a remote / delegated prover. | +| `proverUrl` | `"local" \| "devnet" \| "testnet" \| string` | Default prover for transactions. `"local"` runs in the current environment; remote shorthands and URLs route to a remote / delegated prover. | | `autoSync` | `boolean` | When `true`, the client runs one sync pass before the promise resolves. | | `seed` | `string \| Uint8Array` | Seed for deterministic RNG. Strings are hashed to 32 bytes via SHA-256. | | `storeName` | `string` | Store isolation key (IndexedDB database name in browsers). Set this to keep multiple clients' data separate in the same origin. | -| `keystore` | `{ getKey, insertKey, sign }` | External keystore callbacks. Leave unset to use the built-in keystore. | +| `keystore` | `{ getKey, insertKey, sign }` | Browser-only external keystore callbacks. Leave unset to use the built-in keystore; Node uses its filesystem keystore. | +| `useWorker` | `boolean` | Browser-only. Defaults to `true`; set it to `false` for callback provers or single-WebView native shells. | ### Factory defaults @@ -76,9 +77,8 @@ Any option not passed falls back to the factory default, then to an SDK default. | `createDevnet(opts?)` | `"devnet"` | `"devnet"` | `"devnet"` | `true` | | `create(opts?)` **with** `rpcUrl` | your value | `"local"` | _none_ | `false` | | `create(opts?)` **without** `rpcUrl` | _delegates to `createTestnet(opts)`_ | ← | ← | ← | -| `createMock(opts?)` | _(no network)_ | _(dummy proving)_ | _(in-memory)_ | _(manual)_ | -`create()` without an `rpcUrl` is not a separate "custom" client — it forwards its options to `createTestnet()`. If you want a no-prover, no-autosync client against localhost, pass `rpcUrl: "localhost"` explicitly. +`create()` without an `rpcUrl` is not a separate "custom" client — it forwards its options to `createTestnet()`. If you want a localhost client with local proving and no autosync, pass `rpcUrl: "localhost"` explicitly. ### Testnet with an in-browser prover @@ -110,9 +110,9 @@ const seed = crypto.getRandomValues(new Uint8Array(32)); const auth = AuthSecretKey.rpoFalconWithRNG(seed); ``` -The caller is responsible for retaining `auth` as long as the account is in use: the client holds a reference for signing, but the secret material only exists on the caller side until it is handed to the keystore. +Passing `auth` to `client.accounts.create()` stores it in the configured keystore for later signing. -See [Accounts](./accounts.md) for full examples covering wallets, contracts, and faucets. For advanced setups — external signers, hardware wallets — the `keystore` option on `ClientOptions` wires the SDK to your own `sign`/`getKey`/`insertKey` callbacks. +See [Accounts](./accounts.md) for full examples covering wallets, contracts, and faucets. For advanced browser setups — external signers, hardware wallets — the `keystore` option on `ClientOptions` wires the SDK to your own `sign`/`getKey`/`insertKey` callbacks. ## Remote provers and per-transaction overrides @@ -144,7 +144,6 @@ import { MidenClient } from "@miden-sdk/miden-sdk"; async function demo() { const client = await MidenClient.createTestnet(); - await client.sync(); const wallet = await client.accounts.create(); console.log("Wallet:", wallet.id().toString()); diff --git a/docs/builder/tools/clients/web-client/sync.md b/docs/builder/tools/clients/web-client/sync.md index 77b2ecfb..85611e40 100644 --- a/docs/builder/tools/clients/web-client/sync.md +++ b/docs/builder/tools/clients/web-client/sync.md @@ -9,7 +9,7 @@ Every operation the Web SDK performs reads from — or writes to — a local sto ## `client.sync()` -Pulls updates from the Miden node and applies them to the local store. Returns a `SyncSummary` describing what changed. +Fetches private notes from the Note Transport Layer, then pulls onchain updates from the Miden node and applies them to the local store. Returns a `SyncSummary` describing what changed. ```typescript import { MidenClient } from "@miden-sdk/miden-sdk"; diff --git a/docs/builder/tools/clients/web-client/testing.md b/docs/builder/tools/clients/web-client/testing.md index ca0f6afe..e38397a7 100644 --- a/docs/builder/tools/clients/web-client/testing.md +++ b/docs/builder/tools/clients/web-client/testing.md @@ -44,7 +44,7 @@ const faucet = await client.accounts.create({ maxSupply: 10_000_000n, }); -client.proveBlock(); +await client.proveBlock(); await client.sync(); await client.transactions.mint({ @@ -53,17 +53,17 @@ await client.transactions.mint({ amount: 1000n, }); -client.proveBlock(); +await client.proveBlock(); await client.sync(); const result = await client.transactions.consumeAll({ account: wallet }); console.log(`Consumed ${result.consumed} notes`); -client.proveBlock(); +await client.proveBlock(); await client.sync(); const balance = await client.accounts.getBalance(wallet, faucet); -console.log(`Balance: ${balance}`); // 1000n +console.log(`Balance: ${balance}`); // Balance: 1000 ``` ## Dummy proving @@ -80,7 +80,7 @@ The `MidenClient` class exposes a few methods that only make sense on mock clien ```typescript if (client.usesMockChain()) { - client.proveBlock(); + await client.proveBlock(); } const chainBytes = await client.serializeMockChain(); @@ -118,17 +118,38 @@ const client = await MidenClient.createMock({ The mock client ships its own in-memory note transport. The same `sendPrivate` / `fetchPrivate` flow works: ```typescript -import { MidenClient } from "@miden-sdk/miden-sdk"; +import { + createP2IDNote, + MidenClient, + type AccountTypeValue, +} from "@miden-sdk/miden-sdk"; + +const FUNGIBLE_FAUCET: AccountTypeValue = 0; const client = await MidenClient.createMock(); +const recipient = await client.accounts.create(); +const faucet = await client.accounts.create({ + type: FUNGIBLE_FAUCET, + symbol: "TEST", + decimals: 8, + maxSupply: 10_000_000n, +}); + +const note = createP2IDNote({ + from: faucet, + to: recipient, + assets: { token: faucet, amount: 1n }, + type: "private", +}); await client.notes.sendPrivate({ - note: "0xnote...", - to: "mtst1recipient...", + note, + to: recipient, + scanAfterBlockNum: 0, }); await client.notes.fetchPrivate(); const notes = await client.notes.list(); -console.log(`Received ${notes.length} notes`); +console.log(`Received ${notes.length} notes`); // Received 1 notes ``` diff --git a/docs/builder/tools/clients/web-client/transactions.md b/docs/builder/tools/clients/web-client/transactions.md index bf0d0410..bd765665 100644 --- a/docs/builder/tools/clients/web-client/transactions.md +++ b/docs/builder/tools/clients/web-client/transactions.md @@ -5,7 +5,7 @@ sidebar_position: 4 # Transactions -`client.transactions` is the resource namespace for everything that mutates onchain state: sending, minting, consuming, swapping, running custom scripts, and inspecting history. Every mutation method handles the full lifecycle — execute, prove, submit — in one call. +`client.transactions` is the resource namespace for sending, minting, consuming, swapping, running custom scripts, and inspecting transaction history. The simplified operations below handle the full lifecycle — execute, prove, submit — in one call. ## Simplified operations @@ -134,21 +134,67 @@ await client.transactions.waitFor(txId.toHex(), { `waitFor` throws on rejection or timeout. -## Preview (dry run) +## Preview transactions awaiting authorization -Run any of the simplified operations as a dry-run to inspect its effects without submitting to the network. The return type is a `TransactionSummary`. +`preview()` derives the `TransactionSummary` that an account is being asked to authorize without proving or submitting the transaction. It only returns a summary when authorization is still pending, such as a multisig request that has not reached its threshold: ```typescript const summary = await client.transactions.preview({ operation: "send", - account: wallet, + account: multisigAccount, to: recipient, token: faucet, amount: 100n, }); ``` -`operation` accepts `"send"`, `"mint"`, `"consume"`, and `"swap"`. +If the account already authorizes the request, execution succeeds without producing a pending summary and `preview()` rejects with `TRANSACTION_ALREADY_AUTHORIZED`. Submit that transaction normally instead. The built-in preview operations follow the same rule. Use `operation: "custom"` when previewing a pre-built `TransactionRequest`, as in the cross-client flow below. + +### Keep a cross-client summary reproducible + +A transaction summary commits to its reference block. When one client proposes a transaction and another verifies or executes it later, capture a `ChainAnchor` and send it alongside the summary so every participant derives the transaction at the same block: + +```typescript +import { ChainAnchor, TransactionSummary } from "@miden-sdk/miden-sdk"; + +// Proposer: capture the request's reference block and derive the summary there. +const anchor = await client.transactions.captureAnchor(request); +const summary = await client.transactions.preview({ + operation: "custom", + account: multisigAccount, + request, + anchor, +}); + +const anchorBytes = anchor.serialize(); +const summaryBytes = summary.serialize(); +await sendProposal(anchorBytes, summaryBytes); + +// Co-signer or executor: restore the proposal and re-derive it at the same block. +const receivedAnchor = ChainAnchor.deserialize(anchorBytes); +const proposedSummary = TransactionSummary.deserialize(summaryBytes); +const derivedSummary = await client.transactions.preview({ + operation: "custom", + account: multisigAccount, + request, + anchor: receivedAnchor, +}); + +if (derivedSummary.toCommitment().toHex() !== proposedSummary.toCommitment().toHex()) { + throw new Error("The request does not match the proposed summary"); +} + +if (receivedAnchor.commitment().toHex() !== proposedSummary.blockCommitment().toHex()) { + throw new Error("The anchor does not match the proposed summary"); +} + +// After collecting the required authorization, replay at the anchored block. +await client.transactions.submit(multisigAccount, request, { anchor: receivedAnchor }); +receivedAnchor.free(); +anchor.free(); +``` + +An anchor makes the request reproducible; it does not prove that the transaction matches the signer's intent. Inspect the summary's account delta and input/output notes before signing. Also verify a received anchor's block against a trusted node when the proposer is not trusted. If `summary.expirationDelta()` is non-zero, the transaction expires at `anchor.blockNum() + summary.expirationDelta()`; if that deadline passes, capture a new anchor and collect authorization again. ## Custom transaction scripts (`execute`) @@ -162,16 +208,14 @@ const client = await MidenClient.createTestnet(); const script = await client.compile.txScript({ code: ` use external_contract::counter_contract - begin + + @transaction_script + pub proc main call.counter_contract::increment_count end `, - libraries: [ - { - namespace: "external_contract::counter_contract", - code: counterContractCode, - }, - ], + // Reuse the component installed on contractAccount. + libraries: [{ component: counterComponent }], }); const { txId } = await client.transactions.execute({ @@ -194,6 +238,7 @@ const client = await MidenClient.createTestnet(); // Compile the foreign contract to get a procedure hash const counterComponent = await client.compile.component({ code: counterContractCode, + namespace: "external_contract::counter_contract", slots: [StorageSlot.emptyValue("miden::tutorials::counter")], }); const getCountHash = counterComponent.getProcedureHash("get_count"); @@ -202,26 +247,28 @@ const script = await client.compile.txScript({ code: ` use external_contract::count_reader_contract use miden::core::sys - begin + + @transaction_script + pub proc main + padw padw padw padw push.${getCountHash} - push.${counterAccount.id().suffix()} push.${counterAccount.id().prefix()} + push.${counterAccount.id().suffix()} call.count_reader_contract::copy_count exec.sys::truncate_stack end `, - libraries: [ - { namespace: "external_contract::count_reader_contract", code: countReaderCode }, - ], + // Reuse the component installed on countReaderAccount. + libraries: [{ component: countReaderComponent }], }); const { txId } = await client.transactions.execute({ account: countReaderAccount.id(), script, foreignAccounts: [ - // Bare reference — client fetches storage requirements automatically + // A bare reference is sufficient here because get_count reads a value slot. counterAccount.id(), - // Or with explicit storage requirements: + // Storage-map entries require explicit storage requirements: // { id: counterAccount.id(), storage: requirements }, ], }); @@ -235,23 +282,23 @@ const { txId } = await client.transactions.execute({ const script = await client.compile.txScript({ code: ` use external_contract::counter_contract - begin + + @transaction_script + pub proc main call.counter_contract::get_count end `, - libraries: [ - { namespace: "external_contract::counter_contract", code: counterContractCode }, - ], + libraries: [{ component: counterComponent }], }); const stack = await client.transactions.executeProgram({ account: contractAccount.id(), script, - foreignAccounts: [counterAccount.id()], }); -// stack is a FeltArray — 16 elements representing the final stack -const count = stack.get(0).asInt(); +// Node.js returns Felt[]; browser WASM returns the FeltArray wrapper. +const first = Array.isArray(stack) ? stack[0] : stack.get(0); +const count = first.asInt(); console.log("Count:", count); ``` @@ -266,9 +313,9 @@ Options: ## Manual `TransactionRequest` -For full control over note inputs and outputs — e.g. emitting multiple custom output notes from one transaction — build a `TransactionRequest` yourself and pass it to `submit`. +For full control over note inputs and outputs — e.g. emitting multiple output notes from one transaction — build a `TransactionRequest` yourself and pass it to `submit`. -The builder accepts WASM array classes (`NoteArray`, `NoteDetailsAndTagArray`, `NoteRecipientArray`) rather than plain TS arrays. This is unusual but required by the underlying wasm-bindgen interface: the array types take ownership of their elements and are explicitly disposable. +The builder accepts WASM array classes (`NoteArray`, `NoteDetailsAndTagArray`, `NoteRecipientArray`) rather than plain TypeScript arrays. ```typescript import { @@ -286,7 +333,6 @@ for (const note of outputNotes) { } const request = new TransactionRequestBuilder() - .withCustomScript(transactionScript) .withOwnOutputNotes(ownOutputs) .build(); @@ -304,13 +350,13 @@ Expected-note hints are also available: ```typescript const request = new TransactionRequestBuilder() .withOwnOutputNotes(ownOutputs) - .withExpirationDelta(10) // expires 10 blocks after submission + .withExpirationDelta(10) // expires 10 blocks after the reference block .build(); await client.transactions.submit(wallet, request); ``` -`withExpirationDelta()` composes with `withCustomScript()` — the builder applies the expiration at the request level regardless of how the script was provided. You can still set expiration inside the script itself when you need a different rule; the two paths don't interact. +`withExpirationDelta()` cannot be combined with `withCustomScript()`; `build()` rejects that combination. A custom script must set its expiration through the transaction context instead. ## Remote proving @@ -368,7 +414,7 @@ for (const tx of all) { tx.finalAccountState().toHex(); tx.inputNoteNullifiers().map((n) => n.toHex()); - tx.outputNotes().toString(); + tx.outputNotes().notes().map((note) => note.id().toString()); } ``` From 9b2bb2018d6b3d63c97203a3030ce8b43e0a4af6 Mon Sep 17 00:00:00 2001 From: 0xrouss-miden <312482795+0xrouss-miden@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:14 +0200 Subject: [PATCH 4/6] docs(tutorials,tools): update remaining v0.16 content Bring Midenup, Playground, and the locally maintained tutorial helper pages in line with the v0.16 toolchain and developer workflows. --- docs/builder/tools/midenup.md | 42 +- docs/builder/tools/playground.md | 26 +- docs/builder/tutorials/helpers/debugging.md | 51 +- docs/builder/tutorials/helpers/pitfalls.md | 302 +++++------- docs/builder/tutorials/helpers/testing.md | 512 +++++++++----------- 5 files changed, 425 insertions(+), 508 deletions(-) diff --git a/docs/builder/tools/midenup.md b/docs/builder/tools/midenup.md index fa5f0239..83076c58 100644 --- a/docs/builder/tools/midenup.md +++ b/docs/builder/tools/midenup.md @@ -2,12 +2,12 @@ title: midenup sidebar_label: Midenup sidebar_position: 2 -description: "The Miden toolchain installer — bootstrap, pin, and switch between Miden VM / compiler / client / stdlib toolchains from a single `miden` entry point." +description: "The Miden toolchain installer — bootstrap, pin, and switch between Miden VM, compiler, client, and package toolchains from a single `miden` entry point." --- # midenup -`midenup` is the Miden toolchain installer. One install gives you a unified `miden` command that delegates to the Miden VM, compiler (`midenc` + `cargo-miden`), client, stdlib, and transaction kernel — all versioned together as a single release channel. +`midenup` is the Miden toolchain installer. One install gives you a unified `miden` command that delegates to the Miden VM, compiler (`midenc` + `cargo-miden`), client, formatter, local package registry, and protocol packages — all versioned together as a single release channel. @@ -42,14 +42,14 @@ Run `miden --version`. If you see "command not found," add `$CARGO_HOME/bin` (de `miden-client` — accounts, transactions, notes, proving. - - `miden-stdlib` — the canonical MASM standard library. + + `miden-format` — format Miden Assembly source files. - - `miden-base` — the transaction kernel that runs inside every account and note script. + + Core, protocol, standards, and transaction-kernel MASP packages used by the toolchain. - - Additional Miden components will be added to `midenup` as they ship. + + `miden-registry` — publish and inspect packages in a filesystem-backed local registry. @@ -58,15 +58,15 @@ Run `miden --version`. If you see "command not found," add `$CARGO_HOME/bin` (de ### Install a channel ```bash -midenup install stable # latest matching component set -midenup install 0.14 # pin to a specific release line +midenup install testnet # toolchain deployed to the public testnet +midenup install 0.16.0 # pin to a specific release line ``` ### Switch the active toolchain ```bash -midenup set 0.14 # pin for the current project (writes miden-toolchain.toml) -midenup override 0.14 # set the system-wide default +midenup set 0.16.0 # pin for the current project (writes miden-toolchain.toml) +midenup override 0.16.0 # set the system-wide default midenup show active-toolchain # which one is active right now? ``` @@ -75,7 +75,7 @@ A `miden-toolchain.toml` in the current directory always wins — otherwise the ### Uninstall ```bash -midenup uninstall 0.14 +midenup uninstall 0.16.0 ``` Delete `$MIDENUP_HOME` to uninstall `midenup` itself. Find its location with `midenup show home`. @@ -91,16 +91,18 @@ Removing toolchain directories manually corrupts the `midenup` environment. Use | `miden` command | Delegates to | What it does | | --- | --- | --- | | `miden new` | `cargo miden new` | Create a new Miden Rust project | -| `miden build` | `cargo miden build` | Build the project | +| `miden build` | `midenc miden-project.toml` | Build the current Miden project | | `miden new-wallet` | `miden-client new-wallet --deploy` | Create and deploy a wallet account | -| `miden account` | `miden-client account` | Create or inspect a local account | +| `miden account` | `miden-client new-account` | Create a local account | | `miden faucet` | `miden-client mint` | Fund an account from the faucet | -| `miden deploy` | `miden-client -s public --account-type regular-account-immutable-code` | Deploy a public, immutable-code contract | -| `miden call` | `miden-client account --show` | Read state from an account (view) | -| `miden send` | `miden-client send` | Send a state-changing transaction | +| `miden call` | `miden-client call` | Call a local account procedure | | `miden simulate` | `miden-client exec` | Dry-run a transaction without committing | +| `miden transfer` | `miden-client transfer` | Transfer assets to another account | +| `miden deploy` | `miden-client new-account --account-type public --deploy` | Create and deploy a public account | +| `miden format` | `miden-format` | Format MASM source (install with `--component format`) | +| `miden registry` | `miden-registry` | Manage the local registry (install with `--component local-registry`) | -Everything outside the alias table is forwarded to the underlying binary — e.g., `miden exec …` goes straight through to `miden-client exec`. +Use the component name to access commands that do not have an alias. ## Related @@ -109,7 +111,7 @@ Everything outside the alias table is forwarded to the underlying binary — e.g Full environment setup — prerequisites, node install, first account.
- Walk through `miden account`, `miden send`, `miden faucet`, and the rest. + Walk through `miden client account`, `miden client note`, `miden client sync`, and the rest. Endpoints the `miden` CLI points at — RPC, faucet, remote prover, block explorer. diff --git a/docs/builder/tools/playground.md b/docs/builder/tools/playground.md index 3a23dd40..fe2555c1 100644 --- a/docs/builder/tools/playground.md +++ b/docs/builder/tools/playground.md @@ -1,16 +1,16 @@ --- title: Playground sidebar_position: 3 -description: "Browser-based environment to write, compile, and execute Miden Assembly programs — no local tooling required." +description: "Browser-based sandbox for building Miden smart contracts and interacting with accounts, notes, and transactions — no local tooling required." --- # Miden Playground -An interactive browser environment for writing, compiling, and executing Miden Assembly (MASM) programs. No installation required — prototype account code, test note scripts, and experiment with VM instructions straight from a URL. +An interactive browser environment for learning Miden and testing smart contracts. No installation required — create a testnet sandbox, write and compile Rust account components and scripts, inspect the generated Miden Assembly (MASM), and execute transactions. - Launch the browser IDE and start writing MASM immediately. + Launch a tutorial, open an example, or create a testnet sandbox. Instruction set, stack semantics, chiplets, and assembler behaviour. @@ -20,19 +20,19 @@ An interactive browser environment for writing, compiling, and executing Miden A ## What you can do - - Syntax-highlighted editor with inline error reporting for the Miden assembler. + + Write and compile account components, authentication components, note scripts, and transaction scripts in Rust. - - Run programs against the Miden VM in-browser and inspect the resulting stack and memory. + + Review the read-only MASM output together with package exports, dependencies, and compilation errors. - - Shareable URLs with embedded code for reproducing bugs or teaching examples. + + Create or import accounts and notes, invoke account procedures, consume notes, and inspect transactions. - -The Playground shines for learning MASM and for quick prototyping. For anything bigger than a snippet — components, storage, note dispatch, transaction flows — move to a `miden new` Rust project locally. See [your first smart contract](../get-started/your-first-smart-contract) for the handoff. + +The Playground is useful for guided tutorials and end-to-end experiments without local setup. Move to a `miden new` Rust project when you need source control, automated tests, or a custom build and deployment workflow. See [your first smart contract](../get-started/your-first-smart-contract) for the handoff. ## Related @@ -41,8 +41,8 @@ The Playground shines for learning MASM and for quick prototyping. For anything Install the toolchain and build + deploy a counter contract in Rust. - - New `word(...)` / `event(...)` constants, `std::math::u128`, and other MASM-level deltas. + + Explicit module declarations, new import syntax, debug procedures, and other MASM-level deltas. Accounts, notes, transactions, and the Rust SDK surface. diff --git a/docs/builder/tutorials/helpers/debugging.md b/docs/builder/tutorials/helpers/debugging.md index ed50001d..5184529a 100644 --- a/docs/builder/tutorials/helpers/debugging.md +++ b/docs/builder/tutorials/helpers/debugging.md @@ -1,12 +1,29 @@ --- sidebar_position: 2 title: "Debugging Guide" -description: "Learn how to debug Miden Rust contracts using assert_eq and cycle counts." +description: "Learn how to debug Miden Rust contracts using debug output and assertions." --- # Debugging Guide -Miden contracts don't support traditional debugging tools like console.log or print statements. Instead, you can use `assert_eq` statements to check values during execution. +Miden contracts don't provide an interactive debugger or console. Use `miden::println!` to trace +execution paths and assertions to check values during execution. + +## Printing Debug Markers + +Use a literal or string expression to mark the path taken through a contract: + +```rust +miden::println!("entered withdraw"); + +if balance == felt!(0) { + miden::println!("balance is empty"); +} +``` + +`miden::println!` accepts string literals and expressions. It also supports Rust-style formatting +arguments, such as `miden::println!("balance: {}", balance)`. Formatted output requires +`extern crate alloc` and a configured global allocator; literal markers don't allocate. ## Using assert_eq @@ -23,37 +40,37 @@ assert_eq(actual_value, expected_value); `assert_eq` is a **function**, not a macro. Use `assert_eq(a, b)` without the exclamation mark. ::: -## Debugging with Cycle Counts +## Narrowing Down Failures -When your code fails, the error output includes a **cycle count** indicating where execution stopped. You can use this to narrow down problems: +Execution errors include source diagnostics when debug information is available. Combine those +diagnostics with markers and assertions to isolate the failing operation: -1. **Note the cycle count** when your code fails -2. **Place an `assert_eq`** before the code you suspect is failing -3. **Run again** and check the result: - - If the assertion fails at an **earlier cycle count**: the value you're checking is wrong - - If the assertion passes and fails at the **same cycle count**: the value is correct, the problem is elsewhere +1. Place `miden::println!` markers before and after the code you suspect. +2. Add an `assert_eq` for the value the code expects. +3. Run again and inspect the last marker and any assertion failure. ### Example ```rust pub fn withdraw(&mut self, depositor: AccountId, amount: Felt) { let balance = self.get_balance(depositor); + miden::println!("loaded balance"); - // Debug: Check if balance is what you expect + // Check the assumption used by the code below. assert_eq(balance, felt!(1000)); - // If the above passes, the problem is below this line - // If it fails, the balance isn't what you expected - let new_balance = balance - amount; - self.balances.set(key, new_balance); + self.balances.set(depositor, new_balance); + miden::println!("updated balance"); } ``` -By moving the `assert_eq` statement around, you can isolate which value is incorrect. +Move the markers and assertion through the function to narrow down which assumption or operation +fails. ## Limitations -- No console.log or print debugging in contract code - `assert_eq` only works with `Felt` values -- This is currently the primary debugging technique available +- `miden::println!` emits output unconditionally and adds execution work; remove debug-only calls + from release code +- Remove only diagnostic assertions; keep assertions that enforce contract invariants diff --git a/docs/builder/tutorials/helpers/pitfalls.md b/docs/builder/tutorials/helpers/pitfalls.md index 965f3d08..df1919c8 100644 --- a/docs/builder/tutorials/helpers/pitfalls.md +++ b/docs/builder/tutorials/helpers/pitfalls.md @@ -8,31 +8,33 @@ description: "Reference guide for known issues, limitations, and workarounds whe This reference documents known issues and limitations when developing with the Miden Rust compiler, along with recommended workarounds. -## Felt Comparison Operators +## Comparing Asset Amounts ### Problem -Direct comparison operators (`<`, `>`, `<=`, `>=`) on `Felt` values produce incorrect results. +`Felt` comparison operators work, but a field element is not a validated integer amount type. +Reading an amount directly from an asset bypasses its fungibility and range checks, and subsequent +arithmetic remains vulnerable to modular wraparound. ```rust -// WRONG: This does NOT work correctly -let a = Felt::new(100); -let b = Felt::new(200); -if a < b { // May produce unexpected results! +// Avoid decoding a token amount as a raw field element. +let amount = asset.value[0]; +if amount <= felt!(1_000_000) { // ... } ``` ### Solution -Always convert Felt values to `u64` before comparing: +Use `AssetAmount` for fungible token amounts. It has integer ordering and checked arithmetic: ```rust -// CORRECT: Convert to u64 first -let a = Felt::new(100); -let b = Felt::new(200); -if a.as_u64() < b.as_u64() { - // Works correctly +use miden::AssetAmount; + +let a = AssetAmount::from(100_u32); +let b = AssetAmount::from(200_u32); +if a < b { + // Integer comparison } ``` @@ -40,21 +42,22 @@ if a.as_u64() < b.as_u64() { ```rust title="contracts/bank-account/src/lib.rs" // Validating deposit amount -let amount = asset.unwrap_fungible().amount().as_u64(); +const MAX_DEPOSIT_AMOUNT: u32 = 1_000_000; + +// Asset::amount() validates that the asset is fungible and returns AssetAmount. +let amount = asset.amount(); -// Use u64 comparison +// Use integer comparison assert!( - amount <= MAX_DEPOSIT_AMOUNT, // MAX_DEPOSIT_AMOUNT is u64 + amount <= AssetAmount::from(MAX_DEPOSIT_AMOUNT), "Deposit exceeds maximum" ); ``` -:::warning Always Use .as_u64() -Any time you compare Felt values, convert them first. This applies to: -- Amount comparisons -- Balance checks -- Index comparisons -- Any numeric ordering +:::warning Raw Felt values +When a protocol API genuinely gives you a raw `Felt`, use `as_canonical_u64()` only after +confirming that the value is intended to have integer semantics. For fungible assets, prefer +`Asset::amount()` and keep the value as `AssetAmount`. ::: --- @@ -69,11 +72,6 @@ The Miden VM stack only allows direct access to the first 16 elements. Complex f invalid stack index: only the first 16 elements on the stack are directly accessible ``` -This may also appear as: -``` -values not found in advice provider -``` - ### Solution **1. Reduce local variables:** @@ -132,47 +130,43 @@ for asset in assets { --- -## Function Argument Limit (4 Words) +## Exported Procedure Argument Limit (4 Words) ### Problem -Miden functions can receive at most 4 Words (16 Felts) as arguments: - -``` -error: expected at most 4 words of arguments -``` +Exported component procedures and direct cross-context calls can currently receive at most 4 +Words (16 Felts) as arguments. ```rust -// WRONG: Too many arguments -fn process( - &mut self, - depositor: AccountId, // ~1 Word - asset: Asset, // 1 Word - serial_num: Word, // 1 Word - tag: Felt, // 1 Felt - note_type: Felt, // 1 Felt - extra_data: Word, // 1 Word - EXCEEDS LIMIT! -) { - // ... +#[component] +trait Processor { + #[account_procedure] + fn process( + &mut self, + depositor: AccountId, // 2 Felts + asset: Asset, // 2 Words (key + value) + serial_num: Word, // 1 Word + tag: Felt, // 1 Felt + note_type: Felt, // 1 Felt + extra_data: Word, // 1 Word - EXCEEDS LIMIT! + ); } ``` ### Solution -**1. Make sure to only pass 4 Words to functions:** +**1. Keep exported procedure inputs within 4 Words:** ```rust -// CORRECT: Only pass 4 Words -fn process( - &mut self, - depositor: AccountId, // ~1 Word - asset: Asset, // 1 Word - serial_num: Word, // 1 Word - params: Word, // [tag, note_type, 0, 0] - 1 Word -) { - let tag = params[0]; - let note_type = params[1]; - // ... +#[component] +trait Processor { + #[account_procedure] + fn process( + &mut self, + asset: Asset, // 2 Words (key + value) + serial_num: Word, // 1 Word + params: Word, // [tag, note_type, 0, 0] - 1 Word + ); } ``` @@ -207,7 +201,7 @@ fn store_config(&mut self, key: Word, config_data: Word) { // Reference by key in other operations fn process_with_config(&mut self, key: Word) { - let config = self.configs.get(&key); + let config = self.configs.get(key); // Use config... } ``` @@ -218,7 +212,7 @@ fn process_with_config(&mut self, key: Word) { ### Problem -Arrays passed from Rust to the Miden VM are received in **reversed order**. +At a Rust/MASM stack boundary, arrays appear on the operand stack in **reversed order**. ```rust // In Rust, you define: @@ -231,38 +225,24 @@ let word = Word::from([a, b, c, d]); Be aware of this when: - Constructing storage keys -- Parsing note inputs +- Parsing note storage - Working with asset data **Example: Storage Key Construction** ```rust -// Balance key format in Rust +// Balance-key input in Rust contract code let key = Word::from([ - depositor.prefix().as_felt(), // Position 0 in Rust - depositor.suffix(), // Position 1 - faucet.prefix().as_felt(), // Position 2 - faucet.suffix(), // Position 3 + depositor.prefix, // Position 0 in Rust + depositor.suffix, // Position 1 + faucet.prefix, // Position 2 + faucet.suffix, // Position 3 ]); // When the VM processes this, it sees: // [faucet.suffix, faucet.prefix, depositor.suffix, depositor.prefix] ``` -**Example: Asset Structure** - -```rust -// Asset Word layout (Rust perspective) -// [amount, 0, faucet_suffix, faucet_prefix] - -let asset_word = Word::from([ - Felt::new(amount), // [0] amount - Felt::new(0), // [1] padding - faucet.id().suffix(), // [2] faucet suffix - faucet.id().prefix().as_felt(), // [3] faucet prefix -]); -``` - :::tip Consistency is Key The reversal doesn't matter as long as you're **consistent**. Always construct and parse arrays the same way throughout your codebase. ::: @@ -277,8 +257,8 @@ Miden uses field element (Felt) arithmetic, which operates in a prime field with ```rust // DANGEROUS: This does NOT error on underflow! -let balance = Felt::new(100); -let withdrawal = Felt::new(500); +let balance = felt!(100); +let withdrawal = felt!(500); let new_balance = balance - withdrawal; // Silently wraps to a huge positive number! ``` @@ -286,54 +266,39 @@ When you subtract a larger value from a smaller one, the result wraps around to ### Why This Happens -The Miden VM performs all Felt arithmetic as modular operations within the prime field. There is no automatic overflow or underflow detection at the VM level. The Rust compiler's default overflow mode is `Unchecked`, meaning it compiles directly to raw VM arithmetic operations. +The Miden VM performs all Felt arithmetic as modular operations within the prime field. There is no automatic overflow or underflow detection at the VM level. ### Solution -**Always validate before subtraction:** +**Use `AssetAmount` for asset balances:** ```rust -// CORRECT: Check balance before subtracting -let current_balance: Felt = self.balances.get(&key); -let withdraw_amount = withdraw_asset.inner[0]; +// CORRECT: Keep balances in a StorageMap. +let current_balance: AssetAmount = self.balances.get(key); +let withdraw_amount = withdraw_asset.amount(); -// Validate that balance is sufficient -assert!( - current_balance.as_u64() >= withdraw_amount.as_u64(), - "Withdrawal amount exceeds available balance" -); - -// Only subtract after validation +// AssetAmount subtraction checks for underflow. let new_balance = current_balance - withdraw_amount; +self.balances.set(key, new_balance); ``` ### Example from Bank Contract ```rust title="contracts/bank-account/src/lib.rs" -pub fn withdraw(&mut self, depositor: AccountId, withdraw_asset: Asset, /* ... */) { - let withdraw_amount = withdraw_asset.inner[0]; - - // Get current balance and validate sufficient funds exist. - // This check is critical: Felt arithmetic is modular, so subtracting - // more than the balance would silently wrap to a large positive number. - let current_balance: Felt = self.balances.get(&key); - assert!( - current_balance.as_u64() >= withdraw_amount.as_u64(), - "Withdrawal amount exceeds available balance" - ); - - let new_balance = current_balance - withdraw_amount; +pub fn withdraw(&mut self, key: Word, withdraw_asset: Asset) { + let current_balance: AssetAmount = self.balances.get(key); + let new_balance = current_balance - withdraw_asset.amount(); self.balances.set(key, new_balance); } ``` :::danger Critical Security Issue -Failure to validate before subtraction can lead to: +Using unchecked raw `Felt` subtraction for balances can lead to: - Users withdrawing more than their balance - Balance values becoming astronomically large - Complete loss of funds in the contract -**Always check bounds before Felt subtraction operations.** +Use `AssetAmount` or explicitly validate bounds before subtracting raw `Felt` values. ::: --- @@ -342,15 +307,12 @@ Failure to validate before subtraction can lead to: ### Problem -The `active_note::add_assets_to_account()` function fails if the consuming account doesn't have the basic wallet component. - -``` -Error: Account does not support asset operations -``` +The `basic_wallet::move_note_assets_to_account` procedure is available only when the consuming +account includes the basic wallet component. ### Solution -Ensure accounts that receive assets via this function have wallet capability: +Ensure note scripts that call this procedure target accounts with wallet capability: ```rust use miden_client::account::component::BasicWallet; @@ -362,23 +324,6 @@ let account = AccountBuilder::new(seed) .build()?; ``` -**Alternative: Use `native_account::add_asset()`** - -For account components, use the native account API instead: - -```rust -#[component] -impl Bank { - pub fn deposit(&mut self, depositor: AccountId, asset: Asset) { - // This works for any account - no wallet required - native_account::add_asset(asset); - - // Track balance in storage - self.update_balance(depositor, asset); - } -} -``` - --- ## Storage Map Key Consistency @@ -392,27 +337,37 @@ Storage map lookups return unexpected results or zeros when keys are constructed Define a single key construction pattern and use it everywhere: ```rust title="contracts/bank-account/src/lib.rs" -#[component] -impl Bank { - /// Construct a balance key for a depositor and asset. - /// Key format: [depositor_prefix, depositor_suffix, faucet_prefix, faucet_suffix] - fn balance_key(&self, depositor: AccountId, faucet_id: AccountId) -> Word { +use miden::{component_storage, AccountId, Asset, AssetAmount, StorageMap, Word}; + +#[component_storage] +struct BankStorage { + #[storage(description = "fungible balances by depositor and asset")] + balances: StorageMap, +} + +impl BankStorage { + /// Combine the depositor and fungible asset ID into one map key. + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { + // Reject non-fungible assets before deriving the compact key. + let _ = asset.amount(); + Word::from([ - depositor.prefix().as_felt(), - depositor.suffix(), - faucet_id.prefix().as_felt(), - faucet_id.suffix(), + depositor.prefix, + depositor.suffix, + asset.key[3], + asset.key[2], ]) } - pub fn get_balance(&self, depositor: AccountId, faucet_id: AccountId) -> Felt { - let key = self.balance_key(depositor, faucet_id); - self.balances.get(&key) + fn get_depositor_balance(&self, depositor: AccountId, asset: &Asset) -> AssetAmount { + let key = BankStorage::balance_key(depositor, asset); + self.balances.get(key) } - fn update_balance(&mut self, depositor: AccountId, faucet_id: AccountId, amount: Felt) { - let key = self.balance_key(depositor, faucet_id); - self.balances.set(key, amount); + fn update_balance(&mut self, depositor: AccountId, asset: &Asset, amount: AssetAmount) { + let key = BankStorage::balance_key(depositor, asset); + let current = self.balances.get(key); + self.balances.set(key, current + amount); } } ``` @@ -432,13 +387,13 @@ Use the correct values for note types: | Value | Type | Description | |-------|------|-------------| | 1 | Public | Note data is visible onchain | -| 2 | Private | Note data is hidden (only hash onchain) | +| 0 | Private | Only a commitment to the note details is published | ```rust -// In note inputs or when creating output notes -let note_type = Felt::new(1); // Public note +// In note storage or when creating output notes +let note_type = felt!(1); // Public note // or -let note_type = Felt::new(2); // Private note +let note_type = felt!(0); // Private note ``` --- @@ -449,13 +404,13 @@ let note_type = Felt::new(2); // Private note When creating P2ID (Pay-to-ID) output notes, you need the script's MAST root. The old v0.13 pattern of hardcoding the digest is fragile — it hashed under RPO, which v0.14 replaced with Poseidon2, and any future change to the P2ID script invalidates the constant silently. -### Solution (v0.15) +### Solution -Carry the P2ID script root on the initiating note's storage and read it at runtime instead of hardcoding a value. This is the pattern used in the current `miden-bank` example: +Carry the P2ID script root on the initiating note's storage and read it at runtime instead of hardcoding a value: ```rust title="contracts/bank-account/src/lib.rs" -// The withdraw-request note encodes the P2ID script root at storage slots -// 10..14 (4 felts = 1 Word). The Poseidon2-hashed digest of the P2ID note +// The withdraw-request note encodes the P2ID script root in storage elements +// 10 through 13 (4 felts = 1 Word). The Poseidon2-hashed digest of the P2ID note // script is injected by the caller when the note is created. let storage = active_note::get_storage(); let script_root = Word::from([ @@ -472,12 +427,12 @@ On the client side, compute the script root dynamically from the standard P2ID n use miden_client::note::P2idNote; use miden_client::Word; -// v0.15: script roots are typed NoteScriptRoot values; convert when a Word is needed. -let p2id_script_root: Word = P2idNote::script().root().into(); +// Script roots are typed NoteScriptRoot values; convert when a Word is needed. +let p2id_script_root: Word = P2idNote::script_root().into(); ``` :::info Why Not Hardcode -The native hash function changed from RPO to Poseidon2 in v0.14, so every MAST root — including the P2ID script's — is different from v0.13. Any hardcoded digest from v0.13 will fail a script-root check on current releases. Reading the root from `P2idNote::script().root()` (or the active note's storage for onchain code) keeps the contract resilient to future script changes. +The native hash function changed from RPO to Poseidon2 in v0.14, so every MAST root — including the P2ID script's — is different from v0.13. Any hardcoded digest from v0.13 will fail a script-root check on current releases. Reading the root from `P2idNote::script_root()` (or the active note's storage for onchain code) keeps the contract resilient to future script changes. ::: --- @@ -488,13 +443,7 @@ The native hash function changed from RPO to Poseidon2 in v0.14, so every MAST r Every Miden transaction must either change tracked account state (storage, vault, or nonce) **or** consume at least one input note. A transaction that does neither is rejected. -The Rust client surfaces this as `TransactionRequestError::NoInputNotesNorAccountChange` before submission: - -``` -empty transaction: the request has no input notes and no account state changes -``` - -The VM kernel enforces the same invariant during execution, surfacing the message: +The VM kernel enforces this invariant during execution, surfacing the message: ``` executed transaction neither changed the account state, nor consumed any notes @@ -533,7 +482,9 @@ fn run(arg: Word, account: &mut Account) { } ``` -Alternatively, if the flow naturally consumes a note (most do — note scripts mutate state when they run), make sure the transaction request includes at least one input note. Pass the `Note` (the client deduces authentication from the note record) along with optional `NoteArgs`: +Alternatively, if the flow naturally consumes a note, make sure the transaction request includes +it. Pass the `Note` with optional `NoteArgs`; the client uses the presence of an inclusion proof in +its store to decide whether to consume it as an authenticated or unauthenticated note: ```rust let request = TransactionRequestBuilder::new() @@ -541,39 +492,22 @@ let request = TransactionRequestBuilder::new() .build()?; ``` -:::tip Standard auth handles this for you -Most account templates run an authentication procedure that calls `incr_nonce()` on every transaction. If your account uses `BasicWallet`, `IncrNonceAuthComponent`, or any auth component that increments the nonce, you only hit this pitfall in transaction-script-only flows that skip the auth path. See [Authentication](../../smart-contracts/accounts/authentication) for details. -::: - -### Why this exists - -A Miden transaction commits to a state delta plus a set of consumed notes. A transaction with neither is indistinguishable from "no transaction at all" — admitting it would waste a block slot and a proof verification. The invariant lets the network reject empty proofs cheaply. - -:::info See also -- Client-side error catalog: [`TransactionRequestError::NoInputNotesNorAccountChange`](../../tools/clients/common-errors) -- Failure modes table: [Account Operations](../../smart-contracts/accounts/account-operations#when-proof-generation-fails) -::: - --- ## Quick Reference Table | Pitfall | Symptom | Solution | |---------|---------|----------| -| Felt comparison | Wrong comparison results | Use `.as_u64()` | +| Asset amount stored as `Felt` | Modular wraparound or an invalid amount | Use `AssetAmount` | | Stack overflow | "16 elements" error | Reduce locals, split functions | -| Too many args | "4 words" error | Group into Words, use inputs | +| Too many exported procedure inputs | Export-lifting error | Group into Words, use note storage | | Array reversal | Wrong data order | Be consistent with construction | -| Felt underflow | Balance wraps to huge number | Validate before subtraction | +| Felt underflow | Balance wraps to huge number | Use `AssetAmount` or validate raw values | | Missing wallet | Asset operation fails | Add `BasicWallet` component | | Key mismatch | Zero balances | Use helper function for keys | -| Note type | Wrong note visibility | Use 1 (Public) or 2 (Private) | +| Note type | Wrong note visibility | Use 1 (Public) or 0 (Private) | | Empty transaction | "Neither changed account state nor consumed notes" | Mutate state in every path, or consume a note | -:::tip View Complete Source -See these patterns in context in the [miden-bank repository](https://github.com/keinberger/miden-bank). -::: - ## Next Steps - **[Debugging Guide](./debugging)** - Troubleshoot errors diff --git a/docs/builder/tutorials/helpers/testing.md b/docs/builder/tutorials/helpers/testing.md index 2de3fd19..fc64e542 100644 --- a/docs/builder/tutorials/helpers/testing.md +++ b/docs/builder/tutorials/helpers/testing.md @@ -30,6 +30,7 @@ your-project/ └── integration/ ├── Cargo.toml ├── src/ + │ ├── lib.rs # Exports test helpers │ └── helpers.rs # Test utilities └── tests/ └── my_test.rs # Test files @@ -41,10 +42,8 @@ your-project/ [package] name = "integration" version = "0.1.0" -edition = "2021" - -[lib] -path = "src/helpers.rs" +edition = "2024" +rust-version = "1.96.1" [[test]] name = "my_test" @@ -53,50 +52,54 @@ path = "tests/my_test.rs" [dependencies] anyhow = "1.0" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +miden-protocol = "0.16" +miden-standards = { version = "0.16", features = ["testing"] } +miden-testing = "0.16" +rand = "0.10" +``` + +Export the helpers from the integration crate: -# Miden dependencies -cargo-miden = { version = "0.8" } -miden-client = { version = "0.15", features = ["tonic", "testing"] } -miden-client-sqlite-store = { version = "0.15", package = "miden-client-sqlite-store" } -miden-core = { version = "0.23" } -miden-standards = { version = "0.15", default-features = false, features = ["testing"] } -miden-testing = "0.15" -miden-mast-package = { version = "0.23", default-features = false } -rand = { version = "0.9" } +```rust title="integration/src/lib.rs" +pub mod helpers; ``` ## Building Contracts for Tests -Use `cargo-miden` to build your contracts programmatically: +Use the `miden` toolchain to build contracts and then load the generated `.masp` artifact: ```rust title="integration/src/helpers.rs" -use std::path::Path; +use std::{path::{Path, PathBuf}, process::Command}; use anyhow::{bail, Context, Result}; -use cargo_miden::{run, OutputType}; -use miden_mast_package::Package; +use miden_protocol::{assembly::Package, utils::serde::Deserializable}; pub fn build_project_in_dir(dir: &Path, release: bool) -> Result { - let profile = if release { "--release" } else { "--debug" }; - let manifest_path = dir.join("Cargo.toml"); - let manifest_arg = manifest_path.to_string_lossy(); - - let args = vec![ - "cargo", "miden", "build", - profile, - "--manifest-path", &manifest_arg, - ]; - - let output = run(args.into_iter().map(String::from), OutputType::Masm) - .context("Failed to compile project")? - .context("Cargo miden build returned None")?; - - let artifact_path = match output { - cargo_miden::CommandOutput::BuildCommandOutput { output } => match output { - cargo_miden::BuildOutput::Masm { artifact_path } => artifact_path, - other => bail!("Expected Masm output, got {:?}", other), - }, - other => bail!("Expected BuildCommandOutput, got {:?}", other), - }; + let profile_dir = if release { "release" } else { "dev" }; + + let mut command = Command::new("miden"); + command.arg("build"); + if release { + command.arg("--release"); + } + + let status = command + .current_dir(dir) + .status() + .context("failed to run miden build")?; + if !status.success() { + bail!("miden build failed with {status}"); + } + + let artifact_dir = dir.join("target/miden").join(profile_dir); + let mut artifacts = std::fs::read_dir(&artifact_dir)? + .filter_map(|entry| entry.ok().map(|entry| entry.path())) + .filter(|path| path.extension().is_some_and(|ext| ext == "masp")); + let artifact_path: PathBuf = artifacts + .next() + .context("miden build produced no MASP artifact")?; + if artifacts.next().is_some() { + bail!("expected one MASP artifact in {}", artifact_dir.display()); + } let package_bytes = std::fs::read(&artifact_path)?; Package::read_from_bytes(&package_bytes) @@ -132,12 +135,12 @@ async fn my_test() -> anyhow::Result<()> { Faucets mint assets for testing: ```rust -use miden_client::auth::AuthSchemeId; +use miden_protocol::account::auth::AuthScheme; // Create a faucet with 1,000,000 max supply and 100 initial tokens let faucet = builder.add_existing_basic_faucet( Auth::BasicAuth { - auth_scheme: AuthSchemeId::Falcon512Poseidon2, + auth_scheme: AuthScheme::Falcon512Poseidon2, }, "TEST", // Token symbol 1_000_000, // Max supply @@ -150,13 +153,12 @@ let faucet = builder.add_existing_basic_faucet( Create accounts with initial assets: ```rust -use miden_client::asset::FungibleAsset; -use miden_client::auth::AuthSchemeId; +use miden_protocol::{account::auth::AuthScheme, asset::FungibleAsset}; // Create a wallet with 100 tokens from the faucet let sender = builder.add_existing_wallet_with_assets( Auth::BasicAuth { - auth_scheme: AuthSchemeId::Falcon512Poseidon2, + auth_scheme: AuthScheme::Falcon512Poseidon2, }, [FungibleAsset::new(faucet.id(), 100)?.into()], )?; @@ -167,19 +169,19 @@ let sender = builder.add_existing_wallet_with_assets( For accounts with custom components, create configuration helpers: ```rust title="integration/src/helpers.rs" -use miden_client::account::{AccountType, StorageSlot}; +use miden_protocol::account::{component::InitStorageData, AccountType}; #[derive(Clone)] pub struct AccountCreationConfig { pub account_type: AccountType, - pub storage_slots: Vec, + pub init_storage_data: InitStorageData, } impl Default for AccountCreationConfig { fn default() -> Self { Self { account_type: AccountType::Public, - storage_slots: vec![], + init_storage_data: InitStorageData::default(), } } } @@ -188,8 +190,15 @@ impl Default for AccountCreationConfig { ### Creating Account from Package ```rust -use miden_client::account::{StorageMap, StorageSlot, StorageSlotName}; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; + +use miden_protocol::{ + account::{ + component::InitStorageData, AccountBuilder, AccountComponent, StorageSlotName, + }, + Word, +}; +use miden_testing::{AccountState, Auth}; // Build the contract let bank_package = Arc::new(build_project_in_dir( @@ -197,68 +206,41 @@ let bank_package = Arc::new(build_project_in_dir( true, // release mode )?); -// Configure named storage slots +// Initialize values declared by the package's storage schema. let initialized_slot = StorageSlotName::new("miden::component::miden_bank_account::initialized") .expect("Valid slot name"); -let balances_slot = - StorageSlotName::new("miden::component::miden_bank_account::balances") - .expect("Valid slot name"); +let mut init_storage_data = InitStorageData::default(); +init_storage_data.insert_value(&initialized_slot, Word::default())?; let config = AccountCreationConfig { - storage_slots: vec![ - StorageSlot::with_value(initialized_slot, Word::default()), - StorageSlot::with_map( - balances_slot.clone(), - StorageMap::with_entries([]).expect("Empty storage map"), - ), - ], + init_storage_data, ..Default::default() }; -// Create the account -let mut account = create_testing_account_from_package( - bank_package.clone(), - config, -).await?; - -// Add to MockChain -builder.add_account(account.clone())?; +// Instantiate the component from the package and add an existing account. +let component = AccountComponent::from_package( + &bank_package, + &config.init_storage_data, +)?; +let account = builder.add_account_from_builder( + Auth::IncrNonce, + AccountBuilder::new([7_u8; 32]) + .account_type(config.account_type) + .with_component(component), + AccountState::Exists, +)?; ``` ## Creating Notes -### Note Configuration - -```rust title="integration/src/helpers.rs" -use miden_client::note::{NoteAssets, NoteTag, NoteType}; -use miden_core::Felt; - -pub struct NoteCreationConfig { - pub note_type: NoteType, - pub tag: NoteTag, - pub assets: NoteAssets, - pub inputs: Vec, -} - -impl Default for NoteCreationConfig { - fn default() -> Self { - Self { - note_type: NoteType::Public, - tag: NoteTag::new(0), - assets: Default::default(), - inputs: Default::default(), - } - } -} -``` - ### Creating Notes with Assets ```rust -use miden_client::asset::{Asset, FungibleAsset}; -use miden_client::note::NoteAssets; -use miden_client::transaction::RawOutputNote; +use std::{path::Path, sync::Arc}; + +use miden_protocol::{asset::FungibleAsset, transaction::RawOutputNote}; +use miden_standards::testing::note::NoteBuilder; // Build note script let deposit_note_package = Arc::new(build_project_in_dir( @@ -269,17 +251,13 @@ let deposit_note_package = Arc::new(build_project_in_dir( // Create assets to attach let deposit_amount: u64 = 1000; let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; -let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; - -// Create the note -let deposit_note = create_testing_note_from_package( - deposit_note_package.clone(), - sender.id(), // Note sender - NoteCreationConfig { - assets: note_assets, - ..Default::default() - }, -)?; + +// Create the note from the compiled package. +let mut rng = rand::rng(); +let deposit_note = NoteBuilder::new(sender.id(), &mut rng) + .package((*deposit_note_package).clone()) + .add_assets([fungible_asset.into()]) + .build()?; // Add to MockChain builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); @@ -287,36 +265,31 @@ builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); ### Creating Notes with Inputs -For notes that read parameters via `active_note::get_inputs()`: +For notes that read parameters via `active_note::get_storage()`: ```rust -use miden_core::Felt; - -// Note inputs are a vector of Felts -let inputs = vec![ - // Asset data [0-3] - Felt::new(withdraw_amount), - Felt::new(0), - faucet.id().suffix(), - faucet.id().prefix().as_felt(), - // Serial number [4-7] - Felt::new(0x1234567890abcdef), - Felt::new(0xfedcba0987654321), - Felt::new(0xdeadbeefcafebabe), - Felt::new(0x0123456789abcdef), - // Additional parameters - Felt::new(tag as u64), - Felt::new(1), // note_type (1 = Public) +use miden_protocol::{Felt, Word}; + +// Note storage is a vector of Felts. Define and document the schema for each note. +let serial_num = Word::from([ + Felt::new(0x1234567890abcdef).expect("serial limb is below the field modulus"), + Felt::new(0xfedcba0987654321).expect("serial limb is below the field modulus"), + Felt::new(0xdeadbeefcafebabe).expect("serial limb is below the field modulus"), + Felt::new(0x0123456789abcdef).expect("serial limb is below the field modulus"), +]); +let storage = vec![ + // Serial number [0-3] + serial_num[0], serial_num[1], serial_num[2], serial_num[3], + // Additional parameters [4-5] + Felt::from(tag), + Felt::ONE, // note_type (1 = Public) ]; -let note = create_testing_note_from_package( - note_package.clone(), - sender.id(), - NoteCreationConfig { - inputs, - ..Default::default() - }, -)?; +let mut rng = rand::rng(); +let note = NoteBuilder::new(sender.id(), &mut rng) + .package((*note_package).clone()) + .note_storage(storage)? + .build()?; ``` ## Executing Transactions @@ -327,21 +300,19 @@ let note = create_testing_note_from_package( // Build MockChain after adding all accounts and notes let mut mock_chain = builder.build()?; -// Build transaction context -// Args: (account_id, input_note_ids, expected_output_note_ids) -let tx_context = mock_chain - .build_tx_context(account.id(), &[note.id()], &[])? +// Build and execute a transaction that consumes a committed note. +let executed_tx = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) .build()?; - -// Execute -let executed_tx = tx_context.execute().await?; - -// Apply state changes to local account copy -account.apply_delta(&executed_tx.account_delta())?; +let executed_tx = executed_tx.execute().await?; // Add to pending transactions and prove block mock_chain.add_pending_executed_transaction(&executed_tx)?; mock_chain.prove_next_block()?; + +// Read the updated account from committed chain state. +let account = mock_chain.committed_account(account.id())?; ``` ### Transaction with Script @@ -349,7 +320,7 @@ mock_chain.prove_next_block()?; For transaction scripts (like initialization): ```rust -use miden_client::transaction::TransactionScript; +use miden_protocol::transaction::TransactionScript; // Build the transaction script let init_package = Arc::new(build_project_in_dir( @@ -357,16 +328,15 @@ let init_package = Arc::new(build_project_in_dir( true, )?); -let init_program = init_package.unwrap_program(); -let init_tx_script = TransactionScript::new((*init_program).clone()); +let init_tx_script = TransactionScript::from_package(&init_package)?; // Execute with script -let tx_context = mock_chain - .build_tx_context(account.id(), &[], &[])? +let executed_tx = mock_chain + .build_transaction(account.id()) .tx_script(init_tx_script) - .build()?; - -let executed_tx = tx_context.execute().await?; + .build()? + .execute() + .await?; ``` ### Transactions with Expected Output Notes @@ -374,7 +344,7 @@ let executed_tx = tx_context.execute().await?; When your contract creates output notes, specify them: ```rust -use miden_client::transaction::RawOutputNote; +use miden_protocol::{note::Note, transaction::RawOutputNote}; // Build the expected output note let expected_note = Note::new( @@ -383,10 +353,13 @@ let expected_note = Note::new( recipient, ); -let tx_context = mock_chain - .build_tx_context(account.id(), &[input_note.id()], &[])? - .extend_expected_output_notes(vec![RawOutputNote::Full(expected_note)]) - .build()?; +let executed_tx = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(input_note.id()) + .expected_output_note(RawOutputNote::Full(expected_note)) + .build()? + .execute() + .await?; ``` ## Verifying State Changes @@ -394,7 +367,10 @@ let tx_context = mock_chain ### Reading Storage After Transaction ```rust -// After executing and applying delta... +use miden_protocol::{account::StorageMapKey, Felt, Word}; + +// After adding the transaction and proving its block... +let account = mock_chain.committed_account(account.id())?; // Read Value storage (by slot name) let value: Word = account.storage().get_item(&initialized_slot)?; @@ -406,12 +382,14 @@ let key = Word::from([ faucet.id().prefix().as_felt(), faucet.id().suffix(), ]); -let balance = account.storage().get_map_item(&balances_slot, key)?; +let balance = account + .storage() + .get_map_item(&balances_slot, StorageMapKey::new(key))?; // Assert expected values assert_eq!( balance, - Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1000)]), + Word::from([Felt::from(1000_u32), Felt::ZERO, Felt::ZERO, Felt::ZERO]), "Balance should match deposited amount" ); ``` @@ -425,12 +403,13 @@ assert_eq!( async fn should_fail_without_initialization() -> anyhow::Result<()> { // Setup WITHOUT initialization step... - let tx_context = mock_chain - .build_tx_context(account.id(), &[note.id()], &[])? - .build()?; - // Execute and expect failure - let result = tx_context.execute().await; + let result = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .build()? + .execute() + .await; assert!( result.is_err(), @@ -456,7 +435,11 @@ async fn deposit_exceeds_max_should_fail() -> anyhow::Result<()> { // ... setup code ... - let result = tx_context.execute().await; + let transaction = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .build()?; + let result = transaction.execute().await; assert!( result.is_err(), @@ -469,138 +452,123 @@ async fn deposit_exceeds_max_should_fail() -> anyhow::Result<()> { ## Complete Test Example -```rust title="integration/tests/deposit_test.rs" -use integration::helpers::{ - build_project_in_dir, create_testing_account_from_package, - create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, -}; -use miden_client::{ - account::{StorageMap, StorageSlot, StorageSlotName}, - asset::{Asset, FungibleAsset}, - auth::AuthSchemeId, - note::NoteAssets, - transaction::{RawOutputNote, TransactionScript}, - Felt, Word, -}; -use miden_testing::{Auth, MockChain}; +```rust title="integration/tests/counter_test.rs" use std::{path::Path, sync::Arc}; +use anyhow::Context; +use integration::helpers::build_project_in_dir; +use miden_protocol::{ + account::{ + auth::AuthScheme, + component::InitStorageData, + AccountBuilder, + AccountComponent, + AccountType, + StorageMapKey, + StorageSlotName, + }, + crypto::rand::RandomCoin, + note::NoteScript, + transaction::RawOutputNote, + Felt, + Word, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{AccountState, Auth, MockChain}; + +const COUNTER_STORAGE_KEY: Word = + Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + +fn counter_storage_slot() -> anyhow::Result { + Ok(StorageSlotName::new( + "counter_account::counter_contract::count_map", + )?) +} + #[tokio::test] -async fn deposit_test() -> anyhow::Result<()> { - // 1. Setup MockChain builder +async fn counter_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - // 2. Create faucet and sender - let faucet = builder.add_existing_basic_faucet( - Auth::BasicAuth { - auth_scheme: AuthSchemeId::Falcon512Poseidon2, - }, - "TEST", - 1000, - Some(100), - )?; - let sender = builder.add_existing_wallet_with_assets( - Auth::BasicAuth { - auth_scheme: AuthSchemeId::Falcon512Poseidon2, - }, - [FungibleAsset::new(faucet.id(), 100)?.into()], - )?; + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; - // 3. Build contracts - let bank_package = Arc::new(build_project_in_dir( - Path::new("../contracts/bank-account"), true + let contract_package = Arc::new(build_project_in_dir( + Path::new("../contracts/counter-account"), + true, )?); - let deposit_note_package = Arc::new(build_project_in_dir( - Path::new("../contracts/deposit-note"), true - )?); - let init_tx_script_package = Arc::new(build_project_in_dir( - Path::new("../contracts/init-tx-script"), true + let note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/increment-note"), + true, )?); - // 4. Create bank account with named storage slots - let initialized_slot = - StorageSlotName::new("miden::component::miden_bank_account::initialized") - .expect("Valid slot name"); - let balances_slot = - StorageSlotName::new("miden::component::miden_bank_account::balances") - .expect("Valid slot name"); - - let bank_cfg = AccountCreationConfig { - storage_slots: vec![ - StorageSlot::with_value(initialized_slot, Word::default()), - StorageSlot::with_map( - balances_slot.clone(), - StorageMap::with_entries([]).expect("Empty storage map"), - ), - ], - ..Default::default() - }; - let mut bank_account = create_testing_account_from_package( - bank_package.clone(), bank_cfg - ).await?; - - // 5. Create deposit note - let deposit_amount: u64 = 1000; - let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; - let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; - let deposit_note = create_testing_note_from_package( - deposit_note_package.clone(), - sender.id(), - NoteCreationConfig { assets: note_assets, ..Default::default() }, + let counter_storage_slot = counter_storage_slot()?; + let mut init_storage_data = InitStorageData::default(); + init_storage_data.insert_map_entry( + counter_storage_slot.clone(), + COUNTER_STORAGE_KEY, + 0_u64, )?; - // 6. Add to builder and build chain - builder.add_account(bank_account.clone())?; - builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + let counter_component = AccountComponent::from_package(&contract_package, &init_storage_data) + .context("failed to build account component from counter package")?; + let counter_account = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + AccountBuilder::new([3_u8; 32]) + .account_type(AccountType::Public) + .with_component(counter_component), + AccountState::Exists, + )?; + + let mut note_rng = RandomCoin::new(Word::from( + NoteScript::from_package(note_package.as_ref()) + .context("failed to build note script from package")? + .root(), + )); + let counter_note = NoteBuilder::new(sender.id(), &mut note_rng) + .package((*note_package).clone()) + .build() + .context("failed to build counter note from package")?; + + builder.add_output_note(RawOutputNote::Full(counter_note.clone())); let mut mock_chain = builder.build()?; - // 7. Initialize bank - let init_program = init_tx_script_package.unwrap_program(); - let init_tx_script = TransactionScript::new((*init_program).clone()); - let init_tx_context = mock_chain - .build_tx_context(bank_account.id(), &[], &[])? - .tx_script(init_tx_script) + let transaction = mock_chain + .build_transaction(counter_account.id()) + .authenticated_input_note(counter_note.id()) .build()?; - let executed_init = init_tx_context.execute().await?; - bank_account.apply_delta(&executed_init.account_delta())?; - mock_chain.add_pending_executed_transaction(&executed_init)?; - mock_chain.prove_next_block()?; + let executed_transaction = transaction.execute().await?; - // 8. Execute deposit - let tx_context = mock_chain - .build_tx_context(bank_account.id(), &[deposit_note.id()], &[])? - .build()?; - let executed_tx = tx_context.execute().await?; - bank_account.apply_delta(&executed_tx.account_delta())?; - mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.add_pending_executed_transaction(&executed_transaction)?; mock_chain.prove_next_block()?; - // 9. Verify balance - let depositor_key = Word::from([ - sender.id().prefix().as_felt(), - sender.id().suffix(), - faucet.id().prefix().as_felt(), - faucet.id().suffix(), - ]); - let balance = bank_account.storage().get_map_item(&balances_slot, depositor_key)?; - let expected = Word::from([ - Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(deposit_amount) - ]); - assert_eq!(balance, expected, "Balance should match deposit"); - - println!("Deposit test passed!"); + let count = mock_chain + .committed_account(counter_account.id())? + .storage() + .get_map_item( + &counter_storage_slot, + StorageMapKey::new(COUNTER_STORAGE_KEY), + )?; + + assert_eq!(count[0].as_canonical_u64(), 1); + Ok(()) } ``` +For a step-by-step walkthrough, see +[Test Your Contract](../../get-started/your-first-smart-contract/test). + ## Running Tests ```bash title=">_ Terminal" # Run all tests cargo test -p integration -- --nocapture -# Run specific test -cargo test -p integration deposit_test -- --nocapture +# Run the configured integration test target +cargo test -p integration --test my_test -- --nocapture # Run with verbose output RUST_LOG=debug cargo test -p integration -- --nocapture @@ -611,14 +579,10 @@ RUST_LOG=debug cargo test -p integration -- --nocapture 1. **MockChain Builder Pattern** - Use `MockChain::builder()` to set up test environments 2. **Build Contracts First** - Use `build_project_in_dir()` to compile contracts before tests 3. **Configure Storage Slots** - Match your contract's storage layout when creating accounts -4. **Apply Deltas** - Always call `apply_delta()` on local account copies after transactions +4. **Read Committed State** - After proving a block, use `committed_account()` for updated state 5. **Prove Blocks** - Call `prove_next_block()` after adding executed transactions 6. **Test Failures** - Use `result.is_err()` to verify constraint violations -:::tip View Complete Source -See the complete test implementations in the [miden-bank repository](https://github.com/keinberger/miden-bank/tree/main/integration/tests). -::: - ## Next Steps - **[Debugging Guide](./debugging)** - Troubleshoot common issues From f293933b662cac2f5626068beac937d1e89dcc0c Mon Sep 17 00:00:00 2001 From: 0xrouss-miden <312482795+0xrouss-miden@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:14 +0200 Subject: [PATCH 5/6] docs(ai): refresh LLM entrypoints for v0.16 Update llms.txt and the bundled Miden skill with the current repository split, v0.16 stable guidance, and v0.17 next-version context. --- static/llms.txt | 11 ++++++----- static/skill.md | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/static/llms.txt b/static/llms.txt index 5209f880..19e65571 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -1,9 +1,9 @@ # Miden Documentation -> Miden is a privacy-preserving blockchain where users execute and prove transactions locally. Accounts are programmable smart contracts, notes are programmable messages, and private state stays client-side unless a developer opts into public or network-visible state. +> Miden is a privacy-preserving blockchain where users execute and prove transactions locally. Accounts are programmable smart contracts, notes are programmable messages, and private state stays client-side unless a developer opts for public accounts or notes. -- [Docs home](https://docs.miden.xyz/): Latest stable docs, currently 0.14. -- [Next / unstable builder docs](https://docs.miden.xyz/next/builder/): Current development builder docs, currently labeled 0.15 unstable. +- [Docs home](https://docs.miden.xyz/): Latest stable docs, currently 0.16. +- [Next / unstable builder docs](https://docs.miden.xyz/next/builder/): Current development builder docs, currently labeled 0.17 unstable. - [Next / unstable reference](https://docs.miden.xyz/next/reference/): Current development protocol, VM, node, and compiler docs. - [Agent skill](https://docs.miden.xyz/skill.md): Compact Miden context for AI coding assistants. - [GitHub organization](https://github.com/0xMiden): Source repositories. @@ -13,7 +13,7 @@ - [Get Started](https://docs.miden.xyz/builder/get-started/): Install the toolchain, create accounts, work with notes, read storage, and build a first smart contract. - [Installation](https://docs.miden.xyz/builder/get-started/setup/installation): Install Miden tools with `midenup`. - [CLI Basics](https://docs.miden.xyz/builder/get-started/setup/cli-basics): Common account, note, sync, and transaction commands. -- [Accounts](https://docs.miden.xyz/builder/get-started/accounts): Account types, storage modes, and creation patterns. +- [Accounts](https://docs.miden.xyz/builder/get-started/accounts): Account visibility and creation patterns. - [Notes](https://docs.miden.xyz/builder/get-started/notes): Note concepts and asset transfer flow. - [Read Storage](https://docs.miden.xyz/builder/get-started/read-storage): Query account storage and inspect on-chain state. - [Your First Smart Contract](https://docs.miden.xyz/builder/get-started/your-first-smart-contract/): Build, test, and deploy a Rust smart contract. @@ -68,7 +68,8 @@ - [docs](https://github.com/0xMiden/docs): Docusaurus documentation site. - [protocol](https://github.com/0xMiden/protocol): Protocol types, account model, notes, assets, transactions, and MASM protocol library. - [miden-vm](https://github.com/0xMiden/miden-vm): Virtual machine and assembler. -- [miden-client](https://github.com/0xMiden/miden-client): Rust client, Web SDK, and React SDK. +- [rust-sdk](https://github.com/0xMiden/rust-sdk): Rust client library and CLI. +- [web-sdk](https://github.com/0xMiden/web-sdk): Web SDK, React SDK, and browser tooling. - [node](https://github.com/0xMiden/node): Miden node implementation. - [compiler](https://github.com/0xMiden/compiler): Rust-to-MASM compiler. - [tutorials](https://github.com/0xMiden/tutorials): Tutorial source content. diff --git a/static/skill.md b/static/skill.md index c72dc695..3688e155 100644 --- a/static/skill.md +++ b/static/skill.md @@ -2,28 +2,28 @@ name: miden-architecture description: > Miden protocol, SDK, and documentation knowledge. Use when working on Miden - docs or source repositories, especially protocol, miden-client, node, + docs or source repositories, especially protocol, rust-sdk, web-sdk, node, miden-vm, compiler, tutorials, and application templates. compatibility: Designed for AI coding assistants. metadata: author: 0xMiden - docs_default: "0.14 (latest stable)" - docs_next: "0.15 (unstable)" - latest_stable: "0.14" + docs_default: "0.16 (latest stable)" + docs_next: "0.17 (unstable)" + latest_stable: "0.16" --- # Miden Protocol Skill ## Version Awareness -- The default docs at `https://docs.miden.xyz/` are the latest stable docs, currently **0.14**. -- The next-release docs are under `https://docs.miden.xyz/next/...` routes and are currently labeled **0.15 unstable**. +- The default docs at `https://docs.miden.xyz/` are the latest stable docs, currently **0.16**. +- The next-release docs are under `https://docs.miden.xyz/next/...` routes and are currently labeled **0.17 unstable**. - If a user asks about released behavior, check the matching versioned docs and release tag before answering. - If a user asks about current development, use `/next/builder/` or `/next/reference/` reference docs and the relevant source repository branch. ## What Is Miden -Miden is a privacy-preserving blockchain where accounts are programmable smart contracts and users execute and prove transactions locally. Accounts communicate asynchronously through programmable notes. Private account and note data stays with the client unless a developer deliberately chooses public or network-visible state. +Miden is a privacy-preserving blockchain where accounts are programmable smart contracts and users execute and prove transactions locally. Accounts communicate asynchronously through programmable notes. Private account and note data stays with the client unless a developer deliberately chooses public accounts or notes. ## Key Mental Model Shifts @@ -36,7 +36,7 @@ Miden is a privacy-preserving blockchain where accounts are programmable smart c ## Protocol Building Blocks -- **Accounts**: Smart contracts with ID, code, storage, vault, and nonce. Storage modes include private, public, and network account modes. +- **Accounts**: Smart contracts with ID, code, storage, vault, and nonce. Account state can be private or public; network-account behavior comes from account components rather than a third storage mode. - **Components**: Reusable account modules for storage, methods, authentication, wallet behavior, and application-specific logic. - **Notes**: Programmable asset containers. They can be private or public and are consumed by account transactions. - **Transactions**: Single-account state transitions with note processing, optional transaction scripts, and proof generation. @@ -78,7 +78,8 @@ Miden is a privacy-preserving blockchain where accounts are programmable smart c - Docs: https://github.com/0xMiden/docs - Protocol: https://github.com/0xMiden/protocol - Miden VM and assembler: https://github.com/0xMiden/miden-vm -- Client SDKs: https://github.com/0xMiden/miden-client +- Rust SDK and CLI: https://github.com/0xMiden/rust-sdk +- Web and React SDKs: https://github.com/0xMiden/web-sdk - Node: https://github.com/0xMiden/node - Compiler: https://github.com/0xMiden/compiler - Tutorials: https://github.com/0xMiden/tutorials @@ -93,7 +94,7 @@ Miden is a privacy-preserving blockchain where accounts are programmable smart c - Do not cite a GitHub blob URL unless the referenced file still exists at that branch or tag. - Do not assume `/next/*` behavior has been released. Use the default docs for latest stable behavior. - For Web SDK and React SDK code, verify names against the shipped npm types when possible. -- For network account and network note behavior, verify against the node version being discussed; `next` RPC names may differ from v0.14. +- For network account and network note behavior, verify against the node version being discussed; `next` RPC names may differ from v0.16. - For MASM import and assembler behavior, check `miden-vm` and protocol library docs together. ## Answering Guidance From c81584c656048560a63e9cb82f5efe3ef5288098 Mon Sep 17 00:00:00 2001 From: 0xrouss-miden <312482795+0xrouss-miden@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:14 +0200 Subject: [PATCH 6/6] chore(guardian): update docs ingestion to v0.17.0-rc.1 Pin the live Guardian documentation ingest and its generated source links to the tested v0.17.0-rc.1 release. --- .github/workflows/deploy-docs.yml | 2 +- scripts/ingest-guardian.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index fb3d6044..aca0fde2 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -55,7 +55,7 @@ jobs: DEFAULT_BRIDGE_PORTAL_REF: main # Guardian is an external (OpenZeppelin) repo released by tag; pin to a tested # release rather than tracking a branch. Bump on each Guardian release. - DEFAULT_GUARDIAN_REF: v0.15.0 + DEFAULT_GUARDIAN_REF: v0.17.0-rc.1 steps: - name: Checkout docs site uses: actions/checkout@v4 diff --git a/scripts/ingest-guardian.mjs b/scripts/ingest-guardian.mjs index 31df3f16..e9c77f79 100644 --- a/scripts/ingest-guardian.mjs +++ b/scripts/ingest-guardian.mjs @@ -28,7 +28,7 @@ import { } from "fs"; import { join, dirname, relative, posix } from "path"; -const [srcDir, destDir, ref = "v0.15.0"] = process.argv.slice(2); +const [srcDir, destDir, ref = "v0.17.0-rc.1"] = process.argv.slice(2); if (!srcDir || !destDir || !existsSync(srcDir)) { console.error(`ingest-guardian: usage: node scripts/ingest-guardian.mjs [ref]; src not found: ${srcDir}`); process.exit(1);