/starknet-abigen-rs

Exploratory work on abigen in rust for Starknet 🦀

Primary LanguageRustApache License 2.0Apache-2.0

⚠️ ARCHIVED ⚠️

This repo will no longer be maintained, in favor of it's new version available here: https://github.com/cartridge-gg/cainome.

Starknet abigen for rust bindings

Passionate work about providing rust binding to Starknet community, by the community.

The current state of the repo is still very experimental, but we are reaching a first good milestrone.

  • Types generation with serialization/deserialization for any type in the contract.
  • Support for generic types.
  • Auto generation of the contract with it's functions (call and invoke).
  • Generation of Events structs to parse automatically EmittedEvent.

How to use it

For now this crate is not yet published on crated.io, but here is how you can do the following.

# Cargo.toml of your project

[dependencies]
starknet-abigen-parser = { git = "https://github.com/glihm/starknet-abigen-rs", tag = "v0.1.3" }
starknet-abigen-macros = { git = "https://github.com/glihm/starknet-abigen-rs", tag = "v0.1.3" }
starknet = "0.7.0"
// Your main.rs or other rust file:
...
use starknet_abigen_parser;
use starknet_abigen_macros::abigen;

abigen!(MyContract, "./path/to/abi.json");

#[tokio::main]
async fn main() {
    ...
}

You can find a first simple example in the examples folder here.

Quick start

  1. Terminal 1: Run Katana
dojoup -v nightly
katana
  1. Terminal 2: Contracts setup
cd contracts && scarb build && make setup
cargo run --example simple_get_set

Cairo - Rust similarity

We've tried to leverage the similarity between Rust and Cairo. With this in mind, the bindings are generated to be as natural as possible from a Rust perspective.

So most of the types are Rust types, and the basic value for us is the FieldElement from starknet-rs.

// Cairo: fn get_data(self: @ContractState) -> Span<felt252>
fn get_data() -> Vec<FieldElement>

// Cairo: fn get_opt(self: @ContractState, val: u32) -> Option<felt252>
fn get_opt(val: u32) -> Option<FieldElement>

// Cairo: struct MyData { a: felt252, b: u32, c: Span<u32> }
struct MyData {
  a: FieldElement,
  b: u32,
  c: Vec<u32>,
}

If you want to leverage the (de)serialization generated by the bindings, to make raw calls with starknet-rs, you can:

let d = MyData {
  a: FieldElement::TWO,
  b: 123_u32,
  c: vec![8, 9],
};

let felts = MyData::serialize(&d);

let felts = vec![FieldElement::ONE, FieldElement::TWO];
// For now you have to provide the index. Later an other method will consider deserialization from index 0.
let values = Vec::<u32>::deserialize(felts, 0).unwrap;

Any type implementing the CairoType trait can be used this way.

Generate the binding for your contracts

  1. If you have a large ABI, consider adding a file (at the same level of your Cargo.toml) with the JSON containing the ABI. Then you can load the whole file using:
abigen!(MyContract, "./mycontract.abi.json")
  1. (DISABLED FOR NOW) If you only want to make a quick call without too much setup, you can paste an ABI directly using:
abigen!(MyContract, r#"
[
  {
    "type": "function",
    "name": "get_val",
    "inputs": [],
    "outputs": [
      {
        "type": "core::felt252"
      }
    ],
    "state_mutability": "view"
  }
]
"#);
  1. To extract ABI from your contract, please use the tool jq if you are in local, or any starknet explorer. With jq, you can do: cat target/dev/my_contract.contract_class.json | jq .abi > /path/abi.json.

How to work with events

Events are special structs/enum that we usually want to deserialize effectively. The abigen! macro generate all the events associated types, and this always include one enum always named Event.

Any contract you use abigen! on will contain this enum, and this also includes the convertion from EmittedEvent, which is the starknet-rs type returned when we fetch events.

So you can do this:

// the Event enum is always declared if at least 1 event is present
// in the cairo file.
use myContract::{Event as AnyEvent};

let events = provider.fetch_events(...);

for e in events {
    // The `TryFrom` is already implemented for each variant, which includes
    // the deserialization of the variant.
    let my_event: AnyEvent = match e.try_into() {
        Ok(ev) => ev,
        Err(_s) => {
            // An event from other contracts, ignore.
            continue;
        }
    };
    
    // Then, you can safely check which event it is, and work with it,
    // with the rust type!
    match my_event {
        AnyEvent::MyEventA(a) => {
            // do stuff with a.
        }
        AnyEvent::MyEventB(b) => {
            // do stuff with b.
        }
        ...
    };
}

Serialization

Cairo serializes everything as felt252. Some edge cases to have in mind:

  1. Enum

Enumerations are serialized with the index of the variant first, and then the value (is any).

enum MyEnum {
    V1: u128,
    V2,
}

let a = MyEnum::V1(2_u128);
let b = MyEnum::V2;

Will be serialized like this, with enum variant index first:

a: [0, 2]
b: [1]
  1. Span/Array

After serialization, Span and Array are processed in the same fashion. The length is serialized first, and then the following elements.

let a = array![];
let b = array![1, 2];

Will be serialized as:

a: [0]
b: [2, 1, 2]
  1. Struct

struct are serialized as their fields define it. There is no length at the beginning. It depends on the fields order.

struct MyStruct {
    a: felt252,
    b: u256,
    c: Array<felt252>,
}

let s = MyStruct {
    a: 123,
    b: 1_u256,
    c: array![9],
}

Will be serialized as:

[123, 1, 0, 1, 9]

PR on starknet-rs

The goal of this work was to be included in starknet-rs library. You can follow the status of such process checking those PRs:

  1. xJonathanLEI/starknet-rs#475
  2. Please take a look to this README for the newest information about the syntax being worked on.

But we may choose a standalone path in the future to add more features.

Disclaimer

This is a very early stage of the project. The idea is to have a first version that can be revised by the community and then enhanced.

Hopefully one day we can have a great lib that can be integrated to starknet-rs or remain a stand alone crate which can be combined with starknet-rs.

Credits

None of these crates would have been possible without the great work done in: