Skip to main content
Version: 0.15

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 StorageMap works for tracking depositor balances
  • Implemented a get_depositor_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_depositor_balance() │ ◄── NEW
│ + require_initialized() │
└──────────────────────────────────┘

The #[component] Attributes

A bank component is described with three attributes that work together:

  • #[component_storage] marks the struct that declares the persistent storage fields
  • #[component] on a trait declares the component's exported API
  • #[component] on the impl Trait for Storage block implements that API

When you compile with miden build, the macros generate:

  • WIT (WebAssembly Interface Types) bindings for cross-component calls
  • MASM (Miden Assembly) code for the account logic
  • Storage slot management code

Only the methods declared in the #[component] trait are exported. Private helpers live in a separate plain impl BankStorage block (covered later in this tutorial).

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. The storage struct is marked with #[component_storage]. Here is 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::*;

use miden::Felt;

/// Storage layout for the bank account component.
#[component_storage]
struct BankStorage {
/// 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, faucet ID) -> balance (as Felt).
/// Key: [depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]
#[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 {package_name}::{component_struct}::{field_name} — here bank_account::bank::initialized and bank_account::bank::balances.

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).
// The bank keys balances per (depositor, faucet): asset.key[3] is the faucet
// id prefix and asset.key[2] is the faucet id suffix (plus a metadata byte).
let key = Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3],
asset.key[2],
]);

// 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 = Felt::new(balance.as_canonical_u64() + deposit_amount.as_canonical_u64()).unwrap();
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:

NameTypePurpose
initializedStorageValue<Word>Initialization flag
balancesStorageMap<Word, Felt>Depositor balances

The description attribute generates named slot identifiers (e.g., 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 exported API is declared as a #[component] trait, and the #[component] attribute is used again on the impl Bank for BankStorage block that implements it:

contracts/bank-account/src/lib.rs
/// API of the bank account component.
#[component]
trait Bank {
/// Initialize the bank account, enabling deposits.
fn initialize(&mut self);

/// Get the bank-tracked balance for a depositor and specific asset type.
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt;
}

#[component]
impl Bank for BankStorage {
fn initialize(&mut self) {
// Check not already initialized
let current: Word = self.initialized.get();
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);
}

fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
self.balances.get(BankStorage::balance_key(depositor, &asset))
}
}

Note the method is named get_depositor_balance, not get_balance: the account wrapper generates a built-in ActiveAccount::get_balance vault method, so reusing that name would collide with it.

The private helpers (balance_key, require_initialized, …) are not part of the exported API, so they live in a separate plain impl BankStorage block — the #[component] macro only exports the trait methods:

contracts/bank-account/src/lib.rs
/// Internal helpers that are not part of the component's exported WIT API.
impl BankStorage {
/// Derive the `balances` map key identifying a (depositor, faucet) pair:
/// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`.
fn balance_key(depositor: AccountId, asset: &Asset) -> Word {
Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3],
asset.key[2],
])
}

/// 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"
);
}
}

A fungible asset's vault key Word is [asset_id_suffix, asset_id_prefix, faucet_suffix | metadata_byte, faucet_prefix]. So asset.key[3] is the faucet id prefix and asset.key[2] is the faucet id suffix folded together with a metadata byte (composition + callback flag) in its low 8 bits — key[2] is not the raw faucet suffix. For the callbacks-disabled fungible assets this bank accepts the metadata byte is constant, so (key[3], key[2]) is a stable per-faucet identifier. The host-side mirror is FungibleAsset::to_key_word() indices [3]/[2].

The bank requires initialization before accepting deposits: require_initialized() is called at the top of deposit() and withdraw() (covered in later parts).

Exported vs Internal Methods

  • Trait methods (declared in the #[component] trait) are exposed in the generated WIT interface and can be called by other contracts
  • Inherent helpers (in the plain impl BankStorage) are internal and cannot be called from the outside
// Exported: Can be called by note scripts and other contracts
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { ... }

// 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

The build prints non-fatal MAST-serialization ERROR lines on every run. These are cosmetic — the build still succeeds and produces the .masp package.

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:

  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, 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("bank_account::bank::initialized")
.expect("Valid slot name");
let balances_slot =
StorageSlotName::new("bank_account::bank::balances")
.expect("Valid slot name");

// The `initialized` value slot has no schema default, so it MUST be seeded
// here — otherwise AccountComponent::from_package fails with InitValueNotProvided.
// Only the `balances` map defaults to empty.
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:

>_ Terminal
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
contracts/bank-account/src/lib.rs
#![no_std]
#![feature(alloc_error_handler)]

#[macro_use]
extern crate alloc;

use miden::*;

use miden::Felt;

/// Storage layout for the bank account component.
#[component_storage]
struct BankStorage {
/// 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, faucet ID) -> balance (as Felt).
/// Key: [depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
}

/// API of the bank account component.
#[component]
trait Bank {
/// Initialize the bank account, enabling deposits.
fn initialize(&mut self);

/// Get the bank-tracked balance for a depositor and specific asset type.
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt;
}

#[component]
impl Bank for BankStorage {
fn initialize(&mut self) {
// Check not already initialized
let current: Word = self.initialized.get();
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);
}

fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
self.balances.get(BankStorage::balance_key(depositor, &asset))
}
}

/// Internal helpers that are not part of the component's exported WIT API.
///
/// The `#[component]` macro exports only the methods of the `Bank` trait, so these
/// inherent methods stay private to the contract.
impl BankStorage {
/// Derive the `balances` map key identifying a (depositor, faucet) pair:
/// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`.
fn balance_key(depositor: AccountId, asset: &Asset) -> Word {
Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3],
asset.key[2],
])
}

/// 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

  1. #[component] marks structs and impl blocks as Miden account components
  2. StorageValue<Word> stores a single Word, read with .get(), write with .set()
  3. StorageMap<Word, Felt> stores key-value pairs, access with .get() and .set()
  4. Storage slots are identified by name (IDs derived from hashed slot names), each holds 4 Felts (32 bytes)
  5. Trait methods (declared in the #[component] trait) are callable by other contracts via generated bindings; private helpers live in a plain impl block

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.