Part 1: Account Components and Storage
In this section, you'll learn the fundamentals of building Miden account components. We'll explore the storage types introduced in Part 0 — Value and StorageMap — and add component methods.
What You'll Build in This Part
By the end of this section, you will have:
- Understood the
#[component]attribute and what it generates - Explored how
StorageMapworks for tracking depositor balances - Implemented a
get_balance()query method - Verified it works with an integration test
Building on Part 0
In Part 0, we created the Bank struct with initialized and balances storage. Now we'll explore the storage types in detail and add methods:
Part 0: Part 1:
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ Bank │ │ Bank │
│ ──────────────────────────────── │ ──► │ ──────────────────────────────── │
│ initialized (StorageValue<Word>) │ │ initialized (StorageValue<Word>) │
│ balances (StorageMap<Word, Felt>)│ │ balances (StorageMap<Word, Felt>)│
└──────────────────────────────────┘ │ + initialize() │
│ + get_balance() │ ◄── NEW
│ + require_initialized() │
└──────────────────────────────────┘
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: Understand the Storage Layout
In Part 0, we created the Bank struct with two storage fields. Let's examine what they do. Here is 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(description = "initialized")]
initialized: StorageValue<Word>,
/// Maps depositor AccountId -> balance (as Felt)
/// Key: [prefix, suffix, asset_prefix, asset_suffix]
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
}
The balances field is a StorageMap that tracks each depositor's balance. The compiler derives slot IDs by hashing slot names (not by field declaration order). Slot names follow the pattern miden::component::{component_name}::{field_name}.
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:
StorageValue Storage
The StorageValue<Word> type provides access to a single storage slot:
#[storage(description = "initialized")]
initialized: StorageValue<Word>,
Use StorageValue<Word> when you need to store a single Word of data.
Reading and writing:
// Get returns a Word
let current: Word = self.initialized.get();
// Check the first element (our flag)
if current[0].as_canonical_u64() == 0 {
// Not initialized
}
// Set a new value
let new_value = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.set(new_value);
The .get() method requires a type annotation: let current: Word = self.initialized.get();
StorageMap
The StorageMap<Word, Felt> type provides key-value storage within a slot:
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
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 generic type V where V: From<Word>.
// Here we annotate the result as Felt, which works because Felt implements From<Word>.
let balance: Felt = self.balances.get(&key);
// Set stores a value at the key (any type that implements Into<Word>)
let new_balance = balance + deposit_amount;
self.balances.set(key, new_balance);
StorageMap::get() returns a generic type V (constrained by V: From<Word>), not specifically Felt. The type is inferred from the variable annotation. In this tutorial we use Felt because we store single balance values, but you could also use Word or any custom type that implements the trait.
Storage Layout
Plan your storage layout carefully:
| Name | Type | Purpose |
|---|---|---|
initialized | StorageValue<Word> | Initialization flag |
balances | StorageMap<Word, Felt> | Depositor balances |
The description attribute generates named slot identifiers (e.g., miden_bank_account::bank::initialized) used in tests to reference specific slots. The naming convention is {package_name}::{component_struct}::{field_name}. The compiler derives slot IDs by hashing these names, so field declaration order does not affect slot assignment.
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) {
// Get current value from storage
let current: Word = self.initialized.get();
// Check not already initialized
assert!(
current[0].as_canonical_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.set(initialized_word);
}
/// Get the balance for a depositor and specific asset type.
pub fn get_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
let key = Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3], // faucet_prefix
asset.key[2], // faucet_suffix
]);
self.balances.get(key)
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
We define require_initialized() here but leave it commented out in the deposit function until Part 6. In Part 6 (Transaction Scripts), we'll enable it so the bank requires initialization before accepting deposits.
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 the outside
// Public: Can be called by note scripts and other contracts
pub fn get_balance(&self, depositor: AccountId, asset: Asset) -> 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
Optional: Verify Your Code
This is an optional self-check. If you create this test file, you can run it to verify your component. The main runnable tests begin in Part 4.
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, StorageSlotName};
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 named storage slots matching the contract's storage layout
// The naming convention is: {package_name}::{component_struct}::{field_name}
let initialized_slot =
StorageSlotName::new("miden_bank_account::bank::initialized")
.expect("Valid slot name");
let balances_slot =
StorageSlotName::new("miden_bank_account::bank::balances")
.expect("Valid slot name");
let mut init_storage_data = InitStorageData::default();
init_storage_data.insert_value(
StorageValueName::from_slot_name(&initialized_slot),
Word::default(),
)?;
let bank_cfg = AccountCreationConfig {
init_storage_data,
..Default::default()
};
let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg)?;
// =========================================================================
// VERIFY: Check initial storage state
// =========================================================================
// Verify initialized flag starts as 0
let initialized_value = bank_account.storage().get_item(&initialized_slot)?;
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_canonical_u64());
// =========================================================================
// 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(&balances_slot, 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 test_bank_account_storage -- --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 not found": Ensure you're using the correct imports: use miden_client::account::{StorageSlot, StorageSlotName};
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(description = "initialized")]
initialized: StorageValue<Word>,
/// Maps depositor AccountId -> balance (as Felt)
/// Key: [prefix, suffix, asset_prefix, asset_suffix]
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
}
#[component]
impl Bank {
/// Initialize the bank account, enabling deposits.
pub fn initialize(&mut self) {
// Get current value from storage
let current: Word = self.initialized.get();
// Check not already initialized
assert!(
current[0].as_canonical_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.set(initialized_word);
}
/// Get the balance for a depositor and specific asset type.
pub fn get_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
let key = Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3], // faucet_prefix
asset.key[2], // faucet_suffix
]);
self.balances.get(key)
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
Key Takeaways
#[component]marks structs and impl blocks as Miden account componentsStorageValue<Word>stores a single Word, read with.get(), write with.set()StorageMap<Word, Felt>stores key-value pairs, access with.get()and.set()- Storage slots are identified by name (IDs derived from hashed slot names), each holds 4 Felts (32 bytes)
- Public methods are callable by other contracts via generated bindings
See the complete bank account implementation in contracts/bank-account/src/lib.rs.
Next Steps
Now that you understand account components and storage, let's learn how to define business rules with Part 2: Constants and Constraints.