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 file_path = std::path::Path::new(&manifest_dir).join(&input.path);
116
117    let abi_content = match std::fs::read_to_string(&file_path) {
118        Ok(content) => content,
119        Err(e) => {
120            // Use name's span for better error location
121            return syn::Error::new(
122                input.name.span(),
123                format!("Failed to read ABI file '{}': {e}", file_path.display()),
124            )
125            .to_compile_error()
126            .into();
127        }
128    };
129
130    let abi: MoveModuleABI = match serde_json::from_str(&abi_content) {
131        Ok(abi) => abi,
132        Err(e) => {
133            return syn::Error::new(
134                input.name.span(),
135                format!(
136                    "Failed to parse ABI JSON from '{}': {e}",
137                    file_path.display(),
138                ),
139            )
140            .to_compile_error()
141            .into();
142        }
143    };
144
145    // Read optional source file - emit error if source_path is provided but unreadable
146    let source_info = if let Some(source_path) = input.source_path.as_ref() {
147        let source_file = std::path::Path::new(&manifest_dir).join(source_path);
148        match std::fs::read_to_string(&source_file) {
149            Ok(content) => Some(parser::parse_move_source(&content)),
150            Err(e) => {
151                return syn::Error::new(
152                    input.name.span(),
153                    format!(
154                        "Failed to read Move source file '{}': {e}",
155                        source_file.display(),
156                    ),
157                )
158                .to_compile_error()
159                .into();
160            }
161        }
162    } else {
163        None
164    };
165
166    let tokens = generate_contract_impl(&input.name, &abi, source_info.as_ref());
167
168    tokens.into()
169}
170
171/// Derive macro for Move-compatible struct serialization.
172///
173/// Implements BCS serialization and the necessary traits for
174/// using a Rust struct as a Move struct argument or return type.
175///
176/// # Example
177///
178/// ```rust,ignore
179/// use aptos_sdk_macros::MoveStruct;
180///
181/// #[derive(MoveStruct)]
182/// #[move_struct(address = "0x1", module = "coin", name = "CoinStore")]
183/// pub struct CoinStore {
184///     pub coin: u64,
185///     pub frozen: bool,
186/// }
187/// ```
188#[proc_macro_derive(MoveStruct, attributes(move_struct))]
189pub fn derive_move_struct(input: TokenStream) -> TokenStream {
190    let input = parse_macro_input!(input as syn::DeriveInput);
191
192    let name = &input.ident;
193
194    // Parse attributes - collect errors to report them properly
195    let mut address = None;
196    let mut module = None;
197    let mut struct_name = None;
198    let mut parse_error: Option<syn::Error> = None;
199
200    for attr in &input.attrs {
201        if attr.path().is_ident("move_struct") {
202            let result = attr.parse_nested_meta(|meta| {
203                if meta.path.is_ident("address") {
204                    let value: LitStr = meta.value()?.parse()?;
205                    address = Some(value.value());
206                } else if meta.path.is_ident("module") {
207                    let value: LitStr = meta.value()?.parse()?;
208                    module = Some(value.value());
209                } else if meta.path.is_ident("name") {
210                    let value: LitStr = meta.value()?.parse()?;
211                    struct_name = Some(value.value());
212                } else {
213                    return Err(syn::Error::new(
214                        meta.path.span(),
215                        format!(
216                            "Unknown attribute '{}'. Expected 'address', 'module', or 'name'",
217                            meta.path
218                                .get_ident()
219                                .map_or_else(|| "?".to_string(), ToString::to_string)
220                        ),
221                    ));
222                }
223                Ok(())
224            });
225
226            if let Err(e) = result {
227                parse_error = Some(e);
228                break;
229            }
230        }
231    }
232
233    // Return any parsing errors
234    if let Some(e) = parse_error {
235        return e.to_compile_error().into();
236    }
237
238    let address = address.unwrap_or_else(|| "0x1".to_string());
239    let module = module.unwrap_or_else(|| "unknown".to_string());
240    let struct_name = struct_name.unwrap_or_else(|| name.to_string());
241
242    let type_tag = format!("{address}::{module}::{struct_name}");
243    // Convert String to LitStr for quote! interpolation
244    let type_tag_lit = LitStr::new(&type_tag, Span::call_site());
245
246    let expanded = quote! {
247        impl #name {
248            /// Returns the Move type tag for this struct.
249            pub fn type_tag() -> &'static str {
250                #type_tag_lit
251            }
252
253            /// Serializes this struct to BCS bytes.
254            pub fn to_bcs(&self) -> ::aptos_sdk::error::AptosResult<Vec<u8>> {
255                ::aptos_sdk::aptos_bcs::to_bytes(self)
256                    .map_err(|e| ::aptos_sdk::error::AptosError::Bcs(e.to_string()))
257            }
258
259            /// Deserializes this struct from BCS bytes.
260            pub fn from_bcs(bytes: &[u8]) -> ::aptos_sdk::error::AptosResult<Self> {
261                ::aptos_sdk::aptos_bcs::from_bytes(bytes)
262                    .map_err(|e| ::aptos_sdk::error::AptosError::Bcs(e.to_string()))
263            }
264        }
265    };
266
267    expanded.into()
268}