Skip to content

Commit

Permalink
Merge pull request #105 from OffchainLabs/derive-solidity-error
Browse files Browse the repository at this point in the history
Full `#[derive(SolidityError)]`
  • Loading branch information
rachel-bousfield authored Feb 22, 2024
2 parents 330ef7c + 1a0f2a9 commit ad8b9b7
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 60 deletions.
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ Please provide a summary of the changes and any backward incompatibilities.
- [ ] I have read the [DCO][DCO] and ensured that these changes comply.
- [ ] I assign this work under its [open source licensing][terms].

[DCO]: licenses/DCO.txt
[terms]: licenses/COPYRIGHT.md
[DCO]: https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/DCO.txt
[terms]: https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

The Stylus SDK enables smart contract developers to write programs for **Arbitrum chains** written in the [Rust](https://www.rust-lang.org/tools/install) programming language. Stylus programs are compiled to [WebAssembly](https://webassembly.org/) and can then be deployed on-chain to execute alongside Solidity smart contracts. Stylus programs are not only orders of magnitude cheaper and faster but also enable what was thought to be previously impossible for WebAssembly: **EVM-interoperability**.

For information about deploying Rust smart contracts, see the [Cargo Stylus CLI Tool](CargoStylus). For more information about Stylus, see [Stylus: A Gentle Introduction](https://docs.arbitrum.io/stylus/stylus-gentle-introduction). For a simpler intro to Stylus Rust development, see the [Quick Start guide](https://docs.arbitrum.io/stylus/stylus-quickstart).
For information about deploying Rust smart contracts, see the [Cargo Stylus CLI Tool](https://github.com/OffchainLabs/cargo-stylus). For more information about Stylus, see [Stylus: A Gentle Introduction](https://docs.arbitrum.io/stylus/stylus-gentle-introduction). For a simpler intro to Stylus Rust development, see the [Quick Start guide](https://docs.arbitrum.io/stylus/stylus-quickstart).

Comprehensive documentation on the Rust SDK can be found [here](https://docs.arbitrum.io/stylus/rust-sdk-guide).

Expand Down
27 changes: 25 additions & 2 deletions stylus-proc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,33 @@ pub fn derive_erase(input: TokenStream) -> TokenStream {
storage::derive_erase(input)
}

/// For an error type `E`, implement `From<E>` for `Vec<u8>`.
/// Allows an error `enum` to be used in method signatures.
///
/// ```ignore
/// sol! {
/// error InsufficientBalance(address from, uint256 have, uint256 want);
/// error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
/// }
///
/// #[derive(SolidityError)]
/// pub enum Erc20Error {
/// InsufficientBalance(InsufficientBalance),
/// InsufficientAllowance(InsufficientAllowance),
/// }
///
/// #[external]
/// impl Contract {
/// pub fn fallible_method() -> Result<(), Erc20Error> {
/// // code that might revert
/// }
/// }
/// ```
///
/// Under the hood, the above macro works by implementing `From<Erc20Error>` for `Vec<u8>`
/// along with printing code for abi-export.
#[proc_macro_derive(SolidityError)]
pub fn derive_solidity_error(input: TokenStream) -> TokenStream {
storage::derive_solidity_error(input)
methods::error::derive_solidity_error(input)
}

/// Defines the entrypoint, which is where Stylus execution begins.
Expand Down
57 changes: 57 additions & 0 deletions stylus-proc/src/methods/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Fields, ItemEnum};

pub fn derive_solidity_error(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemEnum);
let name = &input.ident;
let mut match_arms = quote!();
let mut errors = vec![];
for variant in input.variants {
let variant_name = variant.ident;
let error = match variant.fields {
Fields::Unnamed(e) if variant.fields.len() == 1 => e.unnamed.first().unwrap().clone(),
_ => error!(variant.fields, "Variant not a 1-tuple"),
};
match_arms.extend(quote! {
#name::#variant_name(e) => stylus_sdk::alloy_sol_types::SolError::encode(&e),
});
errors.push(error);
}
let mut output = quote! {
impl From<#name> for alloc::vec::Vec<u8> {
fn from(err: #name) -> alloc::vec::Vec<u8> {
match err {
#match_arms
}
}
}
};

if cfg!(feature = "export-abi") {
output.extend(quote! {
impl stylus_sdk::abi::export::internal::InnerTypes for #name {
fn inner_types() -> alloc::vec::Vec<stylus_sdk::abi::export::internal::InnerType> {
use alloc::{format, vec};
use core::any::TypeId;
use stylus_sdk::abi::export::internal::InnerType;
use stylus_sdk::alloy_sol_types::SolError;

vec![
#(
InnerType {
name: format!("error {};", <#errors as SolError>::SIGNATURE.replace(',', ", ")),
id: TypeId::of::<#errors>(),
}
),*
]
}
}
});
}

output.into()
}
32 changes: 25 additions & 7 deletions stylus-proc/src/methods/external.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022-2023, Offchain Labs, Inc.
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use crate::types::{self, Purity};
Expand All @@ -20,6 +20,7 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream {
let mut selectors = quote!();
let mut match_selectors = quote!();
let mut abi = quote!();
let mut types = vec![];

for item in input.items.iter_mut() {
let ImplItem::Method(method) = item else {
Expand Down Expand Up @@ -155,10 +156,7 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream {
}
};
let result = Self::#name(#storage #(#expand_args, )* );
match result {
Ok(result) => Some(Ok(internal::encode_return_type(result))),
Err(err) => Some(Err(err.into())),
}
Some(EncodableReturnType::encode(result))
}
});

