SDK Macros
An app is a Rust library built into a calimero:app component. It declares
itself with the attributes calimero-sdk re-exports under calimero_sdk::app
(defined in crates/sdk-macros) and #[derive(Record)]. One import brings them
all in scope:
use calimero_sdk::prelude::*;Summary
Section titled “Summary”| Attribute | On | Does |
|---|---|---|
#[app::state(version = N)] |
struct | The state root: tables and collections. |
#[app::logic] |
impl of the state |
Every pub fn becomes a method; generates the handle export and the ABI. |
#[app::init] |
method | The constructor, run once when the app is created (as the owner). |
#[app::view] |
&self method |
A read: runs as a view, never as a write. |
#[app::confirm] |
method | The caller confirms with a fresh passkey assertion. |
#[app::public] / #[app::public(rate = "10/min")] |
method | Callable without signing in (on a public app), optionally rate-limited. |
#[app::every("1m")] |
method | A timer the node runs on that interval (<n>s, <n>m or <n>h). |
#[app::allow(system)] |
method | The node itself may call it (implied by every). |
#[app::allow(app = "<id>")] / allow(app = "*") |
method | Another app may call it with a cross-app call. |
#[app::action] |
method | An action: may fetch over HTTP and read and call the app, outside the log. |
#[app::allow(action)] |
method | Only the app’s own actions may call it. |
#[app::error] |
enum | The app’s errors: variant name = error code. |
#[app::event] |
enum | The app’s events: variant name = event name. |
#[derive(Record)] + #[table(..)] |
struct | A table row: key, unique constraints, indexes, counts, search. |
app::bail!(err) |
expression | Return early with an app error. |
A complete app
Section titled “A complete app”The sample guest the SDK’s own tests run (crates/sdk/tests/guests/sample),
shortened:
use calimero_sdk::prelude::*;
#[derive(Record)]#[table( key = "id", unique(slot = ["room_id", "start"]), index(by_start = ["checked_in", "start"]), count(by_room = ["room_id"]),)]pub struct Booking { pub id: String, pub room_id: String, pub start: u64, pub title: String, pub checked_in: bool,}
#[app::state(version = 3)]pub struct Rooms { bookings: Table<Booking>, titles: UnorderedMap<String, String>, next_id: Counter,}
#[app::error]pub enum Error { NotFound, Taken { start: u64 }, Invalid(String),}
#[app::event]pub enum Event { Booked { id: String, room_id: String, start: u64 }, Released { id: String },}
#[app::logic]impl Rooms { #[app::init] pub fn init() -> app::Result<Self> { Ok(Self::load()) }
pub fn book(&mut self, room_id: String, start: u64, title: String) -> app::Result<String> { if title.is_empty() { app::bail!(Error::Invalid("empty title".into())); } let id = env::unique_id(); self.bookings.insert(Booking { id: id.clone(), room_id: room_id.clone(), start, title, checked_in: false })?; env::emit(Event::Booked { id: id.clone(), room_id, start }, Audience::All)?; Ok(id) }
#[app::view] #[app::public] pub fn room_count(&self, room_id: String) -> app::Result<u64> { Ok(self.bookings.count("byRoom", &[KeyPart::Str(room_id)])?) }
#[app::confirm] pub fn admin_release(&mut self, id: String) -> app::Result<()> { if !self.bookings.remove(&id)? { app::bail!(Error::NotFound); } env::emit(Event::Released { id }, Audience::All)?; Ok(()) }
#[app::every("30s")] pub fn heartbeat(&mut self) -> app::Result<()> { Ok(()) }}#[app::state(version = N)]
Section titled “#[app::state(version = N)]”The struct holds handles only: each field is a table or collection,
built from its camelCase field name, so Self::load() reads nothing. version
is recorded in the ABI. The fields and their kinds (table, unorderedMap,
counter, cell, set, vector, frozen, perUser, text, richText) are
listed in the ABI too.
#[app::logic]
Section titled “#[app::logic]”On the state’s impl block. Every pub fn is a method, named in camelCase in
the ABI (room_count → roomCount); private fns stay helpers. Methods take
&mut self (a call), &self with #[app::view] (a view), or nothing
(#[app::init], which returns Self). Every method returns app::Result<T>.
The generated handle:
- Arguments are a JSON object keyed by camelCase parameter names; unknown
keys are refused (
BadArgs). - 64-bit integers and bytes:
u64,i64andVec<u8>(andOptionof each) travel as decimal strings and base64url without padding, in arguments, results, record fields, and error and event fields, so no JavaScript client loses precision. The same types nested in another type the macro sees (Vec<u64>,Option<Vec<i64>>) are a compile error: wrap them in aRecordor use a string. - Kinds: a view may run as a call; a call run as a view is
NotAView; an unknown method isUnknownMethod. - Callers are checked after the kind:
allow(system)admits the node,allow(app = ..)another app,publica caller who is not signed in.
Method attributes
Section titled “Method attributes”#[app::init]runs once, as the owner, and takes no other attribute.#[app::view]makes the method a read.#[app::confirm]asks the node to require a fresh passkey assertion for the call.#[app::public]lets a caller who is not signed in reach the method on a public app;rate = "<n>/s|min|h"limits it.#[app::every("<n>s|m|h")]is a timer. It impliesallow(system). Inside it,env::timer()gives the tick (scheduled, andpreviouswhen there was one).#[app::allow(app = "<app id>")], or"*"for any app, admits cross-app calls.#[app::action]takes noself; its first parameter isctx: &mut ActionCtx, withctx.view,ctx.callandctx.fetch(see Outbound).#[app::allow(action)]on a method lets only the app’s own actions call it.
#[app::error] and #[app::event]
Section titled “#[app::error] and #[app::event]”Both go in the #[app::logic] module, at most one of each.
#[app::error]implementsFrom<Error> for AppError. The variant name is the code; the fields become the error’smessage, a JSON object with camelCase keys (Taken { start: 7 }→ codeTaken,{"start":"7"}).#[app::event]implementsAppEvent, emitted withenv::emit(event, audience)(see Events).
app::bail!(Error::NotFound) returns Err(Error::NotFound.into()).
#[derive(Record)] and #[table(..)]
Section titled “#[derive(Record)] and #[table(..)]”A table row. The derive writes the serde impls itself, so do not also derive
Serialize/Deserialize on the record.
| Key | Meaning |
|---|---|
key = "field" |
The primary key. |
unique(name = ["f", ..]) |
A unique constraint; an insert that breaks it fails. |
index(name = ["f", ..]) |
An index, read with Table::find. |
count(name = ["f", ..]) |
A count kept on every write, read with Table::count. |
search = ["f", ..] |
Fields indexed for full-text search. |
Index, count and unique names are used in camelCase (by_room → "byRoom").
u64/i64 fields are CBOR integers when stored and decimal strings in JSON.
The SDK’s name
Section titled “The SDK’s name”The crate is calimero-sdk. Its old name, calimero-app-sdk, still works while
apps move over: it re-exports calimero-sdk, and the attributes expand to paths
through whichever name the app’s Cargo.toml lists (or the name a renamed
dependency gives it).