Contract ABI
Introduction
A contract ABI is a representation of a Starknet contract interface. It is formatted as JSON and describes the functions, structs and events which are defined in the contract.
You can get the contract’s ABI by using starknet-compile
:
cargo run --bin starknet-compile -- --single-file </path/to/input.cairo> </path/to/output.json>
An example contract ABI
The following is an example contract ABI:
-
Cairo v2
-
Cairo v1
[
{
"type": "impl",
"name": "CounterContract",
"interface_name": "new_syntax_test_contract::new_syntax_test_contract::ICounterContract"
},
{
"type": "interface",
"name": "new_syntax_test_contract::new_syntax_test_contract::ICounterContract",
"items": [
{
"type": "function",
"name": "increase_counter",
"inputs": [
{
"name": "amount",
"type": "core::integer::u128"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "decrease_counter",
"inputs": [
{
"name": "amount",
"type": "core::integer::u128"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "get_counter",
"inputs": [],
"outputs": [
{
"type": "core::integer::u128"
}
],
"state_mutability": "view"
}
]
},
{
"type": "constructor",
"name": "constructor",
"inputs": [
{
"name": "initial_counter",
"type": "core::integer::u128"
},
{
"name": "other_contract_addr",
"type": "core::starknet::contract_address::ContractAddress"
}
]
},
{
"type": "event",
"name": "new_syntax_test_contract::new_syntax_test_contract::counter_contract::CounterIncreased",
"kind": "struct",
"members": [
{
"name": "amount",
"type": "core::integer::u128",
"kind": "data"
}
]
},
{
"type": "event",
"name": "new_syntax_test_contract::new_syntax_test_contract::counter_contract::CounterDecreased",
"kind": "struct",
"members": [
{
"name": "amount",
"type": "core::integer::u128",
"kind": "data"
}
]
},
{
"type": "event",
"name": "new_syntax_test_contract::new_syntax_test_contract::counter_contract::Event",
"kind": "enum",
"variants": [
{
"name": "CounterIncreased",
"type": "new_syntax_test_contract::new_syntax_test_contract::counter_contract::CounterIncreased",
"kind": "nested"
},
{
"name": "CounterDecreased",
"type": "new_syntax_test_contract::new_syntax_test_contract::counter_contract::CounterDecreased",
"kind": "nested"
}
]
}
]
[
{
"type": "function",
"name": "constructor",
"inputs": [
{
"name": "name_",
"type": "core::felt252"
},
{
"name": "symbol_",
"type": "core::felt252"
},
{
"name": "decimals_",
"type": "core::integer::u8"
},
{
"name": "initial_supply",
"type": "core::integer::u256"
},
{
"name": "recipient",
"type": "core::starknet::contract_address::ContractAddress"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "get_name",
"inputs": [],
"outputs": [
{
"type": "core::felt252"
}
],
"state_mutability": "view"
},
{
"type": "function",
"name": "get_symbol",
"inputs": [],
"outputs": [
{
"type": "core::felt252"
}
],
"state_mutability": "view"
},
{
"type": "function",
"name": "get_decimals",
"inputs": [],
"outputs": [
{
"type": "core::integer::u8"
}
],
"state_mutability": "view"
},
{
"type": "function",
"name": "get_total_supply",
"inputs": [],
"outputs": [
{
"type": "core::integer::u256"
}
],
"state_mutability": "view"
},
{
"type": "function",
"name": "balance_of",
"inputs": [
{
"name": "account",
"type": "core::starknet::contract_address::ContractAddress"
}
],
"outputs": [
{
"type": "core::integer::u256"
}
],
"state_mutability": "view"
},
{
"type": "function",
"name": "allowance",
"inputs": [
{
"name": "owner",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "spender",
"type": "core::starknet::contract_address::ContractAddress"
}
],
"outputs": [
{
"type": "core::integer::u256"
}
],
"state_mutability": "view"
},
{
"type": "function",
"name": "transfer",
"inputs": [
{
"name": "recipient",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "amount",
"type": "core::integer::u256"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "transfer_from",
"inputs": [
{
"name": "sender",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "recipient",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "amount",
"type": "core::integer::u256"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "approve",
"inputs": [
{
"name": "spender",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "amount",
"type": "core::integer::u256"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "increase_allowance",
"inputs": [
{
"name": "spender",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "added_value",
"type": "core::integer::u256"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "function",
"name": "decrease_allowance",
"inputs": [
{
"name": "spender",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "subtracted_value",
"type": "core::integer::u256"
}
],
"outputs": [],
"state_mutability": "external"
},
{
"type": "event",
"name": "Transfer",
"inputs": [
{
"name": "from",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "to",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "value",
"type": "core::integer::u256"
}
]
},
{
"type": "event",
"name": "Approval",
"inputs": [
{
"name": "owner",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "spender",
"type": "core::starknet::contract_address::ContractAddress"
},
{
"name": "value",
"type": "core::integer::u256"
}
]
}
]
Cairo v2.3.0 changes
Nested events
With Cairo v2.3.0
the limitations on the Event
enum have been relaxed, allowing more flexibility on the events that can be emitted from a given contract.
For example:
-
It is no longer enforced that the
Event
enum variants are structs of the same name as the variant, they can now be a struct or an enum of any name. -
Enum variants inside event ABI entries (entries in the abi with
"type": "event"
and"kind": "enum"
) now have two possible kinds. Beforev2.3.0
it was always"kind": "nested"
, now"kind: "flat"
is also possible. -
v2.3.0
is backward compatible with version ≥2.0.0
ABI, so the same structure of the ABI is kept, while allowing flexibility.
Between versions With |
To illustrate this, consider the following example:
//high-level code defining the events
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ComponentEvent: test_component::Event,
TestCounterIncreased: CounterIncreased,
TestCounterDecreased: CounterDecreased,
TestEnum: MyEnum
}
#[derive(Drop, starknet::Event)]
struct CounterIncreased {
amount: u128
}
#[derive(Drop, starknet::Event)]
struct CounterDecreased {
amount: u128
}
#[derive(Copy, Drop, starknet::Event)]
enum MyEnum {
Var1: MyStruct
}
#[derive(Copy, Drop, Serde, starknet::Event)]
struct MyStruct {
member: u128
}
Variant names different from types
In v2.3.0
enum variant types can now have any name.
As an example the TestCounterIncreased
variant and the CounterIncreased
type, as they appear in the ABI:
{
"type": "event",
"name": "<namespace>::Event",
"kind": "enum",
"variants": [
{
"name": "ComponentEvent",
"type": "<namespace>::test_component::Event",
"kind": "nested"
},
{
"name": "TestCounterIncreased",
"type": "<namespace>::CounterIncreased",
"kind": "nested"
},
{
"name": "TestCounterDecreased",
"type": "<namespace>::CounterDecreased",
"kind": "nested"
},
{
"name": "TestEnum",
"type": "<namespace>::MyEnum",
"kind": "nested"
}
]
},
{
"type": "event",
"name": "<namespace>::CounterIncreased",
"kind": "struct",
"members": [
{
"name": "amount",
"type": "core::integer::u128",
"kind": "data"
}
]
}
When the contract emits the TestCounterIncreased
event, for example by writing self.emit(CounterIncreased { amount }))
, the event that is emitted has the following keys and data:
-
One key based on the variant name:
sn_keccak(TestCounterIncreased)
. This information only appears in the<namespace>::Event
type entry in the ABI, as the nameTestCounterIncreased
does not appear in the"kind": "struct"
ABI entry. This did not matter in previous versions when the variant name and type had to be equal. -
One data element based on the struct
CounterIncreased
which is associated withTestCounterIncreased
via one of theEvent
type variants.
Enum variants inside Event
The introduction of components allows variants of Event
to be enums.
In the following example, we have two such variants: TestEnum
(unrelated to components) and ComponentEvent
.
The serialization to keys and data is the same in both cases, so this example will focus on TestEnum
:
This example shows the TestEnum
variant entry inside Event:
{
"name": "TestEnum",
"type": "<namespace>::MyEnum",
"kind": "nested"
}
This example shows the MyEnum
event entry:
{
"type": "event",
"name": "<namespace>::MyEnum",
"kind": "enum",
"variants": [
{
"name": "Var1",
"type": "<namespace>::MyStruct",
"kind": "nested"
}
]
}
This example shows the MyStruct
event entry:
{
"type": "event",
"name": "<namespace>::MyStruct",
"kind": "struct",
"members": [
{
"name": "member",
"type": "core::integer::u128",
"kind": "data"
}
]
}
If a |
When the event is emitted, the serialization to keys and data happens as follows:
-
Since the
TestEnum
variant haskind
nested, add the first key:sn_keccak(TestEnum)
, and the rest of the serialization to keys and data is done recursively via thestarknet::event
trait implementation ofMyEnum
. -
Next, you can handle a
"kind": "nested"
variant (previously it wasTestEnum
, now it’sVar1
), which means you can add another key depending on the sub-variant:sn_keccak(Var1)
, and proceed to serialize according to thestarknet::event
implementation ofMyStruct
. -
Finally, proceed to serialize
MyStruct
, which gives us a single data member.
This results in keys = [sn_keccak(TestEnum), sn_keccak(Var1)]
and data=[5]
Allowing variants that are themselves enums ( |
For example, if the high level code is changed to:
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ComponentEvent: test_component::Event,
TestCounterIncreased: CounterIncreased,
TestCounterDecreased: CounterDecreased,
TestEnum: MyEnum
}
#[derive(Copy, Drop, starknet::Event)]
enum MyEnum {
Var1: AnotherEnum
}
#[derive(Copy, Drop, Serde, starknet::Event)]
enum AnotherEnum {
Var2: MyStruct
}
#[derive(Copy, Drop, Serde, starknet::Event)]
struct MyStruct {
member: u128,
}
then self.emit(Event::TestEnum(MyEnum::Var1(AnotherEnum::Var2(MyStruct { member: 5 }))))
(as before, Into
implementations can shorten this) will emit an event with keys = [sn_keccak(TestEnum), sn_keccak(Var1), sn_keccak(Var2)]
and data=[5]
.
This will look as follows in the ABI (only the relevant parts are shown):
{
"type": "event",
"name": "<namespace>::Event",
"kind": "enum",
"variants": [
// ignoring all the other variants for brevity
{
"name": "TestEnum",
"type": "<namespace>::MyEnum",
"kind": "nested"
}
]
},
{
"type": "event",
"name": "<namespace>::MyEnum",
"kind": "enum",
"variants": [
{
"name": "Var1",
"type": "<namespace>::AnotherEnum",
"kind": "nested"
}
]
},
{
"type": "event",
"name": "<namespace>::AnotherEnum",
"kind": "enum",
"variants": [
{
"name": "Var2",
"type": "<namespace>::MyStruct",
"kind": "nested"
}
]
}
As TestEnum
, Var1
and Var2
are of kind nested
, a selector should be added to the list of keys, before continuing to recursively serialize.
Flattened enum variants
You might not want to nest enums when serializing the event. For example, if you write an ERC-20 as a component, not a contract, that is pluggable anywhere, you might not want the contract to modify the keys of known events such as Transfer
.
To avoid nesting, write the following high level code:
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ComponentEvent: test_component::Event,
TestCounterIncreased: CounterIncreased,
TestCounterDecreased: CounterDecreased,
#[flat]
TestEnum: MyEnum
}
By writing the above, the TestEnum
variant entry in the ABI will change to:
{
"name": "TestEnum",
"type": "<namespace>::MyEnum",
"kind": "flat"
}
This means that self.emit(Event::TestEnum(MyEnum::Var1(MyStruct {member: 5})))
will emit an event with keys=[sn_keccak(Var1)]
and data=[5]
.
Cairo v2.0.0 changes
With the transition to v2.0.0
, the contract ABI underwent some changes.
Consider the following high level code that generates the ABI in the following example:
#[starknet::interface]
trait IOtherContract<TContractState> {
fn decrease_allowed(self: @TContractState) -> bool;
}
#[starknet::interface]
trait ICounterContract<TContractState> {
fn increase_counter(ref self: TContractState, amount: u128);
fn decrease_counter(ref self: TContractState, amount: u128);
fn get_counter(self: @TContractState) -> u128;
}
#[starknet::contract]
mod counter_contract {
use starknet::ContractAddress;
use super::{
IOtherContractDispatcher, IOtherContractDispatcherTrait, IOtherContractLibraryDispatcher
};
#[storage]
struct Storage {
counter: u128,
other_contract: IOtherContractDispatcher
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CounterIncreased: CounterIncreased,
CounterDecreased: CounterDecreased
}
#[derive(Drop, starknet::Event)]
struct CounterIncreased {
amount: u128
}
#[derive(Drop, starknet::Event)]
struct CounterDecreased {
amount: u128
}
#[constructor]
fn constructor(
ref self: ContractState, initial_counter: u128, other_contract_addr: ContractAddress
) {
self.counter.write(initial_counter);
self
.other_contract
.write(IOtherContractDispatcher { contract_address: other_contract_addr });
}
#[external(v0)]
impl CounterContract of super::ICounterContract<ContractState> {
fn get_counter(self: @ContractState) -> u128 {
self.counter.read()
}
fn increase_counter(ref self: ContractState, amount: u128) {
let current = self.counter.read();
self.counter.write(current + amount);
self.emit(CounterIncreased { amount });
}
fn decrease_counter(ref self: ContractState, amount: u128) {
let allowed = self.other_contract.read().decrease_allowed();
if allowed {
let current = self.counter.read();
self.counter.write(current - amount);
self.emit(CounterDecreased { amount });
}
}
}
}
Interface and Impl ABI entries
Since the CounterContract
impl
is annotated with the #[external(v0)]
attribute, you’ll find the following impl
entry in the ABI:
{
"type": "impl",
"name": "CounterContract",
"interface_name": "new_syntax_test_contract::new_syntax_test_contract::ICounterContract"
}
----
This means that every function appearing in the ICounterContract
interface
is a possible entry point of the contract.
Standalone functions in the contract outside an external |
Events
In Cairo v2, a dedicated type for the contract’s events was introduced. Currently, the contract event type must be an enum named Event
, whose variants are structs of the same name as the variant. Types that can be emitted via self.emit(_)
must implement the Event
trait, which defines how this type should be serialized into two felt252
arrays, keys
and data
.
The Event
enum variants appear in the ABI under "type" = "event"
rather than regular structs.
For such entries, each member has an additional kind
field that specifies how the serialization into keys and data takes place:
-
If the kind is
key
, then this member or variant are serialized into the event’s keys. -
If the kind is
data
, then this member or variant are serialized into the event’s data. -
If the kind is
nested
, then the member or variant are serialized according to theEvent
attribute, potentially adding to both keys and data.
This feature is not yet supported, so no high level code written in Cairo |
Specification
You can find a JSON schema specification of the ABI in the starknet-specs
repository.
For a UI-friendly version, you can use the OPEN-RPC playground.