Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ web-client/playwright-report/
web-client/test-results/
**/.DS_Store
.DS_Store
.claude/
.playwright-mcp/
8 changes: 4 additions & 4 deletions docs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ version = "0.1.0"
edition = "2021"

[dependencies]
miden-client = { version = "0.13.0", features = ["testing", "tonic"] }
miden-client-sqlite-store = { version = "0.13.0", package = "miden-client-sqlite-store" }
miden-protocol = { version = "0.13.0" }
miden-client = { version = "0.14", features = ["testing", "tonic"] }
miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" }
miden-protocol = { version = "0.14" }
rand = { version = "0.9" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] }
tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] }
rand_chacha = "0.9.0"
41 changes: 20 additions & 21 deletions docs/src/miden-bank/00-project-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
miden = { version = "0.10" }
miden = { version = "0.12" }

[package.metadata.component]
package = "miden:bank-account"
Expand Down Expand Up @@ -141,30 +141,30 @@ 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: Value,
initialized: StorageValue<Word>,

/// Maps depositor AccountId -> balance (as Felt).
/// We'll use this to track user balances in Part 1.
#[storage(description = "balances")]
balances: StorageMap,
balances: StorageMap<Word, Felt>,
}

#[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();
// Get current value from storage
let current: Word = self.initialized.get();

// Check not already initialized
assert!(
current[0].as_u64() == 0,
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.write(initialized_word);
self.initialized.set(initialized_word);
}

/// Get the balance for a depositor.
Expand All @@ -174,15 +174,15 @@ impl Bank {
/// in at least one public method.
pub fn get_balance(&self, depositor: AccountId) -> Felt {
let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]);
self.balances.get(&key)
self.balances.get(key)
}
}
```

This is our starting point with two storage slots:

- `initialized`: A `Value` slot to track whether the bank is ready
- `balances`: A `StorageMap` to track user balances (we'll use this starting in Part 1)
- `initialized`: A `StorageValue<Word>` slot to track whether the bank is ready
- `balances`: A `StorageMap<Word, Felt>` to track user balances (we'll use this starting in Part 1)

:::note Compiler Requirement
Account components must use WIT binding types (like `AccountId`, `Asset`, etc.) in at least one public method signature for the compiler to generate the required bindings correctly. The `get_balance` method serves this purpose.
Expand All @@ -209,7 +209,7 @@ edition = "2021"
```

:::info Contracts Are Excluded
In v0.13, contracts are excluded from the Cargo workspace and built independently by `cargo miden`. Each contract specifies its own `miden` dependency directly. Only the `integration` crate remains a workspace member.
In v0.14, contracts are excluded from the Cargo workspace and built independently by `cargo miden`. Each contract specifies its own `miden` dependency directly. Only the `integration` crate remains a workspace member.
:::

## Step 5: Build and Verify
Expand Down Expand Up @@ -260,25 +260,24 @@ async fn test_bank_account_builds_and_loads() -> anyhow::Result<()> {

// Create named storage slots matching the contract's storage layout
let initialized_slot =
StorageSlotName::new("miden::component::miden_bank_account::initialized")
StorageSlotName::new("miden_bank_account::bank::initialized")
.expect("Valid slot name");
let balances_slot =
StorageSlotName::new("miden::component::miden_bank_account::balances")
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 {
storage_slots: vec![
StorageSlot::with_value(initialized_slot, Word::default()),
StorageSlot::with_map(
balances_slot,
StorageMap::with_entries([]).expect("Empty storage map"),
),
],
init_storage_data,
..Default::default()
};

let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg).await?;
create_testing_account_from_package(bank_package.clone(), bank_cfg)?;

// Verify the account was created
println!("Bank account created with ID: {:?}", bank_account.id());
Expand Down
111 changes: 55 additions & 56 deletions docs/src/miden-bank/01-account-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ By the end of this section, you will have:
In Part 0, we created a minimal bank with just an `initialized` flag. Now we'll add balance tracking:

```text
Part 0: Part 1:
┌────────────────────┐ ┌──────────────────────────┐
│ Bank │ │ Bank │
│ ───────────────── │ ──► │ ──────────────────────── │
│ initialized (Value)│ │ initialized (Value)
│ │ │ balances (StorageMap) │ ◄── NEW
└────────────────────┘ └──────────────────────────┘
Part 0: Part 1:
┌──────────────────────────────┐ ┌──────────────────────────────────┐
│ Bank │ │ Bank
│ ──────────────────────── │ ──► │ ────────────────────────────
│ initialized (StorageValue) │ │ initialized (StorageValue<Word>)
│ │ balances (StorageMap<Word, Felt>)│ ◄── NEW
└──────────────────────────────┘ └──────────────────────────────────┘
```

## The #[component] Attribute
Expand Down Expand Up @@ -60,12 +60,12 @@ 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: Value,
initialized: StorageValue<Word>,

