Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: evm contract calls #35

Merged
merged 33 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
044a0d0
feat: built-in deployment support
0xrusowsky Dec 21, 2024
ca1bbc7
wip: constructor logic
0xrusowsky Dec 23, 2024
a09c2c2
style: cleanup
0xrusowsky Dec 23, 2024
644a3a2
remove deploy file
0xrusowsky Dec 23, 2024
b7671a9
Merge branch 'feat/built-in-deployment' into feat/constructor-support
0xrusowsky Dec 26, 2024
00e9340
update comments
0xrusowsky Dec 26, 2024
ebb9acf
housekeeping
0xrusowsky Dec 26, 2024
f724bcb
housekeeping
0xrusowsky Dec 26, 2024
f650cc4
finish constructor support
0xrusowsky Dec 28, 2024
4914e9b
explicit helper fn names
0xrusowsky Dec 28, 2024
f2c57bf
Merge branch 'feat/built-in-deployment' into feat/constructor-support
0xrusowsky Dec 28, 2024
477ca40
finish constructor impl
0xrusowsky Dec 29, 2024
f161090
Merge branch 'main' into feat/constructor-support
0xrusowsky Dec 29, 2024
9b8b65f
style: housekeeping
0xrusowsky Dec 29, 2024
141e360
style: housekeeping
0xrusowsky Dec 29, 2024
af4b1aa
exclude constructor in runtime impl
0xrusowsky Dec 29, 2024
1f3ca66
feat: evm contract calls
0xrusowsky Jan 19, 2025
dac5b33
test evm > r55 calls
0xrusowsky Jan 20, 2025
257fb65
comments
0xrusowsky Jan 20, 2025
69bb362
calls with returndatasize + returndatacopy
0xrusowsky Jan 24, 2025
3fefbd9
fix: merge conflicts
0xrusowsky Feb 6, 2025
fe46cde
style: cargo fmt
0xrusowsky Feb 6, 2025
d16524e
fix: merge conflicts
0xrusowsky Feb 7, 2025
79b2d56
style: clippy and fmt
0xrusowsky Feb 7, 2025
3f8a99f
fix: bump versions
0xrusowsky Feb 10, 2025
32505b3
style: clippy
0xrusowsky Feb 10, 2025
e442e00
fix: CI
0xrusowsky Feb 10, 2025
669bc29
fix: merge conflict
0xrusowsky Feb 10, 2025
e6695bc
style: clippy
0xrusowsky Feb 10, 2025
2396411
fix: merge conflicts
0xrusowsky Feb 11, 2025
43439f4
typo
0xrusowsky Feb 17, 2025
e2ef7a2
fix: typo
0xrusowsky Feb 17, 2025
b478d44
style
0xrusowsky Feb 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contract-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ quote = "1.0"
syn = { version = "1.0", features = ["full"] }
alloy-core = { version = "0.8.20", default-features = false }
alloy-sol-types = { version = "0.8.20", default-features = false }
alloy-dyn-abi = { version = "0.8.20", default-features = false }

[lib]
proc-macro = true
283 changes: 276 additions & 7 deletions contract-derive/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
use std::error::Error;

use alloy_core::primitives::keccak256;
use alloy_dyn_abi::DynSolType;
use alloy_sol_types::SolType;
use quote::{format_ident, quote};
use syn::{FnArg, Ident, ImplItemMethod, ReturnType, TraitItemMethod};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
FnArg, Ident, ImplItemMethod, Meta, ReturnType, TraitItemMethod, Type,
};

// Unified method info from `ImplItemMethod` and `TraitItemMethod`
pub struct MethodInfo<'a> {
Expand Down Expand Up @@ -57,10 +65,106 @@ pub fn get_arg_props_all<'a>(method: &'a MethodInfo<'a>) -> (Vec<Ident>, Vec<&sy
get_arg_props(false, method)
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InterfaceCompilationTarget {
R55,
EVM,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InterfaceNamingStyle {
CamelCase,
}

pub struct InterfaceArgs {
pub target: InterfaceCompilationTarget,
pub rename: Option<InterfaceNamingStyle>,
}

impl Parse for InterfaceArgs {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
// default arg values if uninformed
let mut target = InterfaceCompilationTarget::R55;
let mut rename_style = None;

// Parse all attributes
let mut first = true;
while !input.is_empty() {
if !first {
input.parse::<syn::Token![,]>()?;
}
first = false;

let key: syn::Ident = input.parse()?;

match key.to_string().as_str() {
"rename" => {
input.parse::<syn::Token![=]>()?;

// Handle both string literals and identifiers
let value = if input.peek(syn::LitStr) {
let lit: syn::LitStr = input.parse()?;
lit.value()
} else {
let ident: syn::Ident = input.parse()?;
ident.to_string()
};

rename_style = Some(match value.as_str() {
"camelCase" => InterfaceNamingStyle::CamelCase,
invalid => {
return Err(syn::Error::new(
key.span(),
format!("unsupported rename style: {}", invalid),
))
}
});
}
"target" => {
input.parse::<syn::Token![=]>()?;

// Handle both string literals and identifiers
let value = if input.peek(syn::LitStr) {
let lit: syn::LitStr = input.parse()?;
lit.value()
} else {
let ident: syn::Ident = input.parse()?;
ident.to_string()
};

target = match value.as_str() {
"r55" => InterfaceCompilationTarget::R55,
"evm" => InterfaceCompilationTarget::EVM,
invalid => {
return Err(syn::Error::new(
key.span(),
format!("unsupported compilation target: {}", invalid),
))
}
};
}
invalid => {
return Err(syn::Error::new(
key.span(),
format!("unknown attribute: {}", invalid),
))
}
}
}

Ok(InterfaceArgs {
target,
rename: rename_style,
})
}
}

