Skip to content

How your app describes its ABI

Your app’s ABI is a machine-readable description of everything it exposes: its methods with their argument and return types, the events it emits, and the shape and version of its persistent state. It ships with the code, embedded in the compiled wasm.

Three consumers read it. A caller - meroctl, a frontend, any client - reads methods to build a correctly typed request and decode the reply without ever seeing your Rust. A code generator reads the same entries to emit a typed client. And the node reads it on upgrade: the state portion is what the no-silent-downgrade rail and the migration replay compare between the installed version and the new one, so an ABI that describes the wrong state shape is not a documentation bug, it is a wrong migration plan.

This page is about how that description is produced from your code. For the manifest’s field-by-field format, see App interface (ABI).

There is no schema file to maintain and nothing parses your source. Each type that can appear in an ABI position implements the AbiType trait, which answers two questions: how a use site refers to the type, and what named definition (if any) it contributes to the manifest. Because the compiler picks the impls, an alias is already the aliased type, a re-export resolves to its definition, and a macro-generated type has already expanded by the time it is asked to describe itself.

The SDK covers everything you do not own - the std scalars and collections, the CRDT collections from calimero-storage, and the id types (AccountId, ContextId, BlobId, PublicKey). Your own types are covered like this:

Item Where its AbiType impl comes from
#[app::state] struct derived for you by the attribute
#[app::event] enum derived for you by the attribute
any other struct or enum in a method signature, an event payload, or a state field one line: #[derive(AbiType)]
use calimero_sdk::abi::AbiType;
#[derive(AbiType)]
pub struct Profile {
display_name: Option<String>,
joined_at: u64,
}

#[app::private] is deliberately absent from that table: node-local state is never synchronized and never described.

#[app::logic] generates a hidden __calimero_abi() that walks those impls and assembles the manifest. It is cfg-gated and compiled in only when the toolchain asks for it, which a wasm build never does - so your bytecode is unaffected, and an app pays nothing at runtime for describing itself.

cargo mero build extracts the manifest by compiling and running that entry point on the host, writes it to res/abi.json, and embeds a canonicalized copy into the wasm as the calimero_abi_v1 custom section. The section is part of the module’s bytes, so it is covered by the blob’s content hash and cannot drift from the code it describes. See the cargo mero toolchain for the rest of the pipeline.

Usually nothing. State and events are covered by their own attributes and every SDK and std type already describes itself, so #[derive(AbiType)] is for the auxiliary structs and enums you introduce.

Forget one and it is a compile error naming the type, not a silent gap in the manifest:

error[E0277]: the trait bound `Profile: AbiType` is not satisfied
| ^^^^^^^^^^^^^^ the trait `AbiType` is not implemented for `Profile`

The fix is the derive. With it, the Profile above lands in the manifest’s types as:

"Profile": {
"kind": "record",
"fields": [
{ "name": "display_name", "nullable": true, "type": { "kind": "string" } },
{ "name": "joined_at", "type": { "kind": "u64" } }
]
}

Note what happened to Option<String>: the described type is the unwrapped string, and the optionality rides a nullable flag on the field, while a non-optional field carries no nullable key at all. The same rule applies to parameters and returns, so a client reads nullability off the position rather than off the type.

A type’s manifest name defaults to its Rust identifier. #[abi(name = "...")] overrides it:

#[derive(AbiType)]
#[abi(name = "UserProfile")]
pub struct Profile {
display_name: Option<String>,
joined_at: u64,
}

A name is the manifest’s only handle on a type, so two different shapes may not share one. If they do, the build fails during extraction with:

ABI type name collision: Profile defined with two different shapes.

There are two ways out, depending on what actually collided:

  • Two distinct types with the same identifier, say a Profile in two modules. Give one of them a different manifest identity with #[abi(name = "...")]; the Rust names stay as they are.
  • One generic used at two argument types. A generic is described at its instantiation under its bare identifier, so Wrapper<String> and Wrapper<u64> both want to be Wrapper. Define a separate concrete type per instantiation so every shape has its own name.

cargo mero abi reads a compiled module or the artifacts beside it:

Terminal window
cargo mero abi extract res/my_app.wasm --output abi.json # the full manifest
cargo mero abi types res/my_app.wasm # just the named types
cargo mero abi state res/my_app.wasm # state root + its dependencies
cargo mero abi inspect res/my_app.wasm # the wasm's sections
cargo mero abi diff res/state-schema.json baseline.json # breaking + unsafe changes

diff is the one worth wiring into CI. It applies the same classification the node’s upgrade gate does, flagging a breaking field change and, more importantly, an unsafe identity downgrade - an AuthoredMap, AuthoredVector, or SharedStorage field replaced by a plain type, which silently strips per-entry authorship or the writer ACL. Catching that before a release is much cheaper than after an install.