0xmiden/compiler/rust-sdk-patterns

Claude Code and agent skill by 0xmiden/compiler

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Complete guide to writing Miden smart contracts with the Rust SDK. Covers the three-part #[component_storage]/#[component] account-component pattern, the #[account_procedure] interface marker, #[note]/#[note_script] notes, #[tx_script] scripts, the #[account(...)] wrapper and its generated traits, 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 co…

Install

npx vskill@latest install 0xmiden/compiler/rust-sdk-patterns

SKILL.md

Miden Rust SDK Patterns

Three Contract Types

Account Component (three-part pattern)

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

An account component is written as three parts — the storage struct is annotated #[component_storage], and #[component] applies to the API trait and the impl block:

  1. #[component_storage] on the storage struct — declares typed #[storage(...)] fields and derives slot names.
  2. #[component] on a trait — the component's exported API (this is the source of the generated WIT interface).
  3. #[component] on the `impl Trait for Storage` block — the behavior, wired to the guest bindings.
rust
#![no_std]
#![feature(alloc_error_handler)]
#[macro_use]
extern crate alloc;
use miden::*;
#[component_storage]
struct BankStorage {
#[storage(description = "initialized")]
initialized: StorageValue<Word>,
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
}
#[component]
trait Bank {
#[account_procedure]
fn initialize(&mut self);
#[account_procedure]
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset);
}
#[component]
impl Bank for BankStorage {
fn initialize(&mut self) { /* read/write self.initialized, etc. */ }
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { /* ... */ }
}

Only the trait's methods are exported to WIT. Inherent (impl BankStorage) methods stay private to the contract — use them for helpers like key derivation.

Reference: examples/basic-wallet/src/lib.rs and examples/counter-contract/src/lib.rs in the compiler repo.

#[account_procedure]: being exported is not the same as being callable

A component's methods are not implicitly part of the account interface. Mark every method that must be reachable from a transaction script, a note script, foreign procedure invocation (FPI), or a sibling component with #[account_procedure].

Three rules that catch people out:

  • It goes on the `#[component]` trait declaration, not on the impl block. Putting it on the impl does nothing.
  • An unmarked method still compiles and is still exported by the package — it simply is not an account procedure. There is no error at build time; the call site fails later.
  • `#[account_procedure]` and `#[auth_script]` cannot be combined in one component. They belong to different component kinds: an ordinary account component versus an authentication component. An authentication component uses #[auth_script] alone, and its single method is the interface implicitly.

#[account_procedure] needs no import — the enclosing #[component] macro recognises it, exactly as it does #[auth_script].

Any number of methods may be marked.

Project metadata for accounts: [lib] needs kind, namespace, and an explicit path; [dependencies] needs miden-core and miden-protocol:

toml
# miden-project.toml, matching contracts/counter-account/miden-project.toml
[package]
name = "counter-account"
version = "0.1.0"
[lib]
path = "src/lib.rs"
kind = "account-component"
namespace = "miden::counter_account::counter_contract"
[dependencies]
miden-core = "*"
miden-protocol = "*"
[package.metadata.miden]
supported-types = ["RegularAccountImmutableCode"]

supported-types also accepts "RegularAccountUpdatableCode" and the faucet kinds ["FungibleFaucet", "NonFungibleFaucet"].

The project-template contract Cargo.toml files currently use edition = "2021", crate-type = ["cdylib"], and miden / miden-sdk-build-script-support 0.15.0-rc.2 dependencies. Copy the local manifests unless intentionally changing the template line:

toml
[package]
name = "counter-account"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
miden = { version = "0.15.0-rc.2" }
[build-dependencies]
miden-sdk-build-script-support = { version = "0.15.0-rc.2" }

Contracts build on nightly-2026-09-01 with target wasm32-wasip2. Use the midenup / cargo-miden command documented in this repository's README.md and CLAUDE.md; the local build.rs calls miden_sdk_build_script_support::prepare_package_cache() so source dependencies are available to the SDK macros during Cargo checks and IDE analysis.

