Skip to main content
Version: 0.15 (unstable)

Part 4: Note Scripts

In this section, you'll learn how to write note scripts - code that executes when a note is consumed by an account. We'll create the deposit note that lets users deposit tokens into the bank.

What You'll Build in This Part

By the end of this section, you will have:

  • Created the deposit-note contract
  • Understood the #[note] struct+impl pattern and #[note_script] method attribute
  • Used active_note APIs to access sender and assets
  • Built the note script and its dependencies
  • Verified it works with a complete deposit flow test

Building on Part 3

In Part 3, we completed the bank's deposit method. Now we need a way to trigger it:

Part 3:                          Part 4:
┌──────────────────┐ ┌──────────────────┐
│ Bank (complete) │ │ Bank (complete) │
│ ─────────────────│ │ ─────────────────│
│ + deposit() │ │ + deposit() │
│ + withdraw() │ │ + withdraw() │
└──────────────────┘ └──────────────────┘

│ calls
┌────────────────────┐
│ deposit-note │ ◄── NEW
│ (note script) │
└────────────────────┘

Note Scripts vs Account Components

FeatureAccount ComponentNote Script
PurposePersistent account logicOne-time execution when consumed
StorageHas persistent storageNo storage (reads from note data)
Attribute#[component]#[note] struct + #[note_script] method
Entry pointMethods on structfn run(self, _arg: Word)
InvocationCalled by other contractsExecutes when note is consumed

Note scripts are like "messages" that carry code along with data and assets.

Step 1: Create the Deposit Note Project

First, create the deposit-note contract. If you used miden new, you may have an increment-note folder - rename or replace it:

>_ Terminal
# Remove or rename the example
rm -rf contracts/increment-note
# Or: mv contracts/increment-note contracts/increment-note-backup

# Create the deposit-note directory
mkdir -p contracts/deposit-note/src

Step 2: Configure Cargo.toml

Create the Cargo.toml for the deposit note:

contracts/deposit-note/Cargo.toml
[package]
name = "deposit-note"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

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

[package.metadata.component]
package = "miden:deposit-note"

[package.metadata.miden]
project-kind = "note-script"

# Dependencies on account components
[package.metadata.miden.dependencies]
"miden:bank-account" = { path = "../bank-account" }

[package.metadata.component.target.dependencies]
"miden:bank-account" = { path = "../bank-account/target/generated-wit/" }

Key configuration:

  • project-kind = "note-script" - Marks this as a note script
  • Dependencies sections declare which accounts it can interact with

Step 3: Implement the Deposit Note

Create the note script implementation:

contracts/deposit-note/src/lib.rs
#![no_std]
#![feature(alloc_error_handler)]

use miden::*;

// Import the bank account's generated bindings
use crate::bindings::miden::bank_account::bank_account;

/// Deposit Note Script
///
/// When consumed by the Bank account, this note transfers all its assets
/// to the bank and credits the depositor (note sender) with the deposited amount.
#[note]
struct DepositNote;

#[note]
impl DepositNote {
#[note_script]
fn run(self, _arg: Word) {
// The depositor is whoever created/sent this note
let depositor = active_note::get_sender();

// Get all assets attached to this note
let assets = active_note::get_assets();

// Deposit each asset into the bank
for asset in assets {
bank_account::deposit(depositor, asset);
}
}
}

The crate::bindings::miden::bank_account::bank_account import and bank_account::deposit() call use Miden's cross-component binding system. We'll explain exactly how this works in Part 5: Cross-Component Calls. For now, just know that building bank-account first generates WIT files that deposit-note imports.

The #[note] and #[note_script] Attributes

The #[note] attribute is applied to both a unit struct and its impl block to define a note script. Within the impl block, the #[note_script] attribute marks the entry point method. The function signature is always:

fn run(self, _arg: Word)

The method takes self as its first parameter. The _arg parameter can pass additional data, but we don't use it in the deposit note.

Note Context APIs

The active_note module provides APIs to access note data during execution:

get_sender() - Who Created the Note

let depositor = active_note::get_sender();

Returns the AccountId of the account that created/sent the note. In our bank:

  • The sender is the depositor
  • Their ID is used to credit their balance

get_assets() - Attached Assets

let assets = active_note::get_assets();
for asset in assets {
// Process each asset
}

Returns an iterator over all assets attached to the note.

