Exploratory work on abigen in rust for Starknet 🦀

Overview

Starknet abigen for rust bindings

This exploratory work aims at generating rust bindings from a contract ABI.

Before the first release, we are terminating the following:

  1. Handling the events correctly as struct and enums.

  2. Support generic types, which are often used in cairo. The generic types are the one that may cause a structure / enum being present 2+ times in the ABI. We then must detect that it's a generic struct/enum, and generate only one struct/enum with the genericity included. This has also an impact on the functions, as any function that take an argument that is generic must also take them in account.

Quick start

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

Overview

This repository contains the following crates:

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. Except few exceptions like ContractAddress, ClassHash and EthAddress, which a custom structs to map those Cairo native type, all the types are mapped to native Rust types.

// 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.

Supported types as built-in in cairo-types:

  • u8,16,32,64,128
  • i8,16,32,64,128
  • tuple size 2
  • Span/Array -> Vec
  • ClassHash
  • ContractAddress
  • EthAddress
  • Option
  • Result
  • unit

Any struct/enum in the ABI that use those types or inner struct/enum that uses those types will work.

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. 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"
  }
]
"#);

Initialize the contract

In starknet, we also have call and invoke. A call doesn't alter the state, and hence does not require an account + private key to sign. An invoke requires you to provide an account and a private key to sign and send the transaction.

use abigen_macro::abigen;
use anyhow::Result;
use cairo_types::ty::CairoType;

use starknet::accounts::{Account, SingleOwnerAccount};

use starknet::core::types::*;
use starknet::providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient, Provider};
use starknet::signers::{LocalWallet, SigningKey};

abigen!(MyContract, "./mycontract.abi.json")

#[tokio::main]
async fn main() -> Result<()> {
    let rpc_url = Url::parse("http://0.0.0.0:5050")?;

    let provider =
        AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url.clone())));

    let contract_address = felt!("0x0546a164c8d10fd38652b6426ef7be159965deb9a0cbf3e8a899f8a42fd86761");

     // Call.
    let my_contract = MyContract::new(contract_address, &provider);
    let val = my_contract.get_val().await?;

     let account_address = felt!("0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973");

    let signer = wallet_from_private_key(&Some(
        "0x0000001800000000300000180000000000030000000000003006001800006600".to_string(),
    )).unwrap();
    let account = SingleOwnerAccount::new(&provider, signer, account_address, chain_id);

    // Invoke.
    let mycontract = MyContract::new(contract_address, &provider).with_account(&account).await?;

    mycontract.set_val(FieldElement::TWO).await?;
}

// Util function to create a LocalWallet.
fn wallet_from_private_key(
    private_key: &std::option::Option<String>,
) -> std::option::Option<LocalWallet> {
    if let Some(pk) = private_key {
        let private_key = match FieldElement::from_hex_be(pk) {
            Ok(p) => p,
            Err(e) => {
                println!("Error importing private key: {:?}", e);
                return None;
            }
        };
        let key = SigningKey::from_secret_scalar(private_key);
        Some(LocalWallet::from_signing_key(key))
    } else {
        None
    }
}

This way of initializing the contract is not the final one, feel free to propose alternative in the issues.

Considerations

On Starknet, a contract's ABI is a flat representation of all the types and functions associated with the contract.

Each struct or enum that are used by external functions of the contracts are embedded in the ABI, which ensure a full description of the types, self-contained in a single ABI file.

Cairo has the capability of using generic types. However, the ABI does not reflect this implementation detail.

struct MyStruct<T> {
    a: T,
    b: u8,
}

// This struct in the ABI will be flatten depending on the impl found in the code.

(...)

fn func_1(ms: MyStruct<felt252>)
// This function has the `felt252` impl, so the ABI will contain:

  {
    "type": "struct",
    "name": "package::contract1::MyStruct",
    "members": [
      {
        "name": "a",
        "type": "core::felt252"
      },
      {
        "name": "b",
        "type": "core::integer::u8"
      }
    ]
  },

We don't have the possibility to know which type was impl by the generic type T only looking at the ABI.

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 as:

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]

Current design idea

At first, we tried to take inspiration from alloy, the new implementation for Ethereum rust library.

But cairo differs from solidity in several aspect, starting with generic types. So ideally, we should be able to tokenize the ABI into syn to then totally control how we want to lower the detected types.