// Helper function to generate interface impl from user-defined methods
pub fn generate_interface<T>(
methods: &[&T],
interface_name: &Ident,
interface_style: Option<InterfaceNamingStyle>,
interface_target: InterfaceCompilationTarget,
) -> quote::__private::TokenStream
where
for<'a> MethodInfo<'a>: From<&'a T>,
Expand All @@ -71,11 +175,14 @@ where
let method_impls = methods.iter().map(|method| {
let name = method.name;
let return_type = method.return_type;
let method_selector = u32::from_be_bytes(
keccak256(name.to_string())[..4]
.try_into()
.unwrap_or_default(),
);
let method_selector = match interface_target {
InterfaceCompilationTarget::R55 => u32::from_be_bytes(
generate_selector_r55(method, interface_style).expect("Unable to generate r55 fn selector"),
),
InterfaceCompilationTarget::EVM => u32::from_be_bytes(
generate_selector_evm(method, interface_style).expect("Unable to generate evm fn selector"),
),
};

let (arg_names, arg_types) = get_arg_props_skip_first(method);

Expand Down Expand Up @@ -119,7 +226,7 @@ where
self.address,
0_u64,
&complete_calldata,
32_u64
None
)?;

<#return_ty>::abi_decode(&result, true).ok()
Expand All @@ -142,6 +249,168 @@ where
}
}

// Helper function to generate fn selector for r55 contracts
pub fn generate_selector_r55(method: &MethodInfo, style: Option<InterfaceNamingStyle>) -> Option<[u8; 4]> {
let name = match style {
None => method.name.to_string(),
Some(style) => match style {
InterfaceNamingStyle::CamelCase => to_camel_case(method.name.to_string()),
}
};
keccak256(name)[..4].try_into().ok()
}

// Helper function to generate fn selector for evm contracts
pub fn generate_selector_evm(method: &MethodInfo, style: Option<InterfaceNamingStyle>) -> Option<[u8; 4]> {
let name = match style {
None => method.name.to_string(),
Some(style) => match style {
InterfaceNamingStyle::CamelCase => to_camel_case(method.name.to_string()),
}
};

let (_, arg_types) = get_arg_props_skip_first(method);
let args = arg_types
.iter()
.map(|ty| rust_type_to_sol_type(ty))
.collect::<Result<Vec<_>, _>>()
.ok()?;
let args_str = args
.iter()
.map(|ty| ty.sol_type_name().into_owned())
.collect::<Vec<_>>()
.join(",");

let selector = format!("{}({})", name, args_str);
keccak256(selector)[..4].try_into().ok()
}

// Helper function to convert rust types to their solidity equivalent
// TODO: make sure that the impl is robust, so far only tested with "simple types"
fn rust_type_to_sol_type(ty: &Type) -> Result<DynSolType, &'static str> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there should be something in Alloy that does this or actually the entire conversion from function signature to selector? cc @gakonst

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaniPopes not sure if we have this in sol somewhere but maybe this is useful somehow

https://docs.rs/alloy-sol-types/latest/alloy_sol_types/trait.SolType.html