Note Script (#[note] / #[note_script])

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

A note is two parts: a #[note] struct (the note inputs type) and a #[note] impl block containing exactly one #[note_script] entrypoint. The entrypoint takes self by value, exactly one Word argument, and optionally a single reference to an #[account(...)] wrapper (&MyAccount or &mut MyAccount). The consuming account is declared separately with #[account(...)].

rust
#![no_std]
#![feature(alloc_error_handler)]
use miden::*;
#[account(counter_account::CounterContract)]
pub struct Wallet;
#[note]
struct IncrementNote;
#[note]
impl IncrementNote {
#[note_script]
fn run(self, _arg: Word, account: &mut Wallet) {
let initial_value = account.get_count();
account.increment_count();
let expected_value = initial_value + Felt::from_u32(1);
let final_value = account.get_count();
assert_eq(final_value, expected_value);
}
}

A #[note] struct with fields is auto-decoded from active_note::get_storage(). The decoder is strict: it calls ensure_eof(), so surplus felts in the note's storage fail with FeltReprError::TrailingData. A zero-sized note type skips get_storage() entirely.

Reference: examples/p2id-note/src/lib.rs, examples/p2ide-note/src/lib.rs, examples/counter-note/src/lib.rs.

Project metadata for notes: [lib] kind = "note", plus namespace and path. Conventional namespace shape is miden::<pkg>::<pkg> (snake_case package name).

Transaction Script (#[tx_script])

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

#[tx_script] annotates a free fn run. Its signature is fn run(arg: Word) or fn run(arg: Word, account: &mut MyAccount) where MyAccount is an #[account(...)] wrapper. There is no crate::bindings::Account — you declare the account wrapper yourself and the macro instantiates it as the active account.

rust
#![no_std]
#![feature(alloc_error_handler)]
use miden::*;
#[account(bank_account::Bank)]
pub struct Wallet;
#[tx_script]
fn run(_arg: Word, account: &mut Wallet) {
account.initialize();
}

Reference: examples/basic-wallet-tx-script/src/lib.rs.

Project metadata for tx scripts: like a note, but [lib] kind = "tx-script" and namespace = "miden::<pkg>::<pkg>", plus path = "src/lib.rs". The script's entrypoint is exported at <namespace>::run.

#[account(...)] generates one trait per interface

#[account(pkg::Interface)] does not generate inherent methods on the wrapper struct. It generates one trait per referenced interface, named after the interface and carrying the wrapper's visibility, and implements it for the wrapper. Single-component accounts still call account.method(..) unchanged — as long as the generated trait is in scope.

The consequences worth knowing before you hit them:

  • The wrapper struct must not share a name with any generated trait. #[account(counter_contract::CounterContract)] struct CounterContract; is a hard error. Rename the struct (e.g. Counter).
  • A call site in a different module than the wrapper must `use` the generated trait. A same-module #[note] / #[tx_script] entrypoint sees it automatically.
  • A referenced interface must export at least one method, or #[account(...)] errors.
  • Wrappers are module-scope only.
  • Clashing method names are disambiguated with UFCS: <Wallet as BasicWallet>::deposit(account, asset). Generated traits are same-module so they need no import — but disambiguating against an ActiveAccount built-in does: use miden::active_account::ActiveAccount;.
  • Clashing trait names are renamed with `as`: #[account(counter_contract::CounterContract as RemoteCounter)]. The path still selects the interface.
  • A component method named `new` is now legal. Wallet::new(id) resolves to the inherent constructor, wallet.new() to the component method.

Storage Slot Naming

Storage slot names are part of the on-chain storage ABI and are derived as:

<[lib].namespace>::<field_name>

The prefix is the three-segment [lib].namespace Miden path from `miden-project.toml` (e.g. miden::counter_contract::counter_contract) — not the struct name. This deliberately decouples slot names from private Rust renames, and there is no slot(...) attribute. The same namespace names every exported procedure: <namespace>::<method>.

Live values from the examples: miden::counter_contract::counter_contract::count_map, miden::auth_component_rpo_falcon512::auth_component::owner_public_key.

See the rust-sdk-pitfalls skill (P5) for more on slot naming.

Storage Types

TypeUsageReadWrite
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

K: WordKey, and T/V: WordValue. WordValue is implemented for Word, Felt, AssetAmount, Digest, AccountId, Recipient, Tag, NoteIdx, NoteType; WordKey for the same set minus Digest and Recipient.

Native Function Modules

ModuleKey FunctionsPurpose
native_account::add_asset(Asset) -> Word, remove_asset(Asset) -> Word, incr_nonce() -> Nonce, get_id() -> AccountId, get_initial_asset(AssetId) -> Word, get_initial_commitment() -> Word, compute_commitment() -> Word, has_state_changed() -> bool, was_procedure_called(Word) -> bool, compute_delta_commitment() -> WordModify / read the native account
active_account::get_id() -> AccountId, get_nonce() -> Nonce, get_asset(asset_id: AssetId) -> Word, has_asset(asset_id: AssetId) -> bool, get_vault_root() -> Word, get_num_procedures() -> u32, get_procedure_root(u32) -> Word, has_procedure(Word) -> boolQuery the active account
active_note::get_storage() -> Vec<Felt>, get_initial_assets() -> Vec<Asset>, get_sender() -> AccountId, get_recipient() -> Recipient, get_metadata() -> NoteMetadata, find_attachment(Felt) -> Option<u32>, write_attachment_to_memory(u32) -> Vec<Word>Query the note being consumed
note::build_recipient(Word, Word, Vec<Felt>) -> RecipientBuild note recipients from serial number, script root, and note storage
output_note::create(Tag, NoteType, Recipient) -> NoteIdx, add_asset(Asset, NoteIdx), the *_attachment familyCreate output notes
faucet::mint(Asset), burn(Asset)Move assets in and out of existence
tx::get_reference_block_number() -> BlockNumber, get_block_timestamp() -> u32, get_num_input_notes() -> u32, get_num_output_notes() -> u32, get_expiration_block_delta() -> u16, update_expiration_block_delta(u16), execute_foreign_procedure(..)Transaction context and FPI
Intrinsicsassert(Felt), assertz(Felt), assert_eq(Felt, Felt)Validation (assert fails unless the felt equals 1; assertz fails unless it equals 0)

add_asset, remove_asset and the active_account queries are also trait methods auto-implemented on the #[component_storage] struct, so the idiomatic body is self.add_asset(asset) rather than the free function.

Three context restrictions the compiler will not catch

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
116
Repository forks
83
7-day trend score
29

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/0xmiden/compiler/rust-sdk-patterns/badge)](https://verified-skill.com/skills/0xmiden/compiler/rust-sdk-patterns)

Something wrong with this skill or its page? Tell us.