tomevault-io

rust-sdk-patterns

Complete guide to writing Miden smart contracts with the Rust SDK. Covers #[component], #[note], #[tx_script] macros, storage patterns, native functions, asset handling, cross-component calls, P2ID note creation, and asset receiving via component methods. Use when writing, editing, or reviewing Miden Rust contract code. Use when this capability is needed.

tomevault-io 1 Updated 1d ago
GitHub

Install

npx skillscat add tomevault-io/tomes/rust-sdk-patterns

Install via the SkillsCat registry.

SKILL.md

Miden Rust SDK Patterns

Three Contract Types

Account Component (#[component])

Defines reusable logic and storage for accounts. Accounts are composed of one or more components.

See counter-account/src/lib.rs for a working example demonstrating #[component], typed StorageMap<Word, Felt>, get()/set(), and felt arithmetic.

Cargo.toml for accounts: See counter-account/Cargo.toml for the required crate-type, miden dependency, component metadata, and project-kind.

Note Script (#[note])

Executes when a note is consumed by an account. Can call component methods on the consuming account.

See increment-note/src/lib.rs for a working example demonstrating #[note], #[note_script], and cross-component calls.

Cargo.toml for notes: See increment-note/Cargo.toml for the required miden deps, cross-component dependencies, wit deps, and project-kind = "note-script".

Transaction Script (#[tx_script])

One-off logic executed in the context of an account. Used for initialization, admin operations, etc.

#![no_std]
#![feature(alloc_error_handler)]
use miden::*;
use crate::bindings::Account;

#[tx_script]
fn run(_arg: Word, account: &mut Account) {
    account.initialize();
}

Cargo.toml: Same as account but with project-kind = "tx-script".

Storage Types

Type Usage Read Write
StorageValue<T> Single typed slot (flags, counters, IDs) .get() -> T .set(T) -> T
StorageMap<K, V> Typed key-value mapping (balances, records) .get(K) -> V .set(K, V) -> V

Native Function Modules

Module Key Functions Purpose
native_account:: add_asset(Asset), remove_asset(Asset), incr_nonce() Modify account vault/nonce
active_account:: get_id() -> AccountId, get_balance(AccountId) -> Felt Query current account
active_note:: get_assets() -> Vec<Asset>, get_sender() -> AccountId Query note being consumed (typed note storage arrives as self in the #[note_script] method; see "Cross-Component Note Pattern" below)
note:: build_recipient(Word, Word, Vec<Felt>) -> Recipient Build note recipients from serial number, script root, and note storage
output_note:: create(Tag, NoteType, Recipient) -> NoteIdx, add_asset(Asset, NoteIdx) Create output notes
faucet:: create_fungible_asset(Felt) -> Asset, mint(Asset), burn(Asset) Asset minting
tx:: get_block_number() -> Felt, get_block_timestamp() -> Felt Transaction context
Intrinsics assert(bool), assertz(Felt), assert_eq(Felt, Felt) Validation

Asset Handling

Asset is now a two-word value:

Constructor: Asset::new(word) creates an Asset from a Word.

See miden-bank bank-account for complete asset handling patterns including deposit, withdrawal, and balance tracking.

pub struct Asset {
    pub key: Word,
    pub value: Word,
}

For fungible assets, the amount lives in asset.value[0]. The asset class / vault identity lives in asset.key.

// Access fungible amount
let amount = asset.value[0];

// Keep the asset key if you need to persist or compare the asset class
let asset_key = asset.key;

// Add asset to account vault (only from component methods, not note scripts; see pitfall P11)
native_account::add_asset(asset);

// Remove asset from account vault
native_account::remove_asset(asset.clone());

P2ID Output Note Creation

To send assets to another account, create a P2ID (Pay-to-ID) output note. See miden-bank bank-account create_p2id_note() for a complete working implementation.

Cross-Component Dependencies

To call another component's methods from a note or tx script, declare the component under [dependencies] in miden-project.toml (for example counter-account = { path = "../counter-account" }). See increment-note/miden-project.toml for a working example; the interface comes from the dependency's compiled package, so no Cargo.toml metadata is needed.

Then import the bindings in your Rust code. See increment-note/src/lib.rs line 13 for the import pattern: use crate::bindings::miden::target_component::target_component;

Common Type Conversions

// Felt from integer
let f = felt!(42);                     // preferred for literals in contract code
let f = Felt::new(42);                 // construct a Felt from a u64
let f = Felt::from_u32(42);
let f = Felt::from_canonical_checked(42).unwrap();

// Word from Felts
let w = Word::from([f0, f1, f2, f3]);
let w = Word::new([f0, f1, f2, f3]);
let w = Word::from([0_u32, 0, 0, 1]);
let w = Word::try_from([0_u64, 0, 0, 1]).unwrap();

// Inspect a Word
let limbs: [Felt; 4] = w.into_elements();
let bytes: [u8; 32] = w.as_bytes();
let hex = w.to_hex();

// Felt to u64 (for comparisons and arithmetic safety)
let n: u64 = f.as_canonical_u64();

No-std Requirements

Every contract file must start with #![no_std] and #![feature(alloc_error_handler)]. See any contract in contracts/ for the pattern.

If you need heap allocation (Vec, String, etc.):

extern crate alloc;
use alloc::vec::Vec;

Cross-Component Note Pattern

A note script reads from active_note::* and forwards work to a public account-component method via generated bindings. This is the canonical pattern for any note that updates account state, because note scripts cannot call native_account::* directly (see rust-sdk-pitfalls skill, P11).

The #[note] macro generates TryFrom<&[Felt]> for the note struct, so the note's serialized storage is deserialized into typed fields before the script runs. The #[note_script] method receives the deserialized note as self (by value) and never indexes a raw Felt slice manually. Alongside the required Word arg, the method may optionally accept a &Account or &mut Account parameter. See compiler/sdk/base-macros/src/lib.rs for the macro contract and compiler/sdk/base-macros/src/note.rs for the generated deserialization (each named field is read via <T as miden::felt_repr::FromFeltRepr>::from_felt_repr(...) and EOF is asserted at the end).

Supported field types include Felt, the unsigned integer scalars (u64, u32, u8), bool, Option<T>, and Vec<T> via the FromFeltRepr trait (compiler/sdk/field-repr/repr/src/lib.rs), plus any user type that opts in with #[derive(FromFeltRepr)] (this is how AccountId supports the macro - see compiler/sdk/base-sys/src/bindings/types.rs). Do not use Asset or Word directly as note struct fields; those types do not currently derive FromFeltRepr. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep it on the side as a separate active_note::get_assets() read.

For Cargo.toml wiring (cross-component dependencies + bindings import), see "Cross-Component Dependencies" above. See increment-note/src/lib.rs for the project-template's local example of the #[note] struct + #[note] impl macro form.

Storage-free case (sender + assets, single component call per asset): declare a unit struct (#[note] struct DepositNote;). The script reads active_note::get_sender() and iterates active_note::get_assets(), calling the component method per asset. The macro still generates the deserialization wrapper; for a unit struct it only asserts the storage Felt slice is empty.

Typed-storage case (note carries scripted data): declare named fields on the note struct. The macro deserializes them in declaration order, and the script accesses them via self.<field>. Illustrative shape:

#[note]
struct DepositNote {
    depositor: AccountId,
}

#[note]
impl DepositNote {
    #[note_script]
    pub fn run(self, _arg: Word) {
        let assets = active_note::get_assets();
        for asset in assets {
            bank_account::deposit(self.depositor, asset);
        }
    }
}

(use statements and crate attributes elided; see increment-note/src/lib.rs for a complete file.) For a verified working example with an &mut Account parameter, see compiler/examples/p2id-note/src/lib.rs (#[note] struct P2idNote { target_account_id: AccountId }, where the script asserts account.get_id() == self.target_account_id and calls account.receive_asset(asset) for each attached asset).

Component side that absorbs the call: see miden-bank bank-account for deposit(...) and withdraw(...) in a fuller example. The component method validates (felt-arithmetic safety, see rust-sdk-pitfalls P1), updates storage, and (for withdraw) creates a P2ID output note via the existing P2ID pattern. Note: miden-bank currently demonstrates an older raw-indexing variant for its withdraw-request note; treat the typed pattern shown above as the preferred shape for new note scripts.

Test wiring: tests pass the serialized Felt representation of the note struct's fields through NoteCreationConfig.storage, in declaration order. See rust-sdk-testing-patterns skill, "Note Construction" section, for the helper that builds a note from a compiled .masp package and a populated NoteCreationConfig.

Asset Receiving via Component Methods

Note scripts cannot call native_account::add_asset() directly (see pitfall P11). The canonical pattern is for an account component to expose a public method that wraps native_account::add_asset(), and note scripts call that method via cross-component bindings.

See miden-bank bank-account deposit() for the component side: the deposit() method validates the deposit, updates storage, and calls native_account::add_asset().

See miden-bank deposit-note for the note side: the note script calls bank_account::deposit() via generated bindings.

Validation Checklist

  • #![no_std] and #![feature(alloc_error_handler)] at top of every contract
  • crate-type = ["cdylib"] in Cargo.toml
  • Correct project-kind in [package.metadata.miden]
  • Typed storage uses StorageValue<T> / StorageMap<K, V> with get() / set()
  • Cross-component deps declared under [dependencies] in miden-project.toml
  • Felt arithmetic validated before subtraction (see rust-sdk-pitfalls skill)
  • Felt comparisons use .as_canonical_u64() (see rust-sdk-pitfalls skill)

Source: 0xMiden/compiler — distributed by TomeVault.

Categories