Skip to main content
Version: 0.13 (unstable)

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 StorageMap for 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:

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:

SlotTypePurpose
0ValueInitialization flag
1StorageMapDepositor 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:

contracts/bank-account/src/lib.rs
#[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:

>_ Terminal
cd contracts/bank-account
miden build

This compiles the Rust code to Miden Assembly and generates:

  • target/miden/release/bank_account.masp - The compiled package
  • target/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:

  1. Create a bank account
  2. Initialize it
  3. Verify the storage was updated

Create a new test file:

integration/tests/part1_account_test.rs
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:

>_ Terminal
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
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,
}

#[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

  1. #[component] marks structs and impl blocks as Miden account components
  2. Value stores a single Word, read with .read(), write with .write()
  3. StorageMap stores key-value pairs, access with .get() and .set()
  4. Storage slots are numbered from 0, each holds 4 Felts (32 bytes)
  5. 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.