Skip to main content

aptos_sdk_macros/
lib.rs

1//! Procedural macros for type-safe Aptos contract bindings.
2//!
3//! This crate provides macros for generating Rust bindings from Move module ABIs
4//! at compile time.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use aptos_sdk_macros::aptos_contract;
10//!
11//! aptos_contract! {
12//!     name: CoinModule,
13//!     abi: r#"{"address": "0x1", "name": "coin", ...}"#
14//! }
15//!
16//! // Generated:
17//! // pub struct CoinModule;
18//! // impl CoinModule {
19//! //     pub fn transfer(...) -> AptosResult<TransactionPayload> { ... }
20//! //     pub async fn view_balance(...) -> AptosResult<Vec<Value>> { ... }
21//! // }
22//! ```
23
24use proc_macro::TokenStream;
25use proc_macro2::Span;
26use quote::quote;
27use syn::{LitStr, parse_macro_input, spanned::Spanned};
28
29mod abi;
30mod codegen;
31mod parser;
32
33use abi::MoveModuleABI;
34use codegen::generate_contract_impl;
35
36/// Generates type-safe contract bindings from an ABI.
37///
38/// # Syntax
39///
40/// ```rust,ignore
41/// aptos_contract! {
42///     name: StructName,
43///     abi: "{ ... JSON ABI ... }",
44///     // Optional: Move source for better parameter names
45///     source: "module 0x1::coin { ... }"
46/// }
47/// ```
48///
49/// # Example
50///
51/// ```rust,ignore
52/// use aptos_sdk_macros::aptos_contract;
53///
54/// aptos_contract! {
55///     name: AptosCoin,
56///     abi: r#"{
57///         "address": "0x1",
58///         "name": "aptos_coin",
59///         "exposed_functions": [
60///             {
61///                 "name": "transfer",
62///                 "visibility": "public",
63///                 "is_entry": true,
64///                 "is_view": false,
65///                 "generic_type_params": [],
66///                 "params": ["&signer", "address", "u64"],
67///                 "return": []
68///             }
69///         ],
70///         "structs": []
71///     }"#
72/// }
73///
74/// // Now you can use:
75/// let payload = AptosCoin::transfer(recipient_addr, 1000)?;
76/// ```
77#[proc_macro]
78pub fn aptos_contract(input: TokenStream) -> TokenStream {
79    let input = parse_macro_input!(input as parser::ContractInput);
80
81    // Parse ABI - use the name's span for error reporting since that's a known token
82    let abi: MoveModuleABI = match serde_json::from_str(&input.abi) {
83        Ok(abi) => abi,
84        Err(e) => {
85            return syn::Error::new(input.name.span(), format!("Failed to parse ABI JSON: {e}"))
86                .to_compile_error()
87                .into();
88        }
89    };
90
91    // Parse optional Move source
92    let source_info = input.source.as_ref().map(|s| parser::parse_move_source(s));
93
94    // Generate the implementation
95    let tokens = generate_contract_impl(&input.name, &abi, source_info.as_ref());
96
97    tokens.into()
98}
99
100/// Generates contract bindings from an ABI file path.
101///
102/// # Example
103///
104/// ```rust,ignore
105/// use aptos_sdk_macros::aptos_contract_file;
106///
107/// aptos_contract_file!("abi/my_module.json", MyModule);
108/// ```
109#[proc_macro]
110pub fn aptos_contract_file(input: TokenStream) -> TokenStream {
111    let input = parse_macro_input!(input as parser::FileInput);
112
113    // Read the file content at compile time
114    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
115    let manifest_path = std::path::Path::new(&manifest_dir);
116    let file_path = manifest_path.join(&input.path);
117
118    // SECURITY: Verify the resolved path is under CARGO_MANIFEST_DIR to prevent
119    // path traversal attacks (e.g., "../../../../etc/passwd").
120    // Canonicalization failures are treated as errors to ensure this check
121    // is never silently skipped.
122    let canonical_manifest = match manifest_path.canonicalize() {
123        Ok(p) => p,
124        Err(e) => {
125            return syn::Error::new(
126                input.name.span(),
127                format!("Failed to resolve project directory: {e}"),
128            )
129            .to_compile_error()
130            .into();
131        }
132    };
133    let canonical_file = match file_path.canonicalize() {
134        Ok(p) => p,
135        Err(e) => {
136            return syn::Error::new(
137                input.name.span(),
138                format!("Failed to resolve ABI file path '{}': {e}", input.path),
139            )
140            .to_compile_error()
141            .into();
142        }
143    };
144    if !canonical_file.starts_with(&canonical_manifest) {
145        return syn::Error::new(
146            input.name.span(),
147            format!(
148                "ABI file path '{}' resolves outside the project directory",
149                input.path
150            ),
151        )
152        .to_compile_error()
153        .into();
154    }
155
156    let abi_content = match std::fs::read_to_string(&file_path) {
157        Ok(content) => content,
158        Err(e) => {
159            // Use name's span for better error location
160            return syn::Error::new(
161                input.name.span(),
162                format!("Failed to read ABI file '{}': {e}", file_path.display()),
163            )
164            .to_compile_error()
165            .into();
166        }
167    };
168
169    let abi: MoveModuleABI = match serde_json::from_str(&abi_content) {
170        Ok(abi) => abi,
171        Err(e) => {
172            return syn::Error::new(
173                input.name.span(),
174                format!(
175                    "Failed to parse ABI JSON from '{}': {e}",
176                    file_path.display(),
177                ),
178            )
179            .to_compile_error()
180            .into();
181        }
182    };
183
184    // Read optional source file - emit error if source_path is provided but unreadable
185    let source_info = if let Some(source_path) = input.source_path.as_ref() {
186        let source_file = std::path::Path::new(&manifest_dir).join(source_path);
187        match std::fs::read_to_string(&source_file) {
188            Ok(content) => Some(parser::parse_move_source(&content)),
189            Err(e) => {
190                return syn::Error::new(
191                    input.name.span(),
192                    format!(
193                        "Failed to read Move source file '{}': {e}",
194                        source_file.display(),
195                    ),
196                )
197                .to_compile_error()
198                .into();
199            }
200        }
201    } else {
202        None
203    };
204
205    let tokens = generate_contract_impl(&input.name, &abi, source_info.as_ref());
206
207    tokens.into()
208}
209
210/// Derive macro for Move-compatible struct serialization.
211///
212/// Implements BCS serialization and the necessary traits for
213/// using a Rust struct as a Move struct argument or return type.
214///
215/// # Example
216///
217/// ```rust,ignore
218/// use aptos_sdk_macros::MoveStruct;
219///
220/// #[derive(MoveStruct)]
221/// #[move_struct(address = "0x1", module = "coin", name = "CoinStore")]
222/// pub struct CoinStore {
223///     pub coin: u64,
224///     pub frozen: bool,
225/// }
226/// ```
227#[proc_macro_derive(MoveStruct, attributes(move_struct))]
228pub fn derive_move_struct(input: TokenStream) -> TokenStream {
229    let input = parse_macro_input!(input as syn::DeriveInput);
230
231    let name = &input.ident;
232
233    // Parse attributes - collect errors to report them properly
234    let mut address = None;
235    let mut module = None;
236    let mut struct_name = None;
237    let mut parse_error: Option<syn::Error> = None;
238
239    for attr in &input.attrs {
240        if attr.path().is_ident("move_struct") {
241            let result = attr.parse_nested_meta(|meta| {
242                if meta.path.is_ident("address") {
243                    let value: LitStr = meta.value()?.parse()?;
244                    address = Some(value.value());
245                } else if meta.path.is_ident("module") {
246                    let value: LitStr = meta.value()?.parse()?;
247                    module = Some(value.value());
248                } else if meta.path.is_ident("name") {
249                    let value: LitStr = meta.value()?.parse()?;
250                    struct_name = Some(value.value());
251                } else {
252                    return Err(syn::Error::new(
253                        meta.path.span(),
254                        format!(
255                            "Unknown attribute '{}'. Expected 'address', 'module', or 'name'",
256                            meta.path
257                                .get_ident()
258                                .map_or_else(|| "?".to_string(), ToString::to_string)
259                        ),
260                    ));
261                }
262                Ok(())
263            });
264
265            if let Err(e) = result {
266                parse_error = Some(e);
267                break;
268            }
269        }
270    }
271
272    // Return any parsing errors
273    if let Some(e) = parse_error {
274        return e.to_compile_error().into();
275    }
276
277    let address = address.unwrap_or_else(|| "0x1".to_string());
278    let module = module.unwrap_or_else(|| "unknown".to_string());
279    let struct_name = struct_name.unwrap_or_else(|| name.to_string());
280
281    let type_tag = format!("{address}::{module}::{struct_name}");
282    // Convert String to LitStr for quote! interpolation
283    let type_tag_lit = LitStr::new(&type_tag, Span::call_site());
284
285    let expanded = quote! {
286        impl #name {
287            /// Returns the Move type tag for this struct.
288            pub fn type_tag() -> &'static str {
289                #type_tag_lit
290            }
291
292            /// Serializes this struct to BCS bytes.
293            pub fn to_bcs(&self) -> ::aptos_sdk::error::AptosResult<Vec<u8>> {
294                ::aptos_sdk::aptos_bcs::to_bytes(self)
295                    .map_err(|e| ::aptos_sdk::error::AptosError::Bcs(e.to_string()))
296            }
297
298            /// Deserializes this struct from BCS bytes.
299            pub fn from_bcs(bytes: &[u8]) -> ::aptos_sdk::error::AptosResult<Self> {
300                ::aptos_sdk::aptos_bcs::from_bytes(bytes)
301                    .map_err(|e| ::aptos_sdk::error::AptosError::Bcs(e.to_string()))
302            }
303        }
304    };
305
306    expanded.into()
307}