This crates contains the compile-time rust macro abigen
to generate rust bindings (using Cairo Serde).
# Cargo.toml
cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.9", features = ["abigen-rs"] }
// Rust code
use cainome::rs::abigen;
abigen!(MyContract, "/path/my_contract.json");
Cairo 0 support is limited (event are not parsed yet), but to interact with a cairo 0 program you can use the legacy macro:
// Rust code
use cainome::rs::abigen;
abigen_legacy!(MyContract, "/path/cairo_0.json");
For examples, please refer to the examples folder.
The abigen!
macro takes 2 or 3 inputs:
-
The name you want to assign to the contract type being generated.
-
Path to the JSON file containing the ABI. This file can have two format:
- The entire Sierra file (
*.contract_class.json
) [Only for Cairo 1] - Only the array of ABI entries. These can be easily extracted with
jq
doing the following:
jq .abi ./target/dev/package_contract.contract_class.json > /path/contract.json
- The entire Sierra file (
-
Optional parameters:
output_path
: if provided, the content will be generated in the given file instead of being expanded at the location of the macro invocation.type_aliases
: to avoid type name conflicts between components / contracts, you can rename some type by providing an alias for the full type path. It is important to give the full type path to ensure aliases are applied correctly.derive
: to specify the derive for the generated structs/enums.contract_derives
: to specify the derive for the generated contract type.
use cainome::rs::abigen;
// Default.
abigen!(MyContract, "/path/contract.json");
// Example with optional output path:
abigen!(MyContract, "/path/contract.json", output_path("/path/module.rs"));
// Example type aliases:
abigen!(
MyContract,
"./contracts/abi/components.abi.json",
type_aliases {
package::module1::component1::MyStruct as MyStruct1;
package::module2::component2::MyStruct as MyStruct2;
},
);
// Example with custom derives:
abigen!(
MyContract,
"./contracts/abi/components.abi.json",
derives(Debug, Clone),
contract_derives(Debug, Clone)
);
fn main() {
// ... use the generated types here, which all of them
// implement CairoSerde trait.
}
As a known limitation of Cargo
, the /path/contract.json
is relative to the Cargo manifest (Cargo.toml
). This is important when executing a specific package (-p
) or from the workspace (--workspace/--all
), the manifest directory is not the same!
The expansion of the macros generates the following:
-
For every type that is exposed in the ABI, a
struct
orenum
will be generated with theCairoSerde
trait automatically derived. The name of the type if always the last segment of the full type path, enforced to be inPascalCase
.// Take this cairo struct, in with the full path `package::my_contract::MyStruct MyStruct { a: felt252, b: u256, } // This will generate a rust struct with the make `MyStruct`: MyStruct { a: starknet::core::types::Felt, a: U256, // Note the `PascalCase` here. As `u256` is a struct, it follows the common rule. }
-
Contract type with the identifier of your choice (
MyContract
in the previous example). This type contains all the functions (externals and views) of your contract being exposed in the ABI. To initialize this type, you need the contract address and any type that implementsConnectedAccount
fromstarknet-rs
. Remember thatArc<ConnectedAccount>
also implementsConnectedAccount
.let account = SingleOwnerAccount::new(...); let contract_address = Felt::from_hex("0x1234..."); let contract = MyContract::new(contract_address, account);
-
Contract Reader type with the identifier of your choice with the suffix
Reader
(MyContractReader
) in the previous example. The reader contains only the views of your contract. To initialize a reader, you need the contract address and a provider fromstarknet-rs
.let provider = AnyProvider::JsonRpcHttp(...); let contract_address = Felt::from_hex("0x1234..."); let contract_reader = MyContractReader::new(contract_address, &provider);
-
For each view, the contract type and the contract reader type contain a function with the exact same arguments. Calling the function returns a
cainome_cairo_serde::call::FCall
struct to allow you to customize how you want the function to be called. Currently, the only setting is theblock_id
. Finally, to actually do the RPC call, you have to usecall()
method on theFCall
struct. The defaultblock_id
value isBlockTag::Pending
.let my_struct = contract .get_my_struct() .block_id(BlockId::Tag(BlockTag::Latest)) .call() .await .expect("Call to `get_my_struct` failed");
-
For each external, the contract type contains a function with the same arguments. Calling the function return a
starknet::accounts::ExecutionV1
type fromstarknet-rs
, which allows you to completly customize the fees, doing only a simulation etc... To actually send the transaction, you use thesend()
method on theExecutionV1
struct. You can find the associated methods with this struct on starknet-rs repo.let my_struct = MyStruct { a: Felt::ONE, b: U256 { low: 1, high: 0, } }; let tx_res = contract .set_my_struct(&my_struct) .max_fee(1000000000000000_u128.into()) .send() .await .expect("Call to `set_my_struct` failed");
To support multicall, currently
ExecutionV1
type does not expose theCall
s. To circumvey this, for each of the external function an other function with_getcall
suffix is generated:// Gather the `Call`s. let set_a_call = contract.set_a_getcall(&Felt::ONE); let set_b_call = contract.set_b_getcall(&U256 { low: 0xff, high: 0 }); // Then use the account exposed by the `MyContract` type to realize the multicall. let tx_res = contract .account .execute(vec![set_a_call, set_b_call]) .send() .await .expect("Multicall failed");
-
For each
Event
enumeration in the contract, the traitTryFrom<EmittedEvent>
is generated.EmittedEvent
is the type used bystarknet-rs
when events are fetched usingprovider.get_events()
.let events = provider.get_events(...).await.unwrap(); for event in events { match event.try_into() { Ok(ev) => { // Here, `ev` is deserialized + selectors are checked. } Err(e) => { trace!("Event can't be deserialized to any known Event variant: {e}"); continue; } };
-
For cairo 0 contracts, for each method that has at least one output, cainome will generate a
struct
with the output fields.{ "inputs": [], "name": "get_blockhash_registry", "outputs": [ { "name": "address", "type": "felt" } ], "stateMutability": "view", "type": "function" }
Will generate with the function's name in PascalCase and the suffix
Output
:pub struct GetBlockhashRegistryOutput { pub address: starknet::core::types::Felt, }
With the current state of the parser, here are some limitations:
- Generic arguments: even if the library currently supports generic arguments, sometimes the simple algorithm for generic resolution is not able to re-construct the expected generic mapping. This may cause compilation errors. Take an example with:
struct GenericTwo<A, B> {
a: A,
b: B,
c: felt252,
}
If the cairo code only have one use of this struct like this:
fn my_func(self: @ContractState) -> GenericTwo<u64, u64>;
Then the ABI will look like this:
{
"type": "struct",
"name": "contracts::abicov::structs::GenericTwo::<core::integer::u64, core::integer::u64>",
"members": [
{
"name": "a",
"type": "core::integer::u64"
},
{
"name": "b",
"type": "core::integer::u64"
},
{
"name": "c",
"type": "core::felt252"
}
]
},
And here... how can we know that a
is A
and b
is B
? The current algorithm will generate the following:
struct GenericTwo<A, B> {
a: A,
b: A,
c: felt252,
}
Which will cause a compilation error.
A first approach to this, is to add a Phantom
placeholder for each of the variant. To ensure that there is always the two generic args used. But this will prevent the easy initialization of the struct with the fields. Need to check if we can use Default
, or instead, using a new(..)
pattern.
- Add a simple transaction status watcher integrated to the contract type.
- Add declare and deploy function to the contract type.
- Custom choice of derive for generated structs/enums.