get_storage() - Note Parameters

let storage = active_note::get_storage();
let first_item = storage[0];

Returns a slice of Felt values passed when the note was created. We'll use storage items in the withdraw request note (Part 7).

Step 4: Build the Note Script

Build account components first before building note scripts that depend on them. The note script needs the generated WIT files from the account.

>_ Terminal
# First, ensure bank-account is built (generates WIT files)
cd contracts/bank-account
miden build

# Now build the deposit note
cd ../deposit-note
miden build
Expected output
   Compiling deposit-note v0.1.0
Finished `release` profile [optimized] target(s)
Creating Miden package /path/to/miden-bank/target/miden/release/deposit_note.masp

Execution Flow Diagram

1. User creates deposit note with 100 tokens attached
┌───────────────────────────────────────┐
│ Note: deposit-note │
│ Sender: User's AccountId │
│ Assets: [100 tokens] │
└───────────────────────────────────────┘

2. Bank account consumes the note
┌───────────────────────────────────────┐
│ Bank receives assets into vault │
│ Note script executes... │
└───────────────────────────────────────┘

3. Note script runs
depositor = get_sender() → User's AccountId
assets = get_assets() → [100 tokens]
bank_account::deposit(depositor, 100 tokens)

4. Bank's deposit() method executes
- Validates initialization and amount
- Updates balance: balances[User] += 100
- Adds asset to vault

Try It: Verify Deposits Work

First, verify your deposit-note builds successfully:

>_ Terminal
# Ensure bank-account is built first
cd contracts/bank-account && miden build

# Then build deposit-note
cd ../deposit-note && miden build

This is the first runnable test in the tutorial. It verifies the deposit flow end-to-end — building the bank and deposit-note contracts, creating a deposit, and checking the balance.

The initialization guard (require_initialized()) is intentionally commented out at this tutorial stage. We'll enable it in Part 6 when we build the init transaction script.

Create the test file:

The snippet below illustrates the deposit happy-path. The shipped repository's examples/miden-bank/integration/tests/deposit_test.rs is the source of truth and additionally exercises failure paths (deposit_exceeds_max_should_fail, deposit_without_init_should_fail).

integration/tests/deposit_test.rs (illustrative — see shipped file for the full version)
use integration::helpers::{
build_project_in_dir, create_testing_account_from_package,
create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig,
};
use miden_client::account::{component::{InitStorageData, StorageValueName}, StorageSlotName};
use miden_client::asset::{Asset, FungibleAsset};
use miden_client::auth::AuthSchemeId;
use miden_client::note::NoteAssets;
use miden_client::transaction::{RawOutputNote, TransactionScript};
use miden_client::{Felt, Word};
use miden_testing::{Auth, MockChain};
use std::{path::Path, sync::Arc};

#[tokio::test]
async fn deposit_test() -> anyhow::Result<()> {
// =========================================================================
// SETUP: Build contracts and create mock chain
// =========================================================================
let mut builder = MockChain::builder();

// Create a faucet for test tokens
let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, "TEST", 10_000_000, Some(10))?;

// Create sender (depositor) wallet
let sender = builder.add_existing_wallet_with_assets(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, [FungibleAsset::new(faucet.id(), 1000)?.into()])?;

// Build bank-account and deposit-note only (no init-tx-script needed)
let bank_package = Arc::new(build_project_in_dir(
Path::new("../contracts/bank-account"),
true,
)?);

let deposit_note_package = Arc::new(build_project_in_dir(
Path::new("../contracts/deposit-note"),
true,
)?);

// Create the bank account with storage slots.
//
// Part 4 does NOT run the init transaction script — we exercise the deposit
// flow directly against the bank's commented-out `require_initialized()`
// guard. Part 6 enables the guard and adds the init step.
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 mut bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg)?;
builder.add_account(bank_account.clone())?;

// Create the deposit note
let deposit_amount: u64 = 1000;
let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?;
let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?;

let deposit_note = create_testing_note_from_package(
deposit_note_package.clone(),
sender.id(),
NoteCreationConfig {
assets: note_assets,
..Default::default()
},
)?;

builder.add_output_note(RawOutputNote::Full(deposit_note.clone()));
let mut mock_chain = builder.build()?;

