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}