diff --git a/README.md b/README.md index 764e9b44..48da5d88 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/examples/erc20/Cargo.lock b/examples/erc20/Cargo.lock index 7988a62d..8538f33b 100644 --- a/examples/erc20/Cargo.lock +++ b/examples/erc20/Cargo.lock @@ -19,7 +19,7 @@ checksum = "e416903084d3392ebd32d94735c395d6709415b76c7728e594d3f996f2b03e65" dependencies = [ "alloy-rlp", "bytes", - "cfg-if", + "cfg-if 1.0.0", "const-hex", "derive_more", "hex-literal", @@ -137,6 +137,12 @@ dependencies = [ "libc", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -149,7 +155,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08849ed393c907c90016652a01465a12d86361cd38ad2a7de026c56a520cc259" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "hex", "serde", @@ -294,7 +300,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi", ] @@ -368,11 +374,18 @@ version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "mini-alloc" version = "0.4.2" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", + "wee_alloc", ] [[package]] @@ -623,7 +636,7 @@ version = "0.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", - "cfg-if", + "cfg-if 1.0.0", "convert_case 0.6.0", "lazy_static", "proc-macro2", @@ -640,7 +653,7 @@ version = "0.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", - "cfg-if", + "cfg-if 1.0.0", "derivative", "fnv", "hex", @@ -689,7 +702,7 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "redox_syscall", "rustix", @@ -756,6 +769,40 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/examples/erc20/src/erc20.rs b/examples/erc20/src/erc20.rs index e3a86696..12191bcf 100644 --- a/examples/erc20/src/erc20.rs +++ b/examples/erc20/src/erc20.rs @@ -1,4 +1,4 @@ -use alloc::{string::String, vec::Vec}; +use alloc::string::String; use core::marker::PhantomData; use stylus_sdk::{ alloy_primitives::{Address, U256}, @@ -36,21 +36,12 @@ sol! { error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want); } +#[derive(SolidityError)] pub enum Erc20Error { InsufficientBalance(InsufficientBalance), InsufficientAllowance(InsufficientAllowance), } -// We will soon provide a #[derive(SolidityError)] to clean this up -impl From for Vec { - fn from(err: Erc20Error) -> Vec { - match err { - Erc20Error::InsufficientBalance(e) => e.encode(), - Erc20Error::InsufficientAllowance(e) => e.encode(), - } - } -} - // These methods aren't exposed to other contracts // Note: modifying storage will become much prettier soon impl Erc20 { diff --git a/stylus-proc/src/lib.rs b/stylus-proc/src/lib.rs index 2b9893bd..471b0971 100644 --- a/stylus-proc/src/lib.rs +++ b/stylus-proc/src/lib.rs @@ -264,6 +264,35 @@ pub fn derive_erase(input: TokenStream) -> TokenStream { storage::derive_erase(input) } +/// 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` for `Vec` +/// along with printing code for abi-export. +#[proc_macro_derive(SolidityError)] +pub fn derive_solidity_error(input: TokenStream) -> TokenStream { + methods::error::derive_solidity_error(input) +} + /// Defines the entrypoint, which is where Stylus execution begins. /// Without it the contract will fail to pass [`cargo stylus check`][check]. /// Most commonly this macro is used to annotate the top level storage `struct`. diff --git a/stylus-proc/src/methods/error.rs b/stylus-proc/src/methods/error.rs new file mode 100644 index 00000000..968837f4 --- /dev/null +++ b/stylus-proc/src/methods/error.rs @@ -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 { + fn from(err: #name) -> alloc::vec::Vec { + 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 { + 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() +} diff --git a/stylus-proc/src/methods/external.rs b/stylus-proc/src/methods/external.rs index 77b6cd9c..0956b356 100644 --- a/stylus-proc/src/methods/external.rs +++ b/stylus-proc/src/methods/external.rs @@ -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 { @@ -278,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"), @@ -309,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(()) } diff --git a/stylus-proc/src/methods/mod.rs b/stylus-proc/src/methods/mod.rs index 256b033d..d3e69d2c 100644 --- a/stylus-proc/src/methods/mod.rs +++ b/stylus-proc/src/methods/mod.rs @@ -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; diff --git a/stylus-proc/src/storage/mod.rs b/stylus-proc/src/storage/mod.rs index 3138f1c6..cd6b38af 100644 --- a/stylus-proc/src/storage/mod.rs +++ b/stylus-proc/src/storage/mod.rs @@ -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 use crate::storage::proc::{SolidityField, SolidityFields, SolidityStruct, SolidityStructs}; @@ -214,12 +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() + } + .into() } diff --git a/stylus-sdk/src/abi/export/internal.rs b/stylus-sdk/src/abi/export/internal.rs new file mode 100644 index 00000000..129183a1 --- /dev/null +++ b/stylus-sdk/src/abi/export/internal.rs @@ -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 { + vec![] + } +} + +impl InnerTypes for Result +where + O: InnerTypes, + E: InnerTypes, +{ + fn inner_types() -> Vec { + let mut out = O::inner_types(); + out.extend(E::inner_types()); + out + } +} + +impl InnerTypes for Vec { + fn inner_types() -> Vec { + T::inner_types() + } +} + +impl InnerTypes for [T; N] { + fn inner_types() -> Vec { + 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 InnerTypes for Uint {} +impl InnerTypes for Signed {} +impl InnerTypes for FixedBytes {} + +macro_rules! impl_tuple { + () => { + impl InnerTypes for () {} + }; + ($first:ident $(, $rest:ident)*) => { + impl<$first: InnerTypes $(, $rest: InnerTypes)*> InnerTypes for ( $first $(, $rest)* , ) { + fn inner_types() -> Vec { + 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); diff --git a/stylus-sdk/src/abi/export.rs b/stylus-sdk/src/abi/export/mod.rs similarity index 97% rename from stylus-sdk/src/abi/export.rs rename to stylus-sdk/src/abi/export/mod.rs index c81c6c50..535291c9 100644 --- a/stylus-sdk/src/abi/export.rs +++ b/stylus-sdk/src/abi/export/mod.rs @@ -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. @@ -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. /// diff --git a/stylus-sdk/src/abi/internal.rs b/stylus-sdk/src/abi/internal.rs index 1ad99029..84cb68df 100644 --- a/stylus-sdk/src/abi/internal.rs +++ b/stylus-sdk/src/abi/internal.rs @@ -76,8 +76,8 @@ where if abi == "()" { Ok(()) } else if abi.starts_with('(') { - write!(f, " returns {}", abi) + write!(f, " returns {abi}") } else { - write!(f, " returns ({})", abi) + write!(f, " returns ({abi})") } } diff --git a/stylus-sdk/src/deploy/mod.rs b/stylus-sdk/src/deploy/mod.rs index 2d4155ca..1f295048 100644 --- a/stylus-sdk/src/deploy/mod.rs +++ b/stylus-sdk/src/deploy/mod.rs @@ -4,7 +4,7 @@ //! Deploy other contracts. //! //! Currently this module only supports low-level contract creation via [`RawDeploy`], -//! but word is being done to introduce high-level deployment patterns. +//! but work is being done to introduce high-level deployment patterns. pub use raw::RawDeploy; diff --git a/stylus-sdk/src/deploy/raw.rs b/stylus-sdk/src/deploy/raw.rs index 8cd304b7..74ea6805 100644 --- a/stylus-sdk/src/deploy/raw.rs +++ b/stylus-sdk/src/deploy/raw.rs @@ -80,7 +80,7 @@ impl RawDeploy { /// /// # Safety /// - /// Note that the EVM allows init code to make calls to other contracts, which provides a vector force + /// Note that the EVM allows init code to make calls to other contracts, which provides a vector for /// reentrancy. This means that this method may enable storage aliasing if used in the middle of a storage /// reference's lifetime and if reentrancy is allowed. ///