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. Before v2.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 v2.0.0 and v2.2.0, to identify all potential serializations of events (what raw keys, data arrays can be emitted given the ABI), it was sufficient to iterate over the abi entries with "type": "event" and "kind": "struct", skipping the encapsulating Event type which has "kind": "enum".

With v2.3.0 onwards, doing so may result in losing information.

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 name TestCounterIncreased 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 with TestCounterIncreased via one of the Event 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 TestEnum event is being emitted via self.emit(Event::TestEnum(MyEnum::Var1(MyStruct {member: 5}))), you can implement the trait Into<MyStruct, Event> to avoid having to write it out in full.

When the event is emitted, the serialization to keys and data happens as follows:

  • Since the TestEnum variant has kind nested, add the first key: sn_keccak(TestEnum), and the rest of the serialization to keys and data is done recursively via the starknet::event trait implementation of MyEnum.

  • Next, you can handle a "kind": "nested" variant (previously it was TestEnum, now it’s Var1), which means you can add another key depending on the sub-variant: sn_keccak(Var1), and proceed to serialize according to the starknet::event implementation of MyStruct.

  • 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 (TestEnum is an enum variant here) means further nesting is possible.

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 impl can also be annotated with #[external(v0)] (currently, this is the only way to add L1 handlers). In such cases, a corresponding function (or l1_handler) entry will be found in the ABI in the same hierarchy as impls and interfaces.

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 the Event attribute, potentially adding to both keys and data.

This feature is not yet supported, so no high level code written in Cairo v2.0.0 can generate such an ABI.

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.