But for now, the approach is inspired from alloy, but simpler and more hand made for type parsing.

  1. First, we have the CairoType (which may be renamed as CairoSerializeable) trait. This trait defines how a rust type is serialized / deserialized as Cairo FieldElement from starknet-rs.

  2. Then, AbiType is able to parse any cairo type, even if it's nested. As we have to be able to express how types are nested to ensure the correct serialization.

  3. After having the AbiType, we then want to expand in a macro the types and their serialization logic in a macro. For that, each of the AbiEntry that are struct, enum, function must be expanded using the AbiType info to correctly generate the serialization code.

  4. Finally, the contract itself, must be generated with the provider already internalized, to easily do some invoke and calls, using pure rust types.

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:

Comments
  • Make provider reference

    Make provider reference

    Problem

    Right now when we call new_caller or new_invoker, we pass provider ownership. Which cause us to recreate multiple providers when we initiate contract.

    
        let provider =
            AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url.clone())));
    
        ...
        // Call.
        let contract_caller = MyContract::new_caller(contract_address, provider).await?;
        let val = contract_caller.get_val().await?;
    
        // Work in progress to avoid this duplication.
        let provider2 =
            AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url.clone())));
    
        ...
        // Invoke.
        let contract_invoker =
            MyContract::new_invoker(contract_address, provider2, account_address, signer).await?;
        contract_invoker.set_val(FieldElement::TWO).await?;
    

    Possible approach

    Pass provider into reference so that we can use one provider for caller and invoker.

    We might can create init struct like this and pass it.

    ContractSetup {
       address,
       provider,
       Option<account_address>,
       Option<private_key>
    }
    
    enhancement 
    opened by rkdud007 4
  • provider ref + modify macro a bit

    provider ref + modify macro a bit

    #14

    • signer / provider is all reference ( can reuse )
    • use generic input ( P : Provider, A: Account )
    • update abigen macro using builders pattern with_account

    TODO

    • if not use with_account, contract which accept generic P : Provider, A: Account, will return error bcs they don't know A's specific type. Current temporary solution is explicitly define P & A if not using with_account. Can open other issue
    opened by rkdud007 3
  • Function with nested type expand

    Function with nested type expand

    get function type abi as string and turn it's inputs and outputs into cairo-types

    Abi string as input

    {
        "type": "function",
        "name": "hello_world",
        "inputs": [
          {
            "name": "value",
            "type":"core::array::Array::<core::felt252>"
          }
        ],
        "outputs": [
          {
            "type": "core::array::Array::<core::felt252>"
          }
        ],
        "state_mutability": "view"
      },
    

    AbiType for core::array::Array::<core::felt252>

    AbiType::Nested(
                "array".to_string(),
                vec![AbiType::Nested(
                    AbiType::Basic("core::felt252".to_string()),
                )],
            );
            
    

    CairoType as output

    fn hello_world(value : Vec<FieldElement>) -> Vec<FieldElement> 
    
    • [ ] function string -> abiType -> tokenType::FunctionToken
    • [ ] tokenType::FunctionToken -> turn into cairo-types
    enhancement 
    opened by rkdud007 2
  • Manual generation to have the vision of expected generated code

    Manual generation to have the vision of expected generated code

    Create a folder with examples of expanded code for contracts:

    • [X] Simple contract with only basic types (no struct, no enum, no array).
    • [ ] ~Contract with array.~
    • [X] Contract with tuples no generics.
    • [X] Contract with tuples and generics.
    • [X] Contract with enum only (and basics of course), with no generic.
    • [X] Contract with enum and generic enums.
    • [X] Contract with struct only (no generic).
    • [X] Contract with struct and generic structs.

    For all of these, we have to write a rust file for the expanded contract with:

    // (If struct and enum)
    // First, expand the types and their `CairoType` implementation as necessary.
    
    struct ContractA {
    ...
    }
    
    impl ContractA {
       // Implement call and invoke using the types.
    }
    
    opened by glihm 1
  • Write a macro to generate tuple support for several sizes

    Write a macro to generate tuple support for several sizes

    For now, tuples are only supported for size 2.

    Write a macro that impl CairoType for several sizes.

    If it's not easily possible, at least implement tuple until size 5.

    important 
    opened by glihm 1
  • ABI parsing from string

    ABI parsing from string

    For now, the abigen macro is expecting a string to be a file.

    Modify this behavior to detect if:

    1. One line is given (and ends with .json ?) -> it's a file.
    2. Multiline -> directly consider the string value as the ABI to be parsed.

    This will allow easy testing to avoid writing ABI files, but direclty in literal string in the code.

    opened by glihm 0
  • Write a macro to auto-generate cairo_type trait for `uintX` and `intX`

    Write a macro to auto-generate cairo_type trait for `uintX` and `intX`

    In this file the implementation of CairoType trait is done manually for each integer types.

    Consider writing a macro to auto-generate those, and adding a file for each of the int types.

    This issue is about uint8,16,..128 and int8,16....

    opened by glihm 0
  • update types into Uppercases

    update types into Uppercases

    Right now we use lowercase to 1) Cairo type representation in Rust 2) Original Rust type in uint, int, bool. But I think it's not good practice to have exact same name with original rust type. Maybe we can use remco/uint to Cairo type in Rust as uppercase struct ( Alloy did same approach ).

    reference

    https://github.com/alloy-rs/core/blob/main/crates/dyn-abi/src/resolve.rs

    question 
    opened by rkdud007 1
  • implement event

    implement event

    Consideration

    Event, I might also use struct and enum expansion. If then, we cannot distinguish between struct & eventStruct and enum & eventEnum. The difference between those two is, event type have one more parameter which is kind.

          {
            "name": "user",
            "type": "core::starknet::contract_address::ContractAddress",
            "kind": "key"
          },
    

    Right now not sure how we can use event if we could expand in macro. We can get event data from transaction recept, and the type of this is felt, so might be useful to have (de)serialization method for event.

    So not sure kind is important or not. => Decided to implement independent expansion in event.rs, which will handle kind also.

    Then later if we could find out that field is not using, can refactor using current expansion of struct and enum.

    opened by rkdud007 1
  • Design challenge for initialize contract `abigen!` ( generic + builder pattern )

    Design challenge for initialize contract `abigen!` ( generic + builder pattern )

    Goal

    • Contract should be initialize with or without account. ( Builder pattern )
    • Without account, we only provide Provider. With account, we provide Provider and Account both.
    • We want to give flexibility of both Provider and Account, defined generic as P: Provider and A: Account.

    Current Problem

    • When we initialize without Account, because contract struct is already defined with 2 generic parameters, it returns error because we didn't specified A: Account generic's type.
    • So our current code not use generic types for Provider and Account. But able to initialize with/without account.
     // Call.
    let my_contract = MyContract::new(contract_address, &provider);
     // Invoke.
    let mycontract = MyContract::new(contract_address, &provider).with_account(&account).await?;
    

    Some inspirations

    • ethers-rs approach : use different struct. Add different string to the name of contract and function
       abigen!(
            SimpleContract,
            r#"[
            struct Foo { uint256 x; }
            function foo(Foo memory x)
            function bar(uint256 x, uint256 y, address addr)
            yeet(uint256,uint256,address)
        ]"#,
            derives(serde::Deserialize, serde::Serialize)
        );
     let contract = SimpleContract::new(Address::default(), Arc::new(client));
     let call = BarCall { x: 1u64.into(), y: 0u64.into(), addr: Address::random() };
     let contract_call = SimpleContractCalls::Bar(call);
    
    • alloy : use same struct for contract but different name for function.
    // This is the same as:
    sol! {
       interface MyJsonContract2 {
           struct MyStruct {
               bool[] a;
               bytes18[][] b;
           }
    
           function foo(uint256 bar, MyStruct baz) external view;
       }
    }
    
    #[test]
    fn abigen() {
       assert_eq!(
           MyJsonContract1::fooCall::SIGNATURE,
           MyJsonContract2::fooCall::SIGNATURE,
       );
    }
    
    enhancement 
    opened by rkdud007 0
  • Add custom derive from abigen

    Add custom derive from abigen

    As in ethers-rs, a use may pass all the additional trait that may be derived automatically for a struct/enum.

    abigen!(MyContract, "abi.json", [Serialize, Deserialize, ...])
    

    Or do we want to, by default, implement them?

    opened by glihm 0
  • Scope of the macro generated code

    Scope of the macro generated code

    What's the best manner to generate the code emitted by the abigen macro?

    1. Use a scope?
    2. Create a module?
    3. Export into a new file to allow code DRY?
    4. Anonymous const?
    5. Other?
    question 
    opened by glihm 0
