ABI Format
The generator reads a WASM-ABI v1 manifest: a JSON file describing a
Calimero contract’s public surface. Every shape here mirrors the embedded
wasm-abi-v1.schema.json; every example below passes --validate.
Manifest structure
Section titled “Manifest structure”interface AbiManifest { schema_version: 'wasm-abi/1'; // must be exactly this string types: Record<string, AbiTypeDef>; // named type definitions methods: AbiMethod[]; // callable contract methods events: AbiEvent[]; // emitted events state_root?: string; // type name backing contract state state_version?: number; // state schema version (≥ 1) migrations?: AbiMigrationEdge[]; // declared migration edges}schema_version, types, methods, and events are required — the schema
rejects the manifest without all four (even when types/methods/events are
empty). state_root, state_version, and migrations are optional.
Minimal example
Section titled “Minimal example”{ "schema_version": "wasm-abi/1", "types": { "SetEntryRequest": { "kind": "record", "fields": [ { "name": "key", "type": { "kind": "string" } }, { "name": "value", "type": { "kind": "string" } } ] } }, "methods": [ { "name": "get_value", "params": [{ "name": "key", "type": { "kind": "string" } }], "returns": { "kind": "string" }, "returns_nullable": true, "intent": "read_only" }, { "name": "set_value", "params": [{ "name": "req", "type": { "$ref": "SetEntryRequest" } }], "returns": { "kind": "unit" }, "intent": "mutating" } ], "events": []}Type references
Section titled “Type references”A type reference (AbiTypeRef) is one of: a scalar, a bytes type, a collection,
or a $ref to a named type.
Scalar types
Section titled “Scalar types”Each is an object with a single kind:
| Type | Meaning |
|---|---|
{ "kind": "bool" } |
Boolean |
{ "kind": "string" } |
UTF-8 string |
{ "kind": "unit" } |
Void / no value |
{ "kind": "i32" } / { "kind": "i64" } |
Signed 32- / 64-bit integer |
{ "kind": "u32" } / { "kind": "u64" } |
Unsigned 32- / 64-bit integer |
{ "kind": "f32" } / { "kind": "f64" } |
32- / 64-bit float |
{ "kind": "bytes" } // variable-length{ "kind": "bytes", "size": 32 } // fixed-length (size ≥ 1)encoding is an optional free-form hint (mirrors core’s BytesType); current
SDKs omit it. In generated code, bytes values are represented by the
CalimeroBytes helper class.
Collections
Section titled “Collections”| Kind | Shape |
|---|---|
list |
{ "kind": "list", "items": <type>, "crdt_type"?: <crdt> } |
map |
{ "kind": "map", "key": <type>, "value": <type>, "crdt_type"?: <crdt> } |
record |
{ "kind": "record", "fields": [<field>…], "crdt_type"?: <crdt>, "inner_type"?: <type> } |
tuple |
{ "kind": "tuple", "elements": [<type>…] } (at least one element) |
$ref |
{ "$ref": "TypeName" } — reference to a named type |
A $ref normally names an entry in types, but it may also name a raw Rust
type the generator recognizes (String, bool, u8…u64, i8…i64,
f32/f64, and generic forms like Vec<T>, Option<T>, Result<T, E>, (),
and tuples). Those are mapped straight to TypeScript instead of resolved against
types.
CRDT-backed collections
Section titled “CRDT-backed collections”Any list, map, or record may carry a crdt_type marking it as a
CRDT-backed state collection. The schema accepts exactly these 12 values:
lww_register |
counter |
vector |
replicated_growable_array |
fugue_text |
authored_vector |
shared_storage |
unordered_map |
sorted_map |
authored_map |
unordered_set |
sorted_set |
Named type definitions
Section titled “Named type definitions”The types map defines reusable named types. A named AbiTypeDef is one of
four kinds: record, variant, alias, or bytes.
// Record (struct) — fields is an array of { name, type, nullable? }{ "kind": "record", "fields": [ { "name": "name", "type": { "kind": "string" } }, { "name": "count", "type": { "kind": "u32" } } ]}
// Variant (enum) — variants is an array of { name, code?, payload? }{ "kind": "variant", "variants": [ { "name": "Pending" }, { "name": "Done", "payload": { "kind": "string" } } ]}
// Alias — names another type{ "kind": "alias", "target": { "kind": "u64" } }
// Bytes — a named bytes type (e.g. a fixed-size hash){ "kind": "bytes", "size": 64 }Enum representation
Section titled “Enum representation”Field, variant and event names are serde’s wire names, so they may be camelCase or kebab-case.
The generator declares every object key under its wire name and quotes it when it is not a JS identifier.
A variant type may carry serde’s tagging keys; with none it is externally tagged, as before.
| Keys | Unit variant on the wire | Payload variant on the wire | Generated TS type |
|---|---|---|---|
| none | "Name" |
{ "Name": payload } |
{ name, payload } union plus factory |
tag |
{ tag: "Name" } |
{ tag: "Name", ...payload fields } |
the wire shape |
tag, content |
{ tag: "Name" } |
{ tag: "Name", content: payload } |
the wire shape |
untagged: true |
null |
the bare payload | the wire shape |
{ "kind": "variant", "tag": "kind", "content": "data", "variants": [ { "name": "NotFound", "payload": { "kind": "string" } }, { "name": "Done" } ]}generates
export type Outcome = | { kind: "NotFound"; data: string } | { kind: "Done" };content requires tag, and untagged excludes both; the schema rejects other combinations.
Bytes inside an untagged payload are not converted to CalimeroBytes, because the value does not say which variant it is, so the generated type keeps them as number[].
Aliases are newtypes, and they are branded
Section titled “Aliases are newtypes, and they are branded”kind: "alias" is emitted only for a Rust one-field tuple struct — a newtype
such as pub struct FolderId(pub String);.
A plain type FolderId = String cannot derive AbiType, so it never reaches
the manifest as a named type.
Every alias in a manifest is therefore a distinction the ABI author declared
deliberately.
Emitting export type FolderId = string would throw that distinction away:
FolderId, ContextId and any bare string would all be mutually assignable.
Aliases over a string or numeric primitive are instead emitted as a
branded type plus a constructor:
export type FolderId = string & { readonly __brand: 'FolderId' };export const FolderId = (value: string): FolderId => value as FolderId;The brand exists only in the type system — at runtime a FolderId is the
string it always was, and the JSON on the wire is unchanged.
Branding stops there, and deliberately:
| Alias target | Emitted as | Why |
|---|---|---|
string, i32/i64/u32/u64/f32/f64 |
branded | TypeScript would otherwise erase the distinction entirely |
bool |
plain alias | two inhabitants; nothing to confuse |
bytes |
plain alias (CalimeroBytes) |
already a class; an intersection would reject real instances |
list, map, tuple, inline record |
plain alias | structurally distinct already, and branding would force a cast on every literal |
$ref to a record or variant |
unchanged | records emit an interface and variants a discriminated union, both of which already carry their own shape |
$ref to another alias |
branded under its own name | struct A(B) and B are distinct types in Rust, so they are distinct here |
A newtype over a type the manifest does not define is left as a plain alias.
Note what branding cannot do: a field declared as a bare {"kind": "string"}
stays a bare string.
Branding recovers a distinction the ABI made; it cannot invent one the app
never declared.
Guarding such a field means giving it a newtype in the Rust source first.
How variants are emitted depends on their payloads:
- All-unit variants (no payloads) become a string-literal union, e.g.
type Status = 'Pending' | 'Active'— serde serializes these as bare strings. - Mixed / payload-bearing variants become a discriminated-union
<Name>Payloadtype plus a factoryconst <Name>with a constructor per variant.
A payload-bearing variant passed as a method parameter is rewritten to serde’s
{ Variant: payload } shape before the call, and a unit member of such a variant
to the bare string "Variant", so the ergonomic { name, payload } form never
reaches the node.
A newtype over a variant is rewritten the same way as the variant it wraps.
A response is decoded the other way, from the declared return type rather than
from the shape the JSON happens to have.
So a returned payload-bearing variant is untagged back to { name, payload },
a returned bytes becomes a CalimeroBytes, and a returned list<u32> stays
plain numbers even where it sits beside bytes.
Methods whose return type reaches neither bytes nor a payload-bearing variant
emit no conversion at all.
A null response is passed through untouched whether or not the method declares
returns_nullable.
One limit is worth knowing: a type that reaches itself is decoded only down to
the first cycle, so below that point values arrive as raw JSON and the declared
type is not honoured.
For a self-referential Node { hash: bytes, child: Node }, node.hash is a
CalimeroBytes but node.child.hash is still a plain array, and calling
.toArray() on it throws even though the type checker accepted it.
Decoding a map value also requires Object.fromEntries, so a consumer’s
tsconfig needs lib at ES2019 or newer.
Methods
Section titled “Methods”{ "name": "create_post", "params": [{ "name": "title", "type": { "kind": "string" } }], "returns": { "$ref": "Post" }, "returns_nullable": false, "intent": "mutating", "errors": [{ "code": "TITLE_TOO_LONG" }], "xcall_callable": false, "xcall_callers": "any_in_namespace"}Only name and params are required; everything else is optional. The example
above generates createPost(params: { title: string }): Promise<Post>.
Method intent
Section titled “Method intent”intent declares whether a method reads or writes state:
| Value | Meaning |
|---|---|
read_only |
Reads state only; no mutation. |
mutating |
Writes / changes state. |
unspecified |
Not declared. |
A method with no intent field is treated as potentially mutating (the node
defaults an absent intent to write intent). The generator surfaces a declared
intent as an @intent JSDoc tag on the method — documentation only; it does not
yet change how the method is called.
Cross-context calls (xcall)
Section titled “Cross-context calls (xcall)”Two optional fields describe cross-context invocation, declared by the app author:
| Field | Values | Meaning |
|---|---|---|
xcall_callable |
boolean |
Whether the method is a cross-context entry point. Absent/false means it is not. |
xcall_callers |
any_in_namespace | same_app |
Who may invoke it. any_in_namespace (the default) allows any context in the namespace; same_app restricts callers to contexts running the same application. Enforced by the node. |
The generator surfaces both on the method as an @xcall JSDoc tag, because
nothing in the emitted signature distinguishes a cross-context entry point from
an ordinary method:
/** * transfer * * @xcall same_app (callers must run the same application id) */public async transfer(params: { to: string }): Promise<void>An xcall_callable method with no declared xcall_callers is tagged
any_in_namespace, since that is the value the node applies. The tag is
documentation: the policy is enforced by the node, not by the generated client.
A TEE timer (#[app::tee(every = "..")]) carries its period:
| Field | Values | Meaning |
|---|---|---|
tee_every_secs |
integer, at least 1 |
The node’s TEE scheduler fires the method once per this many seconds, on one TEE authority. Absent for every other method. |
The generator tags such a method @tee every <n>s. Calling it from a client is
refused inside the method: only the TEE scheduler may run it.
State versioning & migrations
Section titled “State versioning & migrations”When a contract’s state schema evolves, the manifest records the current
state_version and the migration edges that reach it. Each edge names the
migration method and the version it upgrades from (note the camelCase wire
key fromVersion):
{ "state_version": 3, "migrations": [ { "method": "migrate_v1_to_v2", "fromVersion": 1 }, { "method": "migrate_v2_to_v3", "fromVersion": 2 } ]}Modelled as AbiMigrationEdge { method: string; fromVersion: number }. Both
state_version and migrations are optional, top-level fields.
A method named by a migration edge is tagged on the emitted client, so it is not mistaken for ordinary app API:
/** * migrate_v1_to_v2 * * @migration from state version 1 to 2 — invoked by the node during upgrade, not by app code */state_version itself is not emitted: it describes the deployed state, not the
client’s call surface.
Events
Section titled “Events”{ "name": "PostCreated", "payload": { "$ref": "PostCreatedPayload" }}An event has a name and an optional payload (a single type reference —
typically a $ref to a record). For an event with a payload, the generator
emits a <EventName>Payload type alias; it also emits a single AbiEvent union
over all events, e.g.:
export type PostCreatedPayload = { /* … */};
export type AbiEvent = | { name: 'PostCreated'; payload: PostCreatedPayload } | { name: 'PostDeleted' };Events whose payload is inline unit (or absent) omit the payload field in
the union.
Documentation (doc)
Section titled “Documentation (doc)”Methods, parameters, named record, variant and alias types, record fields, variants and events may carry an optional doc string: the author’s rustdoc for that item.
A bytes type never does, named or inline: it declares nothing a doc could attach to.
The SDK moves a method’s # Arguments entries onto the matching parameters, so a method’s doc holds the rest of its rustdoc.
An item without documentation has no doc key.
{ "name": "get_folder", "doc": "Look up one folder.\n\n# Errors\nFails when no folder has this id.", "params": [ { "name": "id", "type": { "$ref": "FolderId" }, "doc": "Handle returned by an earlier call." } ]}The generator emits these as JSDoc, so editors show them on hover:
/** * get_folder * * Look up one folder. * * # Errors * Fails when no folder has this id. * * @param params.id Handle returned by an earlier call. */A method may also carry returns_doc (its # Returns section), destructive: true, idempotent: true and handler: true (an #[app::handler] event handler); each is absent otherwise.
They are emitted as @returns <returns_doc> and one @remarks line naming the flags that are set, e.g. @remarks destructive, idempotent.
Type and field docs are emitted above the declaration they describe.
A variant member’s doc is emitted above its factory function, or above its tag property when the enum carries tag.
An event’s doc is emitted above the name property of its AbiEvent member.
Member docs of a unit-only or untagged variant and the doc of a named bytes type have no declaration to attach to and are not emitted.