Expand Down Expand Up @@ -258,7 +256,7 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[inline(always)]
fn route(storage: &mut S, selector: u32, input: &[u8]) -> Option<stylus_sdk::ArbResult> {
use stylus_sdk::{function_selector, alloy_sol_types::SolType};
use stylus_sdk::abi::{internal, AbiType, Router};
use stylus_sdk::abi::{internal, internal::EncodableReturnType, AbiType, Router};
use alloc::vec;

#[cfg(feature = "export-abi")]
Expand All @@ -281,6 +279,24 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream {
return router.into();
}

for item in input.items.iter_mut() {
let ImplItem::Method(method) = item else {
continue;
};
if let ReturnType::Type(_, ty) = &method.sig.output {
types.push(ty);
}
}

let type_decls = quote! {
let mut seen = HashSet::new();
for item in [].iter() #(.chain(&<#types as InnerTypes>::inner_types()))* {
if seen.insert(item.id) {
writeln!(f, "\n {}", item.name)?;
}
}
};

let name = match *self_ty.clone() {
Type::Path(path) => path.path.segments.last().unwrap().ident.clone().to_string(),
_ => error!(self_ty, "Can't generate ABI for unnamed type"),
Expand Down Expand Up @@ -312,12 +328,14 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream {
fn fmt_abi(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use stylus_sdk::abi::{AbiType, GenerateAbi};
use stylus_sdk::abi::internal::write_solidity_returns;
use stylus_sdk::abi::export::{underscore_if_sol};
use stylus_sdk::abi::export::{underscore_if_sol, internal::InnerTypes};
use std::collections::HashSet;
#(#inherited_abis)*
write!(f, "interface I{}", #name)?;
#is_clause
write!(f, " {{")?;
#abi
#type_decls
writeln!(f, "}}")?;
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions stylus-proc/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

pub mod entrypoint;
pub mod error;
pub mod external;
36 changes: 4 additions & 32 deletions stylus-proc/src/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// Copyright 2023, Offchain Labs, Inc.
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use crate::storage::proc::{SolidityField, SolidityFields, SolidityStruct, SolidityStructs};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use std::mem;
use syn::{
parse_macro_input, punctuated::Punctuated, Fields, Index, ItemEnum, ItemStruct, Token, Type,
};
use syn::{parse_macro_input, punctuated::Punctuated, Index, ItemStruct, Token, Type};

mod proc;

Expand Down Expand Up @@ -216,38 +214,12 @@ pub fn derive_erase(input: TokenStream) -> TokenStream {
self.#ident.erase();
});
}
let output = quote! {
quote! {
impl #impl_generics stylus_sdk::storage::Erase for #name #ty_generics #where_clause {
fn erase(&mut self) {
#erase_fields
}
}
};
output.into()
}

pub fn derive_solidity_error(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemEnum);
let name = &input.ident;
let mut match_arms = quote! {};
for variant in input.variants.iter() {
let variant_name = &variant.ident;
match variant.fields {
Fields::Unnamed(_) if variant.fields.len() == 1 => {}
_ => panic!("SolidityError: Each variant must be a tuple struct with one field"),
}
match_arms.extend(quote! {
#name::#variant_name(e) => e.encode(),
})
}
let output = quote! {
impl From<#name> for ::alloc::vec::Vec<u8> {
fn from(err: #name) -> ::alloc::vec::Vec<u8> {
match err {
#match_arms
}
}
}
};
output.into()
.into()
}
82 changes: 82 additions & 0 deletions stylus-sdk/src/abi/export/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