match ty {
Type::Path(type_path) => {
let path = &type_path.path;
let segment = path.segments.last().ok_or("Empty type path")?;
let ident = &segment.ident;
let type_name = ident.to_string();

match type_name.as_str() {
// Fixed-size types
"Address" => Ok(DynSolType::Address),
"Function" => Ok(DynSolType::Function),
"bool" | "Bool" => Ok(DynSolType::Bool),
"String" | "str" => Ok(DynSolType::String),
"Bytes" => Ok(DynSolType::Bytes),
// Fixed-size bytes
b if b.starts_with('B') => {
let size: usize = b
.trim_start_matches('B')
.parse()
.map_err(|_| "Invalid fixed bytes size")?;
if size > 0 && size <= 32 {
Ok(DynSolType::FixedBytes(size))
} else {
Err("Invalid fixed bytes size (between 1-32)")
}
}
// Fixed-size unsigned integers
u if u.starts_with('U') => {
let size: usize = u
.trim_start_matches('U')
.parse()
.map_err(|_| "Invalid uint size")?;
if size > 0 && size <= 256 && size % 8 == 0 {
Ok(DynSolType::Uint(size))
} else {
Err("Invalid uint size (multiple of 8 + leq 256)")
}
}
// Fixed-size signed integers
i if i.starts_with('I') => {
let size: usize = i
.trim_start_matches('I')
.parse()
.map_err(|_| "Invalid int size")?;
if size > 0 && size <= 256 && size % 8 == 0 {
Ok(DynSolType::Int(size))
} else {
Err("Invalid int size (must be multiple of 8, max 256)")
}
}
// Handle vecs
_ => {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
match type_name.as_str() {
"Vec" => {
let inner = args.args.first().ok_or("Empty Vec type argument")?;
if let syn::GenericArgument::Type(inner_ty) = inner {
let inner_sol_type = rust_type_to_sol_type(inner_ty)?;
Ok(DynSolType::Array(Box::new(inner_sol_type)))
} else {
Err("Invalid Vec type argument")
}
}
_ => Err("Unsupported generic type"),
}
} else {
Err("Unsupported type")
}
}
}
}
Type::Array(array) => {
let inner_sol_type = rust_type_to_sol_type(&array.elem)?;
if let syn::Expr::Lit(lit) = &array.len {
if let syn::Lit::Int(size) = &lit.lit {
let size: usize = size
.base10_digits()
.parse()
.map_err(|_| "Invalid array size")?;
Ok(DynSolType::FixedArray(Box::new(inner_sol_type), size))
} else {
Err("Invalid array size literal")
}
} else {
Err("Invalid array size expression")
}
}
Type::Tuple(tuple) => {
let inner_types = tuple
.elems
.iter()
.map(rust_type_to_sol_type)
.collect::<Result<Vec<_>, _>>()?;
Ok(DynSolType::Tuple(inner_types))
}
_ => Err("Unsupported type"),
}
}

fn to_camel_case(s: String) -> String {
let mut result = String::new();
let mut capitalize_next = false;

// Iterate through characters, skipping non-alphabetic separators
for (i, c) in s.chars().enumerate() {
if c.is_alphanumeric() {
if i == 0 {
result.push(c.to_ascii_lowercase());
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
} else {
// Set flag to capitalize next char with non-alphanumeric ones
capitalize_next = true;
}
}

result
}

// Helper function to generate the deployment code
pub fn generate_deployment_code(
struct_name: &Ident,
Expand Down
12 changes: 6 additions & 6 deletions contract-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ use alloy_sol_types::SolValue;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
parse_macro_input, Data, DeriveInput, Fields, ImplItem, ImplItemMethod, ItemImpl, ItemTrait,
ReturnType, TraitItem,
parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, ImplItem, ImplItemMethod, ItemImpl, ItemTrait, LitStr, Meta, NestedMeta, ReturnType, TraitItem
};

mod helpers;
use crate::helpers::MethodInfo;
use crate::helpers::{MethodInfo, InterfaceCompilationTarget, InterfaceArgs};

#[proc_macro_derive(Event, attributes(indexed))]
pub fn event_derive(input: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -226,7 +225,7 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {

// Generate the interface
let interface_name = format_ident!("I{}", struct_name);
let interface = helpers::generate_interface(&public_methods, &interface_name);
let interface = helpers::generate_interface(&public_methods, &interface_name, None, InterfaceCompilationTarget::R55);

// Generate initcode for deployments
let deployment_code = helpers::generate_deployment_code(struct_name, constructor);
Expand Down Expand Up @@ -326,8 +325,9 @@ fn is_payable(method: &syn::ImplItemMethod) -> bool {
}

#[proc_macro_attribute]
pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn interface(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemTrait);
let args = parse_macro_input!(attr as InterfaceArgs);
let trait_name = &input.ident;

let methods: Vec<_> = input
Expand All @@ -343,7 +343,7 @@ pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream {
.collect();

// Generate intreface implementation
let interface = helpers::generate_interface(&methods, trait_name);
let interface = helpers::generate_interface(&methods, trait_name, args.rename, args.target);

let output = quote! {
#interface
Expand Down
Loading