Owner
null
🥷🩸 Madara is a ⚡ blazing fast ⚡ Starknet sequencer, based on substrate, powered by Rust 🦀

Report a Bug - Request a Feature - Ask a Question ⚡ Madara: Starknet Sequencer on Substrate ?? Welcome to Madara, a blazing fast ⚡ Starknet sequencer

Keep StarkNet Strange 138 Apr 22, 2023
🐺 Starknet Rust types 🦀

types-rs ?? Starknet Rust types ?? This repository is an initiative by a group of maintainers to address the fragmentation in the Starknet Rust ecosys

Starknet 22 Jun 2, 2023
Channel some Ki with Lua scripts for sending transactions to Starknet, powered by Rust.

Kipt Kipt is leveraging the simplicity of Lua scripts to manage Starknet contracts using starknet-rs under the hood. With few lines, you can declare,

null 3 Nov 3, 2023
Kraken is a Starknet modular decentralized sequencer implementation.

Lambda Starknet Sequencer A Starknet decentralized sequencer implementation. Getting started The objective of this project is to create an L2 decentra

Lambdaclass 10 Jun 25, 2023
Blazing fast toolkit for developing Starknet contracts.

Starknet Foundry Blazingly fast toolkit for developing Starknet contracts designed & developed by ex Protostar team from Software Mansion based on nat