// =========================================================================
// EXECUTE DEPOSIT (no init needed — guard is commented out at this stage)
// =========================================================================
let tx_context = mock_chain
.build_tx_context(bank_account.id(), &[deposit_note.id()], &[])?
.build()?;

let executed_transaction = tx_context.execute().await?;
bank_account.apply_delta(&executed_transaction.account_delta())?;
mock_chain.add_pending_executed_transaction(&executed_transaction)?;
mock_chain.prove_next_block()?;

println!("Deposit transaction executed!");

// =========================================================================
// VERIFY: Check balance was updated
// =========================================================================
let depositor_key = Word::from([
sender.id().prefix().as_felt(),
sender.id().suffix(),
faucet.id().prefix().as_felt(),
faucet.id().suffix(),
]);

let balance = bank_account.storage().get_map_item(&balances_slot, depositor_key)?;
let balance_value = balance[3].as_canonical_u64();

println!("Depositor balance: {}", balance_value);
assert_eq!(
balance_value,
deposit_amount,
"Balance should equal deposited amount"
);

println!("\nPart 4 deposit test passed!");
Ok(())
}

Run the test from the project root:

>_ Terminal
cargo test --package integration --test deposit_test -- --nocapture
Expected output
   Compiling integration v0.1.0 (/path/to/miden-bank/integration)
Finished `test` profile [unoptimized + debuginfo] target(s)
Running tests/deposit_test.rs

running 3 tests
test deposit_test ... ok
test deposit_exceeds_max_should_fail ... ok
test deposit_without_init_should_fail ... ok

test result: ok. 3 passed; 0 failed; 0 ignored

Preview: Withdraw Request Note

For withdrawals, we'll use note inputs to pass parameters. Here's a preview of the withdraw request note (implemented in Part 7):

contracts/withdraw-request-note/src/lib.rs (preview)
/// Withdraw Request Note Script
///
/// # Note Storage (14 Felts)
/// [0-3]: withdraw asset encoded as [amount, 0, faucet_suffix, faucet_prefix]
/// [4-7]: serial_num (random/unique per note)
/// [8]: tag (P2ID note tag for routing)
/// [9]: note_type (1 = Public, 2 = Private)
/// [10-13]: P2ID script_root (MAST root of the P2ID note script, Poseidon2-hashed)
#[note]
struct WithdrawRequestNote;

#[note]
impl WithdrawRequestNote {
#[note_script]
fn run(self, _arg: Word) {
// Get the 14 storage items and validate the expected count.
let storage = active_note::get_storage();
assert!(
storage.len() == 14,
"Withdraw request requires exactly 14 storage items"
);

// Parse parameters from storage
let withdraw_asset = Asset::new(
Word::from([felt!(0), felt!(0), storage[2], storage[3]]),
Word::from([storage[0], felt!(0), felt!(0), felt!(0)]),
);

let serial_num = Word::from([
storage[4], storage[5], storage[6], storage[7]
]);

let tag = storage[8];
let note_type = storage[9];

// Note: P2ID script root (storage[10..13]) is read by the bank account
// directly from the active note's storage inside bank_account::withdraw.

// The bank identifies the depositor internally via active_note::get_sender()
bank_account::withdraw(withdraw_asset, serial_num, tag, note_type);
}
}

Note inputs are limited. Keep your input layout compact. See Common Pitfalls for stack-related constraints.

Complete Code for This Part

Click to expand deposit-note/src/lib.rs
contracts/deposit-note/src/lib.rs
#![no_std]
#![feature(alloc_error_handler)]

use miden::*;

use crate::bindings::miden::bank_account::bank_account;

/// Deposit Note Script
#[note]
struct DepositNote;

#[note]
impl DepositNote {
#[note_script]
fn run(self, _arg: Word) {
let depositor = active_note::get_sender();
let assets = active_note::get_assets();

for asset in assets {
bank_account::deposit(depositor, asset);
}
}
}

Key Takeaways

  1. #[note] marks the struct and impl block, with #[note_script] on the entry point method fn run(self, _arg: Word)
  2. active_note::get_sender() returns who created the note
  3. active_note::get_assets() returns assets attached to the note
  4. active_note::get_storage() returns parameterized data
  5. Note scripts execute once when consumed - no persistent state
  6. Build order matters - account components first, then note scripts

See the complete note script implementations:

Next Steps

Now that you understand note scripts, let's learn how they call account methods in Part 5: Cross-Component Calls.