Part 1: Account Components and Storage
In this section, you'll learn the fundamentals of building Miden account components. We'll expand our Bank to include balance tracking with a StorageMap, giving us the foundation for deposits and withdrawals.
What You'll Build in This Part
By the end of this section, you will have:
- Understood the
#[component]attribute and what it generates - Added a
StorageMapfor tracking depositor balances - Implemented a
get_balance()query method - Verified it works with a MockChain test
Building on Part 0
In Part 0, we created a minimal bank with just an initialized flag. Now we'll add balance tracking:
Part 0: Part 1:
┌────────────────────┐ ┌────────────────────┐
│ Bank │ │ Bank │
│ ───────────────── │ ──► │ ───────────────── │
│ slot 0: initialized│ │ slot 0: initialized│
│ │ │ slot 1: balances │ ◄── NEW
└────────────────────┘ └────────────────────┘
The #[component] Attribute
The #[component] attribute marks a struct as a Miden account component. When you compile with miden build, it generates:
- WIT (WebAssembly Interface Types) bindings for cross-component calls
- MASM (Miden Assembly) code for the account logic
- Storage slot management code
Let's expand our Bank component:
Step 1: Add the Balances Storage Map
Update contracts/bank-account/src/lib.rs:
#![no_std]
#![feature(alloc_error_handler)]
#[macro_use]
extern crate alloc;
use miden::*;
/// Bank account component that tracks depositor balances.
#[component]
struct Bank {
/// Tracks whether the bank has been initialized (deposits enabled).
/// Word layout: [is_initialized (0 or 1), 0, 0, 0]
#[storage(slot(0), description = "initialized")]
initialized: Value,
/// Maps depositor AccountId -> balance (as Felt)
/// Key: [prefix, suffix, asset_prefix, asset_suffix]
#[storage(slot(1), description = "balances")]
balances: StorageMap,
}
We've added a StorageMap in slot 1 that will track each depositor's balance.
Storage Types Explained
Miden accounts have storage slots that persist state on-chain. Each slot holds one Word (4 Felts = 32 bytes). The Miden Rust compiler provides two abstractions:
Value Storage
The Value type provides access to a single storage slot:
#[storage(slot(0), description = "initialized")]
initialized: Value,
Use Value when you need to store a single Word of data.
Reading and writing:
// Read returns a Word
let current: Word = self.initialized.read();
// Check the first element (our flag)
if current[0].as_u64() == 0 {
// Not initialized
}
// Write a new value
let new_value = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(new_value);
The .read() method requires a type annotation: let current: Word = self.initialized.read();
StorageMap
The StorageMap type provides key-value storage within a slot:
#[storage(slot(1), description = "balances")]
balances: StorageMap,
Use StorageMap when you need to store multiple values indexed by keys.
Reading and writing:
// Create a key (must be a Word)
let key = Word::from([
depositor.prefix,
depositor.suffix,
felt!(0),
felt!(0),
]);
// Get returns a Felt (single value, not a Word)
let balance: Felt = self.balances.get(&key);
// Set stores a Felt at the key
let new_balance = balance + deposit_amount;
self.balances.set(key, new_balance);
Unlike Value::read() which returns a Word, StorageMap::get() returns a single Felt. This is an important distinction.
Storage Slot Layout
Plan your storage layout carefully:
| Slot | Type | Purpose |
|---|---|---|
| 0 | Value | Initialization flag |
| 1 | StorageMap | Depositor balances |
The description attribute is for documentation and debugging - it doesn't affect runtime behavior.
Step 2: Implement Component Methods
Now let's add methods to our Bank. The #[component] attribute is also used on the impl block:
#[component]
impl Bank {
/// Initialize the bank account, enabling deposits.
pub fn initialize(&mut self) {
// Read current value from storage
let current: Word = self.initialized.read();
// Check not already initialized
assert!(
current[0].as_u64() == 0,
"Bank already initialized"
);
// Set initialized flag to 1
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(initialized_word);
}
/// Get the balance for a depositor.
pub fn get_balance(&self, depositor: AccountId) -> Felt {
let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]);
self.balances.get(&key)
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
Public vs Private Methods
- Public methods (
pub fn) are exposed in the generated WIT interface and can be called by other contracts - Private methods (
fn) are internal and cannot be called from outside
// Public: Can be called by note scripts and other contracts
pub fn get_balance(&self, depositor: AccountId) -> Felt { ... }
// Private: Internal helper, not exposed
fn require_initialized(&self) { ... }
Step 3: Build the Component
Build your updated account component:
cd contracts/bank-account
miden build
This compiles the Rust code to Miden Assembly and generates:
target/miden/release/bank_account.masp- The compiled packagetarget/generated-wit/- WIT interface files for other contracts to use
Try It: Verify Your Code
Let's write a MockChain test to verify our Bank component works correctly. This test will:
- Create a bank account
- Initialize it
- Verify the storage was updated
Create a new test file:
use integration::helpers::{
build_project_in_dir, create_testing_account_from_package, AccountCreationConfig,
};
use miden_client::account::{StorageMap, StorageSlot};
use miden_client::{Felt, Word};
use std::{path::Path, sync::Arc};
#[tokio::test]
async fn test_bank_account_storage() -> anyhow::Result<()> {
// =========================================================================
// SETUP: Build contracts and create the bank account
// =========================================================================
// Build the bank account contract
let bank_package = Arc::new(build_project_in_dir(
Path::new("../contracts/bank-account"),
true,
)?);
// Create the bank account with storage slots
let bank_cfg = AccountCreationConfig {
storage_slots: vec![
// Slot 0: initialized flag (starts as 0)
StorageSlot::Value(Word::default()),
// Slot 1: balances map (empty)
StorageSlot::Map(StorageMap::with_entries([])?),
],
..Default::default()
};
let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg).await?;
// =========================================================================
// VERIFY: Check initial storage state
// =========================================================================
// Verify slot 0 (initialized) starts as 0
let initialized_value = bank_account.storage().get_item(0)?;
assert_eq!(
initialized_value,
Word::default(),
"Initialized flag should start as 0"
);
println!("Bank account created successfully!");
println!(" Account ID: {:?}", bank_account.id());
println!(" Initialized flag: {:?}", initialized_value[0].as_int());
// =========================================================================
// VERIFY: Storage slots are correctly configured
// =========================================================================
// Check that we can query the balances map (should return 0 for any key)
let test_key = Word::from([Felt::new(1), Felt::new(2), Felt::new(0), Felt::new(0)]);
let balance = bank_account.storage().get_map_item(1, test_key)?;
// Balance for non-existent depositor should be all zeros
assert_eq!(
balance,
Word::default(),
"Balance for unknown depositor should be zero"
);
println!(" Balances map accessible: Yes");
println!("\nPart 1 test passed!");
Ok(())
}
Run the test from the project root:
cargo test --package integration part1_account_test -- --nocapture
Expected output
Compiling integration v0.1.0 (/path/to/miden-bank/integration)
Finished `test` profile [unoptimized + debuginfo] target(s)
Running tests/part1_account_test.rs
running 1 test
Bank account created successfully!
Account ID: 0x...
Initialized flag: 0
Balances map accessible: Yes
Part 1 test passed!
test test_bank_account_storage ... ok
test result: ok. 1 passed; 0 failed; 0 ignored
"cannot find function build_project_in_dir": Make sure your integration/src/helpers.rs exports this function and integration/src/lib.rs has pub mod helpers;.
"StorageSlot::Map not found": Ensure you're using the correct imports: use miden_client::account::StorageSlot;
Complete Code for This Part
Here's the full lib.rs after Part 1:
Click to expand full code
#![no_std]
#![feature(alloc_error_handler)]
#[macro_use]
extern crate alloc;
use miden::*;
/// Bank account component that tracks depositor balances.
#[component]
struct Bank {
/// Tracks whether the bank has been initialized (deposits enabled).
/// Word layout: [is_initialized (0 or 1), 0, 0, 0]
#[storage(slot(0), description = "initialized")]
initialized: Value,
/// Maps depositor AccountId -> balance (as Felt)
/// Key: [prefix, suffix, asset_prefix, asset_suffix]
#[storage(slot(1), description = "balances")]
balances: StorageMap,
}
#[component]
impl Bank {
/// Initialize the bank account, enabling deposits.
pub fn initialize(&mut self) {
// Read current value from storage
let current: Word = self.initialized.read();
// Check not already initialized
assert!(
current[0].as_u64() == 0,
"Bank already initialized"
);
// Set initialized flag to 1
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(initialized_word);
}
/// Get the balance for a depositor.
pub fn get_balance(&self, depositor: AccountId) -> Felt {
let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]);
self.balances.get(&key)
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
Key Takeaways
#[component]marks structs and impl blocks as Miden account componentsValuestores a single Word, read with.read(), write with.write()StorageMapstores key-value pairs, access with.get()and.set()- Storage slots are numbered from 0, each holds 4 Felts (32 bytes)
- Public methods are callable by other contracts via generated bindings
See the complete bank account implementation in the miden-bank repository.
Next Steps
Now that you understand account components and storage, let's learn how to define business rules with Part 2: Constants and Constraints.