Foundry 149 Aug 1, 2023
A prototype project integrating jni rust into Kotlin and using protobuf to make them work together

KotlinRustProto a prototype project integrating jni rust into Kotlin and using protobuf to make them work together How to start add a RPC call in Droi

woo 11 Sep 5, 2022
a handy utility to work with encrypted DMGs

edmgutil edmgutil is a simple wrapper utility to hdiutil to help you work with disposable, encrypted DMGs. It can decompress an encrypted ZIP into a n

Sentry 9 Nov 29, 2022
legitima is a work in progress LDAP provider for ORY Hydra.

legitima is a work in progress LDAP provider for ORY Hydra. Together with it, it can be used as an OpenID Connect (OIDC) provider to authenticate to any OIDC capable apps.

leona 3 Aug 1, 2022
Glommio Messaging Framework (GMF) is a high-performance RPC system designed to work with the Glommio framework.

Glommio Messaging Framework (GMF) The GMF library is a powerful and innovative framework developed for facilitating Remote Procedure Calls (RPCs) in R

Mohsen Zainalpour 29 Jun 13, 2023
Package used by the cosmos-rust-interface. Makes direct use of cosmos-rust.

Package used by the cosmos-rust-interface. Makes direct use of cosmos-rust (cosmos‑sdk‑proto, osmosis-proto, cosmrs).

Philipp 4 Dec 26, 2022
Rust project for working with ETH - Ethereum transactions with Rust on Ganache and also deploy smart contracts :)

Just a test project to work with Ethereum but using Rust. I'm using plain Rust here, not Foundry. In future we will use Foundry. Hope you're already f

Akhil Sharma 2 Dec 20, 2022
An open source Rust high performance cryptocurrency trading API with support for multiple exchanges and language wrappers. written in rust(🦀) with ❤️

Les.rs - Rust Cryptocurrency Exchange Library An open source Rust high performance cryptocurrency trading API with support for multiple exchanges and

Crabby AI 4 Jan 9, 2023
Simple node and rust script to achieve an easy to use bridge between rust and node.js

Node-Rust Bridge Simple rust and node.js script to achieve a bridge between them. Only 1 bridge can be initialized per rust program. But node.js can h

Pure 5 Apr 30, 2023
Marvin-Blockchain-Rust: A Rust-based blockchain implementation, part of the Marvin blockchain project.

Marvin Blockchain - Rust Implementation Welcome to the Rust implementation of the Marvin Blockchain. This project is part of a comparative study on bu

João Henrique Machado Silva 3 Sep 6, 2024
A Rust library for working with Bitcoin SV

Rust-SV A library to build Bitcoin SV applications in Rust. Documentation Features P2P protocol messages (construction and serialization) Address enco

Brenton Gunning 51 Oct 13, 2022
Coinbase pro client for Rust

Coinbase pro client for Rust Supports SYNC/ASYNC/Websocket-feed data support Features private and public API sync and async support websocket-feed sup

null 126 Dec 30, 2022
Custom Ethereum vanity address generator made in Rust

ethaddrgen Custom Ethereum address generator Get a shiny ethereum address and stand out from the crowd! Disclaimer: Do not use the private key shown i

Jakub Hlusička 153 Dec 27, 2022
The new, performant, and simplified version of Holochain on Rust (sometimes called Holochain RSM for Refactored State Model)

Holochain License: This repository contains the core Holochain libraries and binaries. This is the most recent and well maintained version of Holochai

Holochain 741 Jan 5, 2023
IBC modules and relayer - Formal specifications and Rust implementation

ibc-rs Rust implementation of the Inter-Blockchain Communication (IBC) protocol. This project comprises primarily four crates: The ibc crate defines t

Informal Systems 296 Dec 31, 2022