//! This module provides functions for code generated by `stylus-sdk-proc` for the `export-abi` command.
//! Most users shouldn't call these.
use alloy_primitives::{Address, FixedBytes, Signed, Uint};
use core::any::TypeId;

/// Represents a unique Solidity Type.
pub struct InnerType {
/// Full interface string.
pub name: String,
/// Unique identifier for de-duplication when printing interfaces.
pub id: TypeId,
}

/// Trait for collecting structs and error types.
pub trait InnerTypes {
/// Collect any structs and errors under the type.
/// Empty for primitives.
fn inner_types() -> Vec<InnerType> {
vec![]
}
}

impl<O, E> InnerTypes for Result<O, E>
where
O: InnerTypes,
E: InnerTypes,
{
fn inner_types() -> Vec<InnerType> {
let mut out = O::inner_types();
out.extend(E::inner_types());
out
}
}

impl<T: InnerTypes> InnerTypes for Vec<T> {
fn inner_types() -> Vec<InnerType> {
T::inner_types()
}
}

impl<const N: usize, T: InnerTypes> InnerTypes for [T; N] {
fn inner_types() -> Vec<InnerType> {
T::inner_types()
}
}

macro_rules! impl_inner {
($ty:ident $($rest:ident)+) => {
impl_inner!($ty);
impl_inner!($($rest)+);
};
($ty:ident) => {
impl InnerTypes for $ty {}
};
}

impl_inner!(bool u8 u16 u32 u64 u128 i8 i16 i32 i64 i128 String Address);

impl<const B: usize, const L: usize> InnerTypes for Uint<B, L> {}
impl<const B: usize, const L: usize> InnerTypes for Signed<B, L> {}
impl<const N: usize> InnerTypes for FixedBytes<N> {}

macro_rules! impl_tuple {
() => {
impl InnerTypes for () {}
};
($first:ident $(, $rest:ident)*) => {
impl<$first: InnerTypes $(, $rest: InnerTypes)*> InnerTypes for ( $first $(, $rest)* , ) {
fn inner_types() -> Vec<InnerType> {
vec![]
}
}

impl_tuple! { $($rest),* }
};
}

impl_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023, Offchain Labs, Inc.
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

//! Traits for exporting Solidity interfaces.
Expand All @@ -12,6 +12,9 @@ use core::{fmt, marker::PhantomData};
use lazy_static::lazy_static;
use regex::Regex;

#[doc(hidden)]
pub mod internal;

/// Trait for storage types so that users can print a Solidity interface to the console.
/// This is auto-derived via the [`external`] macro when the `export-abi` feature is enabled.
///
Expand Down
Loading

0 comments on commit ad8b9b7

Please sign in to comment.