#[contract]Expand description
Turn a trait, impl block, or free function into contract entrypoints, generating exports, a typed client, and spec registration.
§State
Methods taking &self/&mut self load the contract’s state from
storage: the self type must be a storage struct (annotate it with
#[storage]; a unit struct like struct Token; works for contracts
that keep no state in the struct). Contracts with only associated
functions need no storage annotation.
§Method visibility (bare impl blocks)
In a bare (non-trait) impl block, pub methods become entrypoints
and private methods are skipped (use them for helpers). Restricted
visibility (pub(crate), pub(super), pub(in ...)) is a compile
error because the intent is ambiguous. Trait impls export every
trait method regardless of visibility.
§Arguments
version = "x.y.z", interface = "Name", exports = "mod_name",
client = ClientIdent, throws = Error / throws(E1, E2),
emits(Event1, Event2).
Trait implementations inherit the trait’s interface name, version, and
entrypoint IDs, including when the trait comes from another crate or is
imported under an alias. A plain #[contract] impl Trait for Contract
is sufficient. version is not allowed on trait impls. An explicit
interface argument is an optional assertion that the name matches the trait.
For trait definitions, bare impls, and free functions, the version still
defaults to the declaring crate’s CARGO_PKG_VERSION.
No throws(...) type may contain a thruput_sdk::signer::Signed: abort
frames carry no auth-token handles, so such a payload could only decode
to an uninitialized sentinel. Spec canonicalization rejects it when the
contract is built. See #[contract_exception].
§Declared method effects
On trait methods and public methods of bare impls, use
#[contract(effect = "pure")], #[contract(effect = "readOnly")], or
#[contract(effect = "stateChanging")]. Omission leaves the effect absent, never
read-only. Trait implementations inherit declarations from the trait;
annotating an implementation method is rejected.
Effects cover the whole invocation, including nested calls. pure uses
only arguments and constants, without state or execution-context reads.
readOnly permits those reads but no application effects. stateChanging
permits storage deltas, events, transfers and deployment; it need not
change state on every call. Event emission is an application effect.
These are author declarations. The macro checks syntax and duplicates, not method behavior; neither the compiler nor runtime enforces the declared effects. Metadata does not change entrypoint IDs, execution paths, authorization, gas accounting, or whether a call can fail.
§Limitations
Generic parameters — including explicit lifetime parameters — are not
supported on #[contract] items or their methods: generated clients,
entrypoint wrappers, and spec consts are non-generic, so a spliced
T or 'a cannot resolve there. Elided borrow lifetimes (&self,
&[u8]) are fine, and #[derive(Event)] types may carry lifetime
parameters (folded to 'static in the spec). Associated types are not
supported in #[contract] traits.
A struct must carry only one #[contract] impl block: depending on
configuration a second block either collides on generated items
(E0428 on names you never wrote) or compiles while silently merging
the entrypoint surface — neither is supported.
§Interface id
A #[contract] trait gains an associated
INTERFACE_ID: thruput_sdk::InterfaceId, derived from the effective
interface name and major version (the pair that also namespaces its
entrypoint IDs), and the generated <Iface>Entrypoints wrapper carries
the same constant. The name is reserved on contract traits. Every
#[contract] impl block also records its interface’s id in the
contract’s interface table (thruput_sdk::interface_table), which is what
thruput-erc165-if answers supports_interface from.
§Storage-domain collision detection
When a contract uses multiple storage structs, #[contract] generates a
lib test that verifies their domain strings are all distinct. This check
runs as a #[cfg(test)] lib test; a crate that sets [lib] test = false
in its Cargo.toml, or that only runs an external test harness without
compiling lib tests, will silently skip this collision check at build time.