diff --git a/docs/builder/get-started/accounts.md b/docs/builder/get-started/accounts.md index eb12da6a..c9dad704 100644 --- a/docs/builder/get-started/accounts.md +++ b/docs/builder/get-started/accounts.md @@ -281,13 +281,13 @@ use miden_client::{ }, AccountType, }, + asset::{AssetAmount, TokenSymbol}, auth::AuthSecretKey, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, rpc::{Endpoint, GrpcClient}, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::asset::{AssetAmount, TokenSymbol}; use miden_standards::AuthMethod; use rand::RngCore; use std::sync::Arc; diff --git a/docs/builder/get-started/notes.md b/docs/builder/get-started/notes.md index d61501ad..dd2f34b5 100644 --- a/docs/builder/get-started/notes.md +++ b/docs/builder/get-started/notes.md @@ -85,6 +85,7 @@ use miden_client::{ }, AccountBuilder, AccountType, }, + asset::{AssetAmount, AssetCallbackFlag, FungibleAsset, TokenSymbol}, auth::AuthSecretKey, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, @@ -93,7 +94,6 @@ use miden_client::{ transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::asset::{AssetAmount, FungibleAsset, TokenSymbol}; use miden_standards::AuthMethod; use rand::RngCore; use std::sync::Arc; @@ -193,7 +193,10 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + // 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); // 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) @@ -255,7 +258,7 @@ export async function demo() { amount: 1000n, type: "public", // note visibility }); - console.log("Mint transaction submitted successfully, ID:", txId.toString()); + console.log("Mint transaction submitted successfully, ID:", txId.toHex()); } ``` @@ -302,6 +305,7 @@ use miden_client::{ }, Account, AccountBuilder, AccountType, }, + asset::{AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol}, auth::AuthSecretKey, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, @@ -310,9 +314,6 @@ use miden_client::{ transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::asset::{ - AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol, -}; use miden_standards::AuthMethod; use rand::RngCore; use std::sync::Arc; @@ -413,7 +414,10 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + // 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); // 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) @@ -481,9 +485,11 @@ 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::Disabled, + AssetCallbackFlag::Enabled, ); println!( "Alice's TEST token balance: {:?}", @@ -533,7 +539,7 @@ export async function demo() { }); console.log( "Mint transaction submitted successfully, ID:", - mintResult.txId.toString(), + mintResult.txId.toHex(), ); // List notes available to Alice and consume them — tokens move into her vault. @@ -545,11 +551,16 @@ export async function demo() { }); console.log( "Consume transaction submitted successfully, ID:", - consumeResult.txId.toString(), + consumeResult.txId.toHex(), ); - // Read Alice's TEST token balance directly via the accounts resource. - const balance = await client.accounts.getBalance(alice, faucet); + // Fetch Alice again so her vault reflects the consumed note. + const updatedAlice = await client.accounts.get(alice); + if (!updatedAlice) { + throw new Error("Alice's account was not found"); + } + + const balance = updatedAlice.vault().getBalance(faucet.id()); console.log("Alice's TEST token balance:", Number(balance)); } ``` @@ -564,7 +575,7 @@ Minting 1000 tokens to Alice... Mint transaction submitted successfully, ID: "0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06" Waiting for note to be consumable... Consume transaction submitted successfully, ID: "0xa75872c498ee71cd6725aef9411d2559094cec1e1e89670dbf99c60bb8843481" -Alice's TEST token balance: Ok(1000) +Alice's TEST token balance: Ok(AssetAmount(1000)) ``` @@ -597,8 +608,9 @@ use miden_client::{ FungibleFaucet, MintPolicyConfig, PolicyRegistration, TokenName, TokenPolicyManager, TransferPolicy, create_fungible_faucet, }, - Account, AccountBuilder, AccountId, AccountType, + Account, AccountBuilder, AccountType, }, + asset::{AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol}, auth::AuthSecretKey, builder::ClientBuilder, keystore::{FilesystemKeyStore, Keystore}, @@ -607,9 +619,6 @@ use miden_client::{ transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::asset::{ - AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol, -}; use miden_standards::AuthMethod; use rand::RngCore; use std::sync::Arc; @@ -710,7 +719,10 @@ async fn main() -> anyhow::Result<()> { keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; let amount: u64 = 1000; - let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + // 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); // 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) @@ -778,9 +790,11 @@ 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::Disabled, + AssetCallbackFlag::Enabled, ); println!( "Alice's TEST token balance: {:?}", @@ -794,9 +808,28 @@ async fn main() -> anyhow::Result<()> { // SENDING TOKENS TO BOB //------------------------------------------------------------ - let bob_account_id = AccountId::from_hex("0x103f8a1ad4b983104aec0412ab0b0d")?; + // Create Bob's account so this example is self-contained. + let mut bob_seed = [0u8; 32]; + client.rng().fill_bytes(&mut bob_seed); + let bob_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let bob_account = AccountBuilder::new(bob_seed) + .account_type(AccountType::Public) + .with_auth_component(AuthSingleSig::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()); + + let bob_account_id = bob_account.id(); let send_amount = 100; - let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)?; + let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)? + .with_callbacks(AssetCallbackFlag::Enabled); let p2id_note = P2idNote::create( alice_account.id(), @@ -862,7 +895,7 @@ export async function demo() { }); console.log( "Mint transaction submitted successfully, ID:", - mintResult.txId.toString(), + mintResult.txId.toHex(), ); const notes = await client.notes.listAvailable({ account: alice }); @@ -873,24 +906,35 @@ export async function demo() { }); console.log( "Consume transaction submitted successfully, ID:", - consumeResult.txId.toString(), + consumeResult.txId.toHex(), ); - const balance = await client.accounts.getBalance(alice, faucet); + // Fetch Alice again so her vault reflects the consumed note. + const updatedAlice = await client.accounts.get(alice); + if (!updatedAlice) { + throw new Error("Alice's account was not found"); + } + + const balance = updatedAlice.vault().getBalance(faucet.id()); console.log("Alice's TEST token balance:", Number(balance)); + // Create Bob's account so this example is self-contained. + const bob = await client.accounts.create({ + storage: "public", + }); + console.log("Bob's account ID:", bob.id().toString()); + // Send 100 tokens from Alice to Bob. - const bobAccountId = "0x103f8a1ad4b983104aec0412ab0b0d"; console.log("Sending 100 tokens to Bob..."); const { txId } = await client.transactions.send({ account: alice, - to: bobAccountId, + to: bob, token: faucet, amount: 100n, type: "public", waitForConfirmation: true, }); - console.log("Send transaction submitted successfully, ID:", txId.toString()); + console.log("Send transaction submitted successfully, ID:", txId.toHex()); } ``` @@ -903,7 +947,8 @@ Faucet account ID: 0xe48c43d6ad6496201bcfa585a5a4b6 Minting 1000 tokens to Alice... Mint transaction submitted successfully, ID: 0x948a0eef754068b3126dd3261b6b54214fa5608fb13c5e5953faf59bad79c75f Consume transaction submitted successfully, ID: 0xc69ab84b784120abe858bb536aebda90bd2067695f11d5da93ab0b704f39ad78 -Alice's TEST token balance: 100 +Alice's TEST token balance: 1000 +Bob's account ID: 0x103f8a1ad4b983104aec0412ab0b0d Send 100 tokens to Bob note transaction ID: "0x51ac27474ade3a54adadd50db6c2b9a2ede254c5f9137f93d7a970f0bc7d66d5" ``` diff --git a/docs/builder/get-started/read-storage.md b/docs/builder/get-started/read-storage.md index 198c9893..d15baec8 100644 --- a/docs/builder/get-started/read-storage.md +++ b/docs/builder/get-started/read-storage.md @@ -35,12 +35,12 @@ Let's interact with a counter contract deployed on the Miden testnet. This contr ### Reading the Count of a Counter contract ```rust title="integration/src/bin/read-count.rs" +use integration::helpers::{counter_storage_slot, COUNTER_STORAGE_KEY}; use miden_client::{ - account::{Account, AccountId, StorageSlotName}, + account::{Account, AccountId}, builder::ClientBuilder, keystore::FilesystemKeyStore, rpc::{Endpoint, GrpcClient}, - Word, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; use std::sync::Arc; @@ -76,7 +76,9 @@ async fn main() -> anyhow::Result<()> { // READ PUBLIC STATE OF THE COUNTER ACCOUNT //------------------------------------------------------------ - let counter_account_id = AccountId::from_hex("0x224a96d294e10d006aef3d4f1b0876")?; + // A counter contract deployed on the Miden testnet. It is a public fixture and + // may need updating after a new release. + let counter_account_id = AccountId::from_hex("0x6a1b2d59a9ebd3f1534cfb2fcf4d7e")?; client.import_account_by_id(counter_account_id).await?; @@ -86,14 +88,11 @@ async fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow::anyhow!("Account not found"))? .try_into()?; - // Read the count from the counter account's named storage map slot - let slot_name = StorageSlotName::new( - "miden::component::miden_counter_account::count_map" - )?; - let count_key = Word::from([0u32, 0, 0, 1]); + // Read the count from the counter account's named storage map slot. Both the slot + // name and the map key come from the project's `integration/src/helpers.rs`. let count = counter_account .storage() - .get_map_item(&slot_name, count_key)?; + .get_map_item(&counter_storage_slot()?, COUNTER_STORAGE_KEY)?; println!("Count: {:?}", count); @@ -108,20 +107,22 @@ export async function demo() { // Initialize client to connect with the Miden Testnet. const client = await MidenClient.createTestnet(); - const counterAccountId = "0x224a96d294e10d006aef3d4f1b0876"; + // A counter contract deployed on the Miden testnet. It is a public fixture and + // may need updating after a new release. + const counterAccountId = "0x6a1b2d59a9ebd3f1534cfb2fcf4d7e"; // Fetch the counter account (imports it into the local store if needed). const counter = await client.accounts.getOrImport(counterAccountId); // Get the count from the counter account by querying its storage map // using the named storage slot and counter key. - const slotName = "miden::component::miden_counter_account::count_map"; + const slotName = "counter_account::counter_contract::count_map"; const counterKey = new Word(BigUint64Array.from([0n, 0n, 0n, 1n])); const count = counter.storage().getMapItem(slotName, counterKey); // The count value is a WORD (array of 4 u64 values). - // The 4th value is the counter number. - console.log("Count:", Number(count?.toU64s()[3])); + // The counter number is the first element. + console.log("Count:", Number(count?.toU64s()[0])); } ``` @@ -129,7 +130,7 @@ export async function demo() { Expected output ```text -Count: 1 +Count: Word([1, 0, 0, 0]) ``` @@ -140,14 +141,27 @@ You can also query the assets (tokens) held by an account: ```rust title="integration/src/bin/token-balance.rs" use miden_client::{ - account::{Account, AccountId}, + account::{ + component::{ + AccessControl, AuthScheme, AuthSingleSig, BasicWallet, BurnPolicyConfig, + FungibleFaucet, MintPolicyConfig, PolicyRegistration, TokenName, TokenPolicyManager, + TransferPolicy, create_fungible_faucet, + }, + Account, AccountBuilder, AccountType, + }, + asset::{AssetAmount, AssetCallbackFlag, AssetVaultKey, FungibleAsset, TokenSymbol}, + auth::AuthSecretKey, builder::ClientBuilder, - keystore::FilesystemKeyStore, + keystore::{FilesystemKeyStore, Keystore}, + note::NoteType, rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::asset::{AssetCallbackFlag, AssetVaultKey}; +use miden_standards::AuthMethod; +use rand::RngCore; use std::sync::Arc; +use tokio::time::Duration; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -177,23 +191,135 @@ async fn main() -> anyhow::Result<()> { client.sync_state().await?; //------------------------------------------------------------ - // READ TOKEN BALANCE OF AN ACCOUNT + // CREATING A FAUCET, MINTING AND CONSUMING TOKENS //------------------------------------------------------------ - let alice_account_id = AccountId::from_hex("0x5b2840a923dedc102ea67e0c1eba3c")?; - let faucet_account_id = AccountId::from_hex("0x29dd1dc628d2842032e751ed1b5da7")?; + // Account seeds + let mut alice_seed = [0u8; 32]; + client.rng().fill_bytes(&mut alice_seed); + let mut faucet_seed = [0u8; 32]; + client.rng().fill_bytes(&mut faucet_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("TEST")?; + let decimals = 8; + let max_supply = AssetAmount::from(1_000_000u32); + + // Generate key pair + let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the account + let account_builder = AccountBuilder::new(alice_seed) + .account_type(AccountType::Public) + .with_auth_component(AuthSingleSig::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicWallet); + + // Build the faucet + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Test Token")?) + .symbol(symbol) + .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 alice_account = account_builder.build()?; + let faucet_account = create_fungible_faucet( + faucet_seed, + faucet, + AccountType::Public, + AuthMethod::SingleSig { + approver: ( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ), + }, + AccessControl::AuthControlled, + policies, + )?; + + 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?; + client.add_account(&faucet_account, false).await?; + + // Add keys to keystore + keystore.add_key(&alice_key_pair, alice_account.id()).await?; + 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); + + // Mint the asset to Alice — this creates a P2ID note she can consume. + let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + )?; + client + .submit_new_transaction(faucet_account.id(), transaction_request) + .await?; + client.sync_state().await?; + + // Public notes must be committed to a block before they can be consumed. + // Poll until the network includes our mint note in a block. + loop { + client.sync_state().await?; - client.import_account_by_id(alice_account_id).await?; + let consumable_notes = client + .get_consumable_notes(Some(alice_account.id())) + .await?; + if consumable_notes.is_empty() { + println!("Waiting for P2ID note to be comitted..."); + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + + let notes: Vec = consumable_notes + .into_iter() + .map(|(record, _)| record.try_into().expect("Failed to convert to Note")) + .collect(); + + let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(notes)?; + client + .submit_new_transaction(alice_account.id(), consume_tx_request) + .await?; + client.sync_state().await?; + + break; + } + + //------------------------------------------------------------ + // READ TOKEN BALANCE OF AN ACCOUNT + //------------------------------------------------------------ + + // Fetch the account again so the vault reflects the consumed note. let alice_account: Account = client - .get_account(alice_account_id) + .get_account(alice_account.id()) .await? .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::Disabled, + faucet_account.id(), + AssetCallbackFlag::Enabled, ); let balance = alice_account.vault().get_balance(balance_key)?; @@ -210,14 +336,46 @@ export async function demo() { // Initialize client to connect with the Miden Testnet. const client = await MidenClient.createTestnet(); - const aliceId = "0x5b2840a923dedc102ea67e0c1eba3c"; - const faucetId = "0x29dd1dc628d2842032e751ed1b5da7"; - - // Fetch Alice's account (imports it into the local store if needed) - // and query her balance for the faucet's token. - await client.accounts.getOrImport(aliceId); - const balance = await client.accounts.getBalance(aliceId, faucetId); - + // Create Alice's account and a faucet. + const alice = await client.accounts.create({ + storage: "public", + }); + console.log("Alice's account ID:", alice.id().toString()); + + const decimals = 8; + const maxSupply = 10_000_000n * 10n ** BigInt(decimals); + const faucet = await client.accounts.create({ + type: 0, // Fungible faucet + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // Mint 1000 tokens to Alice and consume the resulting P2ID note. + await client.transactions.mint({ + account: faucet, + to: alice, + amount: 1000n, + type: "public", + waitForConfirmation: true, + }); + + const notes = await client.notes.listAvailable({ account: alice }); + await client.transactions.consume({ + account: alice, + notes: [notes[0]], + waitForConfirmation: true, + }); + + // Fetch Alice again so the vault reflects the consumed note. + const updatedAlice = await client.accounts.get(alice); + if (!updatedAlice) { + throw new Error("Alice's account was not found"); + } + + const balance = updatedAlice.vault().getBalance(faucet.id()); console.log("Alice's TEST token balance:", Number(balance)); } ``` @@ -226,7 +384,10 @@ export async function demo() { Expected output ```text -Alice's TEST token balance: 900 +Alice's account ID: "0x5b2840a923dedc102ea67e0c1eba3c" +Faucet account ID: "0x29dd1dc628d2842032e751ed1b5da7" +Waiting for P2ID note to be comitted... +Alice's TEST token balance: AssetAmount(1000) ``` diff --git a/docs/builder/get-started/setup/installation.md b/docs/builder/get-started/setup/installation.md index 6616afed..a71584d4 100644 --- a/docs/builder/get-started/setup/installation.md +++ b/docs/builder/get-started/setup/installation.md @@ -88,8 +88,10 @@ The Miden toolchain installer makes it easy to manage Miden components: cargo install midenup ``` +To install a specific release, pass `--version` — this guide is written against `1.0.0-alpha.1`, which as a pre-release is only installed when named explicitly: `cargo install midenup --version 1.0.0-alpha.1`. + :::info -Until published to crates.io, install using: `cargo install --git https://github.com/0xMiden/midenup.git` +To install from source instead, name the package explicitly — the repository contains more than one binary: `cargo install --git https://github.com/0xMiden/midenup.git midenup` ::: **Initialize midenup** 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 9d5d2248..9c764570 100644 --- a/docs/builder/get-started/your-first-smart-contract/create.md +++ b/docs/builder/get-started/your-first-smart-contract/create.md @@ -54,11 +54,16 @@ version = "0.1.0" [lib] 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" [dependencies] miden-core = "*" miden-protocol = "*" + +[package.metadata.miden] +supported-types = ["RegularAccountImmutableCode"] ``` The increment note depends on the counter account's generated WIT so it can call the counter interface: @@ -70,13 +75,15 @@ version = "0.1.0" [lib] kind = "note" -namespace = "miden:increment-note/increment-note@0.1.0" +# Notes export a package-derived interface (`miden-`), matching the `#[note]` macro. +namespace = "miden:increment-note/miden-increment-note@0.1.0" [dependencies] miden-core = "*" miden-protocol = "*" counter-account = { path = "../counter-account" } +# WIT for the account component this note calls, produced by building counter-account. [package.metadata.miden.dependencies] counter-account = { wit = "../counter-account/target/generated-wit/" } ``` @@ -113,7 +120,7 @@ Let's examine the counter account contract that comes with the project template. use miden::{component, component_storage, felt, Felt, StorageMap, Word}; -/// Storage for the counter example. +/// Storage layout for the counter example. #[component_storage] struct CounterContractStorage { /// Storage map holding the counter value. @@ -121,16 +128,17 @@ struct CounterContractStorage { count_map: StorageMap, } -/// Public interface for the counter component. +/// API of the counter contract account component. #[component] trait CounterContract { + /// Returns the current counter value stored in the contract's storage map. fn get_count(&self) -> Felt; + /// Increments the counter value stored in the contract's storage map by one. fn increment_count(&mut self) -> Felt; } #[component] impl CounterContract for CounterContractStorage { - /// Returns the current counter value stored in the contract's storage map. fn get_count(&self) -> Felt { // Define a fixed key for the counter value within the map let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); @@ -138,7 +146,6 @@ impl CounterContract for CounterContractStorage { self.count_map.get(key) } - /// Increments the counter value stored in the contract's storage map by one. fn increment_count(&mut self) -> Felt { // Define the same fixed key let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); @@ -193,12 +200,14 @@ struct CounterContractStorage { #[component] trait CounterContract { + /// Returns the current counter value stored in the contract's storage map. fn get_count(&self) -> Felt; + /// Increments the counter value stored in the contract's storage map by one. 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 package name and field name (e.g., `miden::component::miden_counter_account::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 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`). **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. @@ -235,10 +244,11 @@ Now let's examine the increment note script at `contracts/increment-note/src/lib // extern crate alloc; // use alloc::vec::Vec; -use miden::{account, note, Felt, Word}; +use miden::*; +/// Native account of the note: exposes the `counter-contract` component methods gathered from the `counter-contract` package. #[account(counter_account::CounterContract)] -pub struct CounterAccount; +pub struct Wallet; #[note] struct IncrementNote; @@ -246,7 +256,7 @@ struct IncrementNote; #[note] impl IncrementNote { #[note_script] - fn run(self, _arg: Word, account: &mut CounterAccount) { + fn run(self, _arg: Word, account: &mut Wallet) { let initial_value = account.get_count(); account.increment_count(); let expected_value = initial_value + Felt::from_u32(1); @@ -265,10 +275,10 @@ Similar to the account contract, the note script uses `#![no_std]` with the same #### Miden Imports ```rust -use miden::{account, note, Felt, Word}; +use miden::*; ``` -These imports bring in the note macro, explicit account binding macro, and the basic field/word types used by the note. +The note script glob-imports the `miden` prelude: the `#[note]` and `#[account]` macros, the basic field/word types (`Felt`, `Word`), and free functions such as `assert_eq`. Listing the imports individually is easy to get wrong — `assert_eq` here is a function from the prelude, not Rust's `assert_eq!` macro, so omitting it fails to compile. #### Note Script Structure @@ -281,11 +291,11 @@ struct IncrementNote; #[note] impl IncrementNote { #[note_script] - fn run(self, _arg: Word, account: &mut CounterAccount) { ... } + fn run(self, _arg: Word, account: &mut Wallet) { ... } } ``` -The struct definition (`IncrementNote`) provides a named type for the note script. Unlike account contracts, note scripts don't store persistent data — the struct serves as the entry point container. The `CounterAccount` type is declared with `#[account(counter_account::CounterContract)]`, which binds the note to the counter account interface generated from `miden-project.toml`. +The struct definition (`IncrementNote`) provides a named type for the note script. Unlike account contracts, note scripts don't store persistent data — the struct serves as the entry point container. The `Wallet` type is declared with `#[account(counter_account::CounterContract)]`, which binds the note to the counter account interface generated from `miden-project.toml`. Learn more about [note scripts in the Miden documentation](/reference/protocol/note/). @@ -293,7 +303,7 @@ Learn more about [note scripts in the Miden documentation](/reference/protocol/n ```rust #[note_script] -fn run(self, _arg: Word, account: &mut CounterAccount) { +fn run(self, _arg: Word, account: &mut Wallet) { let initial_value = account.get_count(); account.increment_count(); let expected_value = initial_value + Felt::from_u32(1); 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 5ad5f72e..68c6c977 100644 --- a/docs/builder/get-started/your-first-smart-contract/deploy.md +++ b/docs/builder/get-started/your-first-smart-contract/deploy.md @@ -39,7 +39,7 @@ The integration folder serves two essential functions in Miden development: ### 1. Contract Interaction Scripts (Binary Executables) -Think of the scripts in `src/bin/` as Miden's equivalent to [**Foundry scripts**](https://getfoundry.sh/guides/scripting-with-solidity). These are executable Rust binaries that handle all your contract interactions: +Think of the scripts in `src/bin/` as Miden's equivalent to [**Foundry scripts**](https://www.getfoundry.sh/forge/scripting). These are executable Rust binaries that handle all your contract interactions: - **Contract Deployment**: Scripts that create and deploy accounts to the network - **Function/Procedure Calls**: Scripts that interact with deployed contracts through notes or [transaction scripts](/reference/protocol/transaction#transaction-lifecycle) @@ -83,12 +83,12 @@ cargo run --bin increment_count --release Expected Output ```text -Account ID: V0(AccountIdV0 { prefix: 14134910893364381952, suffix: 3644349760121494784 }) -Sender account ID: "0xd85b347218c5a80052dbd47b2f36ad" -Counter note hash: "0xf0e821396a896eb9983e682bc056021d57ddcaa43082f34597bf9e026421e566" -Note publish transaction ID: "0xc6f080855724402cadf26650ffe993fe97a127a8f6c9c82ec621960e936e6d732 -Consume transaction ID: "0x2d1d8510e546ce0fbc22fa7d1a82322259d73cd1d7e0ca86622d0be70fab0548" -Account delta: AccountDelta { account_id: V0(AccountIdV0 { prefix: 7255964780328958976, suffix: 2724050564200846336 }), storage: AccountStorageDelta { values: {}, maps: {0: StorageMapDelta({LexicographicWord(Word([0, 0, 0, 1])): Word([0, 0, 0, 1])})} }, vault: AccountVaultDelta { fungible: FungibleAssetDelta({}), non_fungible: NonFungibleAssetDelta({}) }, nonce_delta: 1 } +Latest block: 1238402 +Account ID: V1(AccountIdV1 { suffix: 6091438912547090176, prefix: 14314767458661568337 }) +Sender account ID: "0x7d9c7007a5773c116bc58f9f28762b" +Counter note hash: "0xa3642c5eb08d8298a2fb8ed7f268f13c4139771c77540768a7347c882f47ab4f" +Note publish transaction ID: "0xfc190c8edbf972115cd5f00b22c8eac06c9515d0403b84ff697efa7093d6b8a7" +Consume transaction ID: "0xa1aa7971e146710df74a674b7e99d1968bec8d2db1c4da6077a7e22843c92045" ``` @@ -160,20 +160,14 @@ These packages contain all the information needed to deploy and interact with yo Once we have the compiled packages, we convert them into deployable accounts and notes: ```rust -// Configure initial storage for the counter account -let count_storage_key = Word::from([0u32, 0, 0, 1]); -let count_storage_map_key = StorageMapKey::new(count_storage_key); -let initial_count = Word::default(); - -// Use the slot name generated for the component's manifest namespace and field name. -let counter_storage_slot = - StorageSlotName::new("miden::component::miden_counter_account::count_map").unwrap(); -let storage_slots = vec![StorageSlot::with_map( - counter_storage_slot.clone(), - StorageMap::with_entries([(count_storage_map_key, initial_count)]).unwrap(), -)]; +// Configure initial storage for the counter account. +let counter_storage_slot = counter_storage_slot()?; +let mut init_storage_data = InitStorageData::default(); +init_storage_data + .insert_map_entry(counter_storage_slot, COUNTER_STORAGE_KEY, 0_u64) + .context("Failed to seed counter storage")?; let counter_cfg = AccountCreationConfig { - storage_slots, + init_storage_data, ..Default::default() }; @@ -193,10 +187,12 @@ 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 their storage slots specified 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 `miden::component::::`. We define the storage configuration with: +**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: -- A named `StorageMap` slot (`miden::component::miden_counter_account::count_map`) -- The counter key `[0, 0, 0, 1]`, wrapped as a `StorageMapKey`, with initial value `[0, 0, 0, 0]` (representing count = 0) +- 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` + +`InitStorageData` carries these seed values into `AccountComponent::from_package()`, which the helper calls for you. This pre-initialization ensures the account's storage is properly configured before deployment. @@ -205,14 +201,12 @@ This pre-initialization ensures the account's storage is properly configured bef Similarly, we convert the note package into an executable note: ```rust -// Convert the increment note package into an executable note -let counter_note = create_note_from_package( - &mut client, - note_package.clone(), - sender_account.id(), - NoteCreationConfig::default() -) -.context("Failed to create counter note from package")?; +// Build the increment note directly from the compiled package. +let counter_note = NoteBuilder::new(sender_account.id(), client.rng()) + .package((*note_package).clone()) + .tag(0) + .build() + .context("Failed to create counter note from package")?; // Publish the note to the network let note_publish_request = TransactionRequestBuilder::new() @@ -221,11 +215,11 @@ let note_publish_request = TransactionRequestBuilder::new() .context("Failed to build note publish transaction request")?; ``` -The `create_note_from_package()` function: +`NoteBuilder` (from `miden_standards::testing::note`): -- Takes the compiled note script package -- Combines it with the sender account ID and configuration -- Creates an executable note containing the increment script logic +- Takes the sender account ID and the client's RNG +- Accepts the compiled note script package via `.package()` +- Produces an executable note containing the increment script logic - The note can then be published to the network and consumed by the target (counter) account This demonstrates the complete workflow: Rust source code → compiled packages → deployable accounts/notes → network transactions. 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 660e123f..2fa8c5fa 100644 --- a/docs/builder/get-started/your-first-smart-contract/test.md +++ b/docs/builder/get-started/your-first-smart-contract/test.md @@ -47,7 +47,6 @@ You should see output confirming the test passes: ```text title="Expected Output" running 1 test -Test passed! test counter_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out @@ -61,19 +60,20 @@ Your project includes a comprehensive test file at `integration/tests/counter_te Test File ```rust title="integration/tests/counter_test.rs" -use integration::helpers::{ - build_project_in_dir, create_testing_account_from_package, create_testing_note_from_package, - AccountCreationConfig, NoteCreationConfig, -}; +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::{StorageMap, StorageMapKey, StorageSlot, StorageSlotName}, + account::{component::InitStorageData, AccountBuilder, AccountComponent, AccountType}, auth::AuthSchemeId, + crypto::RandomCoin, + note::NoteScript, transaction::RawOutputNote, Word, }; -use miden_testing::{Auth, MockChain}; -use std::{path::Path, sync::Arc}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{AccountState, Auth, MockChain}; #[tokio::test] async fn counter_test() -> anyhow::Result<()> { @@ -95,68 +95,63 @@ async fn counter_test() -> anyhow::Result<()> { true, )?); - // Create the counter account with initial storage and no-auth auth component - let count_storage_key = Word::from([0u32, 0, 0, 1]); - let initial_count = Word::default(); - - // Use the slot name generated for the component's manifest namespace and field name. - let counter_storage_slot = - StorageSlotName::new("miden::component::miden_counter_account::count_map").unwrap(); - let storage_slots = vec![StorageSlot::with_map( - counter_storage_slot.clone(), - StorageMap::with_entries([(StorageMapKey::new(count_storage_key), initial_count)]).unwrap(), - )]; - let counter_cfg = AccountCreationConfig { - storage_slots, - ..Default::default() - }; - - // create testing counter account - let mut counter_account = - create_testing_account_from_package(contract_package.clone(), counter_cfg).await?; - - // create testing increment note - let counter_note = create_testing_note_from_package( - note_package.clone(), - sender.id(), - NoteCreationConfig::default(), + // Create the counter account with its initial storage through the component schema. + 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)?; + + 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: AuthSchemeId::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")?; + // add counter account and note to mockchain - builder.add_account(counter_account.clone())?; builder.add_output_note(RawOutputNote::Full(counter_note.clone())); // Build the mock chain let mut mock_chain = builder.build()?; + // Build the transaction context let tx_context = mock_chain - .build_tx_context(counter_account.id(), &[counter_note.id()], &[])? + .build_tx_context(counter_account.clone(), &[counter_note.id()], &[])? .build()?; // Execute the transaction let executed_transaction = tx_context.execute().await?; - // Apply the account delta to the counter account - counter_account.apply_delta(executed_transaction.account_delta())?; - // Add the executed transaction to the mockchain mock_chain.add_pending_executed_transaction(&executed_transaction)?; mock_chain.prove_next_block()?; // Get the count from the updated counter account - let count = counter_account + let count = mock_chain + .committed_account(counter_account.id())? .storage() - .get_map_item(&counter_storage_slot, count_storage_key) + .get_map_item(&counter_storage_slot, COUNTER_STORAGE_KEY) .expect("Failed to get counter value from storage slot"); - // Assert that the count value is equal to 1 after executing the transaction assert_eq!( - count, - Word::from([0u32, 0, 0, 1]), + count[0].as_canonical_u64(), + 1, "Count value is not equal to 1" ); - - println!("Test passed!"); Ok(()) } ``` @@ -204,56 +199,60 @@ let note_package = Arc::new(build_project_in_dir( ### 3. Creating the Test Account and Note ```rust -// Create the counter account with initial storage and no-auth auth component -let count_storage_key = Word::from([0u32, 0, 0, 1]); -let initial_count = Word::default(); - -let counter_storage_slot = - StorageSlotName::new("miden::component::miden_counter_account::count_map").unwrap(); -let storage_slots = vec![StorageSlot::with_map( - counter_storage_slot.clone(), - StorageMap::with_entries([(StorageMapKey::new(count_storage_key), initial_count)]).unwrap(), -)]; -let counter_cfg = AccountCreationConfig { - storage_slots, - ..Default::default() -}; - -// Create testing entities -let mut counter_account = create_testing_account_from_package(contract_package.clone(), counter_cfg).await?; -let counter_note = create_testing_note_from_package( - note_package.clone(), - sender.id(), - NoteCreationConfig::default(), +// Create the counter account with its initial storage through the component schema. +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)?; + +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: AuthSchemeId::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")?; ``` **What's happening:** -- We configure the **counter account's initial storage** with count = 0 at storage key `[0, 0, 0, 1]` -- We create the **testing counter account** from the compiled package using `create_testing_account_from_package()` -- We create the **testing increment note** using `create_testing_note_from_package()` -- These helper functions create test-specific versions optimized for the Mockchain environment +- We seed the **counter account's initial storage** through `InitStorageData`, mapping `COUNTER_STORAGE_KEY` to `0` inside the named slot returned by `counter_storage_slot()` +- `AccountComponent::from_package()` turns the compiled package plus that storage seed into an account component +- `builder.add_account_from_builder()` registers the account with the mockchain in the `AccountState::Exists` state, so it behaves like an already-deployed account +- The note is built with `NoteBuilder`, seeded from a `RandomCoin` derived from the note script's MAST root — this keeps note generation deterministic across test runs -### 4. Adding Components to the Mockchain +### 4. Adding the Note to the Mockchain ```rust -builder.add_account(counter_account.clone())?; builder.add_output_note(RawOutputNote::Full(counter_note.clone())); let mut mock_chain = builder.build()?; ``` **What's happening:** -- We **add the counter account** to the mockchain builder - We **add the increment note** as a full output note to the mockchain - We **build the mockchain** - now we have a complete testing environment ready to use +The counter account does not need a separate `add_account()` call: `add_account_from_builder()` already registered it in the previous step. + ### 5. Creating and Executing the Transaction ```rust let tx_context = mock_chain - .build_tx_context(counter_account.id(), &[counter_note.id()], &[])? + .build_tx_context(counter_account.clone(), &[counter_note.id()], &[])? .build()?; let executed_transaction = tx_context.execute().await?; @@ -267,32 +266,28 @@ let executed_transaction = tx_context.execute().await?; ### 6. Verifying the Results ```rust -// Apply the account delta to the counter account -counter_account.apply_delta(executed_transaction.account_delta())?; - // Add the executed transaction to the mockchain mock_chain.add_pending_executed_transaction(&executed_transaction)?; mock_chain.prove_next_block()?; // Get the count from the updated counter account -let count = counter_account +let count = mock_chain + .committed_account(counter_account.id())? .storage() - .get_map_item(&counter_storage_slot, count_storage_key) + .get_map_item(&counter_storage_slot, COUNTER_STORAGE_KEY) .expect("Failed to get counter value from storage slot"); -// Assert that the count value is equal to 1 after executing the transaction assert_eq!( - count, - Word::from([0u32, 0, 0, 1]), + count[0].as_canonical_u64(), + 1, "Count value is not equal to 1" ); ``` **What's happening:** -- We **apply the account delta** from the executed transaction to the counter account to update its state -- We **add the executed transaction** to the mockchain -- We **read the counter value** from storage using the same key we initialized +- We **add the executed transaction** to the mockchain and prove the next block, which commits the new account state +- We **read the counter value** back from `mock_chain.committed_account()` — the committed state already reflects the transaction, so there is no need to apply the account delta by hand - We **assert that the count equals 1** - verifying the increment operation worked correctly The test verifies the complete flow: the increment note successfully increments the counter from 0 to 1, proving our smart contract works as expected.