-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add cli and separate rs and rs-macro
- Loading branch information
Showing
20 changed files
with
1,174 additions
and
284 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[package] | ||
name = "cainome-rs-macro" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
anyhow.workspace = true | ||
starknet.workspace = true | ||
cainome-parser.workspace = true | ||
cainome-rs.workspace = true | ||
proc-macro2 = "1.0" | ||
quote = "1.0" | ||
syn = "2.0.15" | ||
serde_json = "1.0.74" | ||
thiserror.workspace = true | ||
cainome-cairo-serde.workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
# Cainome Rust abigen | ||
|
||
This crates contains the compile-time rust macro `abigen` to generate rust bindings (using Cairo Serde). | ||
|
||
## Import in your project | ||
|
||
```toml | ||
# Cargo.toml | ||
|
||
cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.1.10", features = ["abigen-rs"] } | ||
``` | ||
|
||
```rust | ||
// Rust code | ||
use cainome::rs::abigen; | ||
|
||
abigen!(MyContract, "/path/my_contract.json"); | ||
``` | ||
|
||
## Usage | ||
|
||
For examples, please refer to the [examples](../../examples) folder. | ||
|
||
The `abigen!` macro takes 2 or 3 inputs: | ||
|
||
1. The name you want to assign to the contract type being generated. | ||
2. Path to the JSON file containing the ABI. This file can have two format: | ||
|
||
- The entire Sierra file (`*.contract_class.json`) | ||
- 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 | ||
``` | ||
|
||
3. 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. | ||
|
||
```rust | ||
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; | ||
}, | ||
); | ||
|
||
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! | ||
|
||
## What is generated | ||
|
||
The expansion of the macros generates the following: | ||
|
||
- For every type that is exposed in the ABI, a `struct` or `enum` will be generated with the `CairoSerde` trait automatically derived. The name of the type if always the last segment of the full type path, enforced to be in `PascalCase`. | ||
|
||
```rust | ||
// 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::FieldElement, | ||
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 implements `ConnectedAccount` from `starknet-rs`. Remember that `Arc<ConnectedAccount>` also implements `ConnectedAccount`. | ||
```rust | ||
let account = SingleOwnerAccount::new(...); | ||
let contract_address = FieldElement::from_hex_be("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 from `starknet-rs`. | ||
```rust | ||
let provider = AnyProvider::JsonRpcHttp(...); | ||
let contract_address = FieldElement::from_hex_be("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 the `block_id`. Finally, to actually do the RPC call, you have to use `call()` method on the `FCall` struct. | ||
The default `block_id` value is `BlockTag::Pending`. | ||
```rust | ||
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::Execution` type from `starknet-rs`, which allows you to completly customize the fees, doing only a simulation etc... To actually send the transaction, you use the `send()` method on the `Execution` struct. You can find the [associated methods with this struct on starknet-rs repo](https://github.com/xJonathanLEI/starknet-rs/blob/0df9ad3417a5f10d486348737fe75659ca4bcfdc/starknet-accounts/src/account/execution.rs#L118). | ||
|
||
```rust | ||
let my_struct = MyStruct { | ||
a: FieldElement::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 `Execution` type does not expose the `Call`s. | ||
To circumvey this, for each of the external function an other function with `_getcall` suffix is generated: | ||
|
||
```rust | ||
// Gather the `Call`s. | ||
let set_a_call = contract.set_a_getcall(&FieldElement::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 trait `TryFrom<EmittedEvent>` is generated. `EmittedEvent` is the type used | ||
by `starknet-rs` when events are fetched using `provider.get_events()`. | ||
|
||
```rust | ||
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; | ||
} | ||
}; | ||
``` | ||
|
||
## Known limitation | ||
|
||
With the current state of the parser, here are some limitations: | ||
|
||
1. 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: | ||
|
||
```rust | ||
struct GenericTwo<A, B> { | ||
a: A, | ||
b: B, | ||
c: felt252, | ||
} | ||
``` | ||
|
||
If the cairo code only have one use of this struct like this: | ||
|
||
```rust | ||
fn my_func(self: @ContractState) -> GenericTwo<u64, u64>; | ||
``` | ||
|
||
Then the ABI will look like this: | ||
|
||
```json | ||
{ | ||
"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: | ||
|
||
```rust | ||
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. | ||
|
||
## Roadmap | ||
|
||
1. [ ] Add a simple transaction status watcher integrated to the contract type. | ||
2. [ ] Add declare and deploy function to the contract type. | ||
3. [ ] Custom choice of derive for generated structs/enums. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
use cainome_parser::AbiParser; | ||
use cainome_rs::{self}; | ||
use proc_macro::TokenStream; | ||
use quote::quote; | ||
|
||
mod macro_inputs; | ||
mod spanned; | ||
|
||
use crate::macro_inputs::ContractAbi; | ||
|
||
#[proc_macro] | ||
pub fn abigen(input: TokenStream) -> TokenStream { | ||
abigen_internal(input) | ||
} | ||
|
||
fn abigen_internal(input: TokenStream) -> TokenStream { | ||
let contract_abi = syn::parse_macro_input!(input as ContractAbi); | ||
|
||
let contract_name = contract_abi.name; | ||
let abi_entries = contract_abi.abi; | ||
|
||
let abi_tokens = AbiParser::collect_tokens(&abi_entries).expect("failed tokens parsing"); | ||
let abi_tokens = AbiParser::organize_tokens(abi_tokens, &contract_abi.type_aliases); | ||
|
||
let expanded = cainome_rs::abi_to_tokenstream(&contract_name.to_string(), &abi_tokens); | ||
|
||
if let Some(out_path) = contract_abi.output_path { | ||
let content: String = expanded.to_string(); | ||
match std::fs::write(out_path, content) { | ||
Ok(_) => (), | ||
Err(e) => panic!("Failed to write to file: {}", e), | ||
} | ||
|
||
quote!().into() | ||
} else { | ||
expanded.into() | ||
} | ||
} |
Oops, something went wrong.