/// Maps depositor AccountId -> balance (as Felt)
/// Key: [prefix, suffix, asset_prefix, asset_suffix]
#[storage(description = "balances")]
balances: StorageMap,
balances: StorageMap<Word, Felt>,
}
```

Expand All @@ -75,44 +75,44 @@ We've added a `StorageMap` that will track each depositor's balance. The compile

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
### StorageValue Storage

The `Value` type provides access to a single storage slot:
The `StorageValue<Word>` type provides access to a single storage slot:

```rust
#[storage(description = "initialized")]
initialized: Value,
initialized: StorageValue<Word>,
```

Use `Value` when you need to store a single `Word` of data.
Use `StorageValue<Word>` when you need to store a single `Word` of data.

**Reading and writing:**

```rust
// Read returns a Word
let current: Word = self.initialized.read();
// Get returns a Word
let current: Word = self.initialized.get();

// Check the first element (our flag)
if current[0].as_u64() == 0 {
if current[0].as_canonical_u64() == 0 {
// Not initialized
}

// Write a new value
// Set a new value
let new_value = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(new_value);
self.initialized.set(new_value);
```

:::tip Type Annotations
The `.read()` method requires a type annotation: `let current: Word = self.initialized.read();`
The `.get()` method requires a type annotation: `let current: Word = self.initialized.get();`
:::

### StorageMap

The `StorageMap` type provides key-value storage within a slot:
The `StorageMap<Word, Felt>` type provides key-value storage within a slot:

```rust
#[storage(description = "balances")]
balances: StorageMap,
balances: StorageMap<Word, Felt>,
```

Use `StorageMap` when you need to store multiple values indexed by keys.
Expand Down Expand Up @@ -144,12 +144,12 @@ Unlike `Value::read()` which returns a `Word`, `StorageMap::get()` returns a sin

Plan your storage layout carefully:

| Name | Type | Purpose |
| ------------- | ------------ | ------------------- |
| `initialized` | `Value` | Initialization flag |
| `balances` | `StorageMap` | Depositor balances |
| Name | Type | Purpose |
| ------------- | ------------------------ | ------------------- |
| `initialized` | `StorageValue<Word>` | Initialization flag |
| `balances` | `StorageMap<Word, Felt>` | Depositor balances |

The `description` attribute generates named slot identifiers (e.g., `miden::component::miden_bank_account::initialized`) used in tests to reference specific slots. The compiler auto-assigns slot numbers based on field order.
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 auto-assigns slot numbers based on field order.

## Step 2: Implement Component Methods

Expand All @@ -160,31 +160,31 @@ Now let's add methods to our Bank. The `#[component]` attribute is also used on
impl Bank {
/// Initialize the bank account, enabling deposits.
pub fn initialize(&mut self) {
// Read current value from storage
let current: Word = self.initialized.read();
// Get current value from storage
let current: Word = self.initialized.get();

// Check not already initialized
assert!(
current[0].as_u64() == 0,
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.write(initialized_word);
self.initialized.set(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)
self.balances.get(key)
}

/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.read();
let current: Word = self.initialized.get();
assert!(
current[0].as_u64() == 1,
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
Expand Down Expand Up @@ -249,27 +249,26 @@ async fn test_bank_account_storage() -> anyhow::Result<()> {
)?);

// Create named storage slots matching the contract's storage layout
// The naming convention is: miden::component::{package_name_underscored}::{field_name}
// The naming convention is: {package_name}::{component_struct}::{field_name}
let initialized_slot =
StorageSlotName::new("miden::component::miden_bank_account::initialized")
StorageSlotName::new("miden_bank_account::bank::initialized")
.expect("Valid slot name");
let balances_slot =
StorageSlotName::new("miden::component::miden_bank_account::balances")
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 {
storage_slots: vec![
StorageSlot::with_value(initialized_slot.clone(), Word::default()),
StorageSlot::with_map(
balances_slot.clone(),
StorageMap::with_entries([]).expect("Empty storage map"),
),
],
init_storage_data,
..Default::default()
};

let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg).await?;
create_testing_account_from_package(bank_package.clone(), bank_cfg)?;

// =========================================================================
// VERIFY: Check initial storage state
Expand All @@ -285,7 +284,7 @@ async fn test_bank_account_storage() -> anyhow::Result<()> {

println!("Bank account created successfully!");
println!(" Account ID: {:?}", bank_account.id());
println!(" Initialized flag: {:?}", initialized_value[0].as_int());
println!(" Initialized flag: {:?}", initialized_value[0].as_canonical_u64());

// =========================================================================
// VERIFY: Storage slots are correctly configured
Expand Down Expand Up @@ -365,43 +364,43 @@ 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: Value,
initialized: StorageValue<Word>,

/// Maps depositor AccountId -> balance (as Felt)
/// Key: [prefix, suffix, asset_prefix, asset_suffix]
#[storage(description = "balances")]
balances: StorageMap,
balances: StorageMap<Word, Felt>,
}

#[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();
// Get current value from storage
let current: Word = self.initialized.get();

// Check not already initialized
assert!(
current[0].as_u64() == 0,
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.write(initialized_word);
self.initialized.set(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)
self.balances.get(key)
}

/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.read();
let current: Word = self.initialized.get();
assert!(
current[0].as_u64() == 1,
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
Expand All @@ -413,8 +412,8 @@ impl Bank {
## 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()`
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 (auto-assigned by compiler), each holds 4 Felts (32 bytes)
5. **Public methods** are callable by other contracts via generated bindings

Expand Down
Loading