-
Notifications
You must be signed in to change notification settings - Fork 17
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: erc165 support interface #281
Changes from 4 commits
9a3200a
5811bf2
94ed516
252264f
de539d3
0b433b7
402e05e
0615b0f
9229b32
bec9e4c
e385668
4b6eea2
4e0f887
80af3b0
7be160e
7e6a88e
c83e84b
96989da
4cf685e
eb48b39
7d27027
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "openzeppelin-stylus-proc" | ||
description = "Procedural macros for OpenZeppelin Stylus contracts" | ||
version = "0.1.0" | ||
qalisander marked this conversation as resolved.
Show resolved
Hide resolved
|
||
authors.workspace = true | ||
edition.workspace = true | ||
license.workspace = true | ||
keywords.workspace = true | ||
repository.workspace = true | ||
|
||
|
||
[dependencies] | ||
proc-macro2.workspace = true | ||
quote.workspace = true | ||
syn.workspace = true | ||
convert_case = "0.6.0" | ||
|
||
[lints] | ||
workspace = true | ||
|
||
[lib] | ||
proc-macro = true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
use std::mem; | ||
|
||
use convert_case::{Case, Casing}; | ||
use proc_macro::TokenStream; | ||
use proc_macro2::Ident; | ||
use quote::quote; | ||
use syn::{ | ||
parse::{Parse, ParseStream}, | ||
parse_macro_input, FnArg, ItemTrait, LitStr, Result, Token, TraitItem, | ||
}; | ||
|
||
/// Computes interface id as an associated constant for the trait. | ||
pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream { | ||
Check warning on line 13 in contracts-proc/src/interface.rs GitHub Actions / clippy[clippy] contracts-proc/src/interface.rs#L13
Raw output
Check warning on line 13 in contracts-proc/src/interface.rs GitHub Actions / clippy[clippy] contracts-proc/src/interface.rs#L13
Raw output
|
||
ggonzalez94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let mut input = parse_macro_input!(input as ItemTrait); | ||
let mut output = quote! {}; | ||
|
||
let mut selectors = Vec::new(); | ||
for item in &mut input.items { | ||
let TraitItem::Fn(func) = item else { | ||
continue; | ||
}; | ||
|
||
let mut override_name = None; | ||
for attr in mem::take(&mut func.attrs) { | ||
let Some(ident) = attr.path().get_ident() else { | ||
func.attrs.push(attr); | ||
continue; | ||
}; | ||
if *ident == "selector" { | ||
if override_name.is_some() { | ||
error!(attr.path(), "more than one selector attribute"); | ||
} | ||
let args: SelectorArgs = match attr.parse_args() { | ||
Ok(args) => args, | ||
Err(error) => error!(ident, "{}", error), | ||
}; | ||
override_name = Some(args.name); | ||
continue; | ||
} | ||
func.attrs.push(attr); | ||
} | ||
|
||
let sol_name = override_name.unwrap_or_else(|| { | ||
func.sig.ident.clone().to_string().to_case(Case::Camel) | ||
}); | ||
|
||
let args = func.sig.inputs.iter(); | ||
let arg_types: Vec<_> = args | ||
.filter_map(|arg| match arg { | ||
FnArg::Typed(t) => Some(t.ty.clone()), | ||
_ => None, | ||
Check warning on line 51 in contracts-proc/src/interface.rs GitHub Actions / clippy[clippy] contracts-proc/src/interface.rs#L51
Raw output
Check warning on line 51 in contracts-proc/src/interface.rs GitHub Actions / clippy[clippy] contracts-proc/src/interface.rs#L51
Raw output
|
||
}) | ||
.collect(); | ||
|
||
let selector = quote! { u32::from_be_bytes(stylus_sdk::function_selector!(#sol_name #(, #arg_types )*)) }; | ||
selectors.push(selector); | ||
} | ||
|
||
let name = input.ident.clone(); | ||
let vis = input.vis.clone(); | ||
let attrs = input.attrs.clone(); | ||
let trait_items = input.items.clone(); | ||
let (_impl_generics, ty_generics, where_clause) = | ||
input.generics.split_for_impl(); | ||
|
||
output.extend(quote! { | ||
#(#attrs)* | ||
#vis trait #name #ty_generics #where_clause { | ||
#(#trait_items)* | ||
|
||
/// Solidity interface id associated with current trait. | ||
const INTERFACE_ID: u32 = { | ||
#(#selectors)^* | ||
}; | ||
} | ||
}); | ||
|
||
output.into() | ||
} | ||
|
||
struct SelectorArgs { | ||
name: String, | ||
} | ||
|
||
impl Parse for SelectorArgs { | ||
fn parse(input: ParseStream) -> Result<Self> { | ||
let mut name = None; | ||
|
||
if input.is_empty() { | ||
error!(@input.span(), "missing id or text argument"); | ||
} | ||
|
||
while !input.is_empty() { | ||
let ident: Ident = input.parse()?; | ||
let _: Token![=] = input.parse()?; | ||
|
||
match ident.to_string().as_str() { | ||
"name" => { | ||
let lit: LitStr = input.parse()?; | ||
if name.is_some() { | ||
error!(@lit, r#"only one "name" is allowed"#); | ||
} | ||
name = Some(lit.value()); | ||
} | ||
_ => error!(@ident, "Unknown selector attribute"), | ||
} | ||
|
||
// allow a comma | ||
let _: Result<Token![,]> = input.parse(); | ||
} | ||
|
||
if let Some(name) = name { | ||
Ok(Self { name }) | ||
} else { | ||
error!(@input.span(), r#""name" is required"#); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// TODO#q: add crate documentation. | ||
|
||
extern crate proc_macro; | ||
Check warning on line 3 in contracts-proc/src/lib.rs GitHub Actions / clippy[clippy] contracts-proc/src/lib.rs#L3
Raw output
Check warning on line 3 in contracts-proc/src/lib.rs GitHub Actions / clippy[clippy] contracts-proc/src/lib.rs#L3
Raw output
|
||
use proc_macro::TokenStream; | ||
|
||
/// Shorthand to print nice errors. | ||
macro_rules! error { | ||
($tokens:expr, $($msg:expr),+ $(,)?) => {{ | ||
let error = syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+)); | ||
return error.to_compile_error().into(); | ||
}}; | ||
(@ $tokens:expr, $($msg:expr),+ $(,)?) => {{ | ||
return Err(syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+))) | ||
}}; | ||
} | ||
|
||
mod interface; | ||
|
||
/// Computes interface id as an associated constant for the trait. | ||
#[proc_macro_attribute] | ||
pub fn interface(attr: TokenStream, input: TokenStream) -> TokenStream { | ||
interface::interface(attr, input) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ extern crate alloc; | |
|
||
use alloc::vec::Vec; | ||
|
||
use alloy_primitives::{Address, U256}; | ||
use alloy_primitives::{Address, FixedBytes, U256}; | ||
use openzeppelin_stylus::{ | ||
token::erc20::{ | ||
extensions::{capped, Capped, Erc20Metadata, IErc20Burnable}, | ||
|
@@ -105,4 +105,12 @@ impl Erc20Example { | |
self.pausable.when_not_paused()?; | ||
self.erc20.transfer_from(from, to, value).map_err(|e| e.into()) | ||
} | ||
|
||
fn supports_interface( | ||
interface_id: FixedBytes<4>, | ||
) -> Result<bool, Vec<u8>> { | ||
let interface_id = u32::from_be_bytes(*interface_id); | ||
let supported = interface_id == <Erc20 as IErc20>::INTERFACE_ID; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It also should check for IErc165 interface id There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be done relatively straight forward with something like this(not sure if FixedBytes<4> can be converted to [u8; 4] and the return type can be simplified, but I did it for the simplicity of the example): pub trait ERC165 {
const INTERFACE_ID: [u8; 4] = [0x01, 0xff, 0xff, 0xff];
fn supports_erc165_interface(&self, interface_id: [u8; 4]) -> bool {
interface_id == Self::INTERFACE_ID
}
} And then this contract can do: impl ERC165 for Erc20Example {}
impl Erc20Example {
fn supports_interface(&self, interface_id: [u8; 4]) -> bool {
interface_id == <Erc20 as IErc20>::INTERFACE_ID ||
ERC165::supports_erc165_interface(self, interface_id)
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure there are better ways to achieve the same, but something like this should work |
||
Ok(supported) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should also change description of contracts from
OpenZeppelin Contracts for Stylus
toOpenZeppelin smart contracts library for Stylus
? Just seeking for consistency among projects.cc @ggonzalez94
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the original one slightly more, since it is more concise. But I don't have a strong opinion tbh. Agreed with your comment @qalisander that we should keep both consistent.