soroban_rs_macros/
lib.rs

1//! # Soroban Macros
2//!
3//! This crate provides procedural macros for working with Soroban smart contracts.
4//!
5//! ## Features
6//!
7//! - `soroban!` macro: Automatically generates client code for interacting with Soroban contracts by:
8//!   - Parsing contract interface from Rust code
9//!   - Creating type-safe client structs with matching methods
10//!   - Handling parameter transformations and RPC communication
11//!
12//! ## Example
13//!
14//! ```rust,ignore
15//! use soroban_rs_macros::soroban;
16//! use soroban_rs::{xdr::ScVal, ClientContractConfigs};
17//!
18//! soroban!(r#"
19//!     pub struct Token;
20//!
21//!     impl Token {
22//!         pub fn transfer(env: &Env, from: Address, to: Address, amount: u128) -> bool {
23//!             // Contract implementation...
24//!         }
25//!     }
26//! "#);
27//!
28//! // Generated client can be used like this:
29//! async fn use_token_client() {
30//!     let client_configs = ClientContractConfigs::new(/* ... */);
31//!     let mut token_client = TokenClient::new(&client_configs);
32//!     
33//!     // Call the contract method with ScVal parameters
34//!     let result = token_client.transfer(from_scval, to_scval, amount_scval).await;
35//! }
36//! ```
37use proc_macro::TokenStream;
38use quote::{format_ident, quote};
39use syn::{parse_macro_input, File, FnArg, Item, ReturnType};
40
41/// A procedural macro for generating Soroban contract client code.
42///
43/// This macro parses a Soroban contract interface and generates a client struct with
44/// corresponding methods that can be used to interact with the deployed contract.
45///
46/// # How It Works
47///
48/// 1. Parses the provided Rust code containing a contract struct and implementation
49/// 2. Extracts the contract's public methods
50/// 3. Generates a client struct with matching methods that:
51///    - Skip the first parameter (env)
52///    - Convert all other parameters to use `ScVal` types
53///    - Return `Result<GetTransactionResponse, SorobanHelperError>`
54///
55/// # Parameters
56///
57/// * `input`: A string literal containing the Rust code of the contract interface
58///
59/// # Generated Code
60///
61/// For a contract named `Token`, the macro generates:
62/// - A `TokenClient` struct with client configuration
63/// - Methods matching the contract's public interface but with modified signatures
64/// - A `new` method to instantiate the client
65///
66/// # Example
67///
68/// ```rust,ignore
69/// soroban!(r#"
70///     pub struct Counter;
71///
72///     impl Counter {
73///         pub fn increment(env: &Env, amount: u32) -> u32 {
74///             // Contract implementation...
75///         }
76///     }
77/// "#);
78/// // or
79/// soroban!("path/to/counter.rs");
80///
81/// // Use the generated client:
82/// let mut counter_client = CounterClient::new(&client_configs);
83/// let result = counter_client.increment(amount_scval).await?;
84/// ```
85#[proc_macro]
86pub fn soroban(input: TokenStream) -> TokenStream {
87    let lit = parse_macro_input!(input as syn::LitStr);
88    let value = lit.value();
89
90    let code = if value.ends_with(".rs") {
91        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
92        let full_path = std::path::Path::new(&manifest_dir).join(&value);
93        std::fs::read_to_string(&full_path)
94            .unwrap_or_else(|_| panic!("Failed to read file at {}", full_path.display()))
95    } else {
96        value
97    };
98    let file_ast: File = syn::parse_str(&code).expect("Failed to parse input");
99
100    let mut struct_name = None;
101    let mut methods = Vec::new();
102
103    // Find struct and impl methods
104    for item in file_ast.items {
105        match item {
106            Item::Struct(item_struct) => struct_name = Some(item_struct.ident),
107            Item::Impl(impl_block) => {
108                for impl_item in impl_block.items {
109                    if let syn::ImplItem::Fn(method) = impl_item {
110                        methods.push(method);
111                    }
112                }
113            }
114            _ => (),
115        }
116    }
117
118    let struct_ident = struct_name.expect("No struct found");
119    let client_struct_ident = format_ident!("{}Client", struct_ident);
120
121    // Transform each method according to your requirement
122    let transformed_methods = methods.iter().filter_map(|method| {
123        let method_name = &method.sig.ident;
124        let method_name_str = method_name.to_string();
125
126        // Skip __constructor or new() methods
127        if method_name_str == "__constructor" || method_name_str == "new" {
128            return None;
129        }
130
131        // Transform inputs: Skip first arg (env), transform rest
132        let transformed_inputs: Vec<_> = method
133            .sig
134            .inputs
135            .iter()
136            .skip(1)
137            .map(|arg| match arg {
138                FnArg::Typed(pat_type) => {
139                    let pat = &pat_type.pat;
140                    quote! { #pat : soroban_rs::xdr::ScVal }
141                }
142                FnArg::Receiver(r) => quote! { #r },
143            })
144            .collect();
145
146        // Also create a list of just the parameter names for the invoke call
147        let param_names = method
148            .sig
149            .inputs
150            .iter()
151            .skip(1)
152            .map(|arg| match arg {
153                FnArg::Typed(pat_type) => {
154                    let pat = &pat_type.pat;
155                    quote! { #pat }
156                }
157                FnArg::Receiver(r) => quote! { #r },
158            })
159            .collect::<Vec<_>>();
160
161        // Transform return type to ScVal
162        let transformed_output = match &method.sig.output {
163            ReturnType::Default => quote! {},
164            ReturnType::Type(_, _) => {
165                quote! { -> Result<soroban_rs::SorobanTransactionResponse, soroban_rs::SorobanHelperError>  }
166            }
167        };
168
169        Some(quote! {
170            pub async fn #method_name(&mut self, #(#transformed_inputs),*) #transformed_output {
171                // internally calls invoke API.
172                self.contract.invoke(stringify!(#method_name), vec![#(#param_names),*]).await
173            }
174        })
175    });
176
177    let expanded = quote! {
178        pub struct #client_struct_ident {
179            client_configs: soroban_rs::ClientContractConfigs,
180            contract: soroban_rs::Contract,
181        }
182
183        impl #client_struct_ident {
184            #(#transformed_methods)*
185            pub fn new(client_configs: &soroban_rs::ClientContractConfigs) -> Self {
186                let contract = soroban_rs::Contract::from_configs(client_configs.clone());
187                Self { client_configs: client_configs.clone(), contract }
188            }
189
190        }
191    };
192
193    expanded.into()
194}