1extern crate proc_macro;
2
3mod arbitrary;
4mod attribute;
5mod derive_args;
6mod derive_client;
7mod derive_enum;
8mod derive_enum_int;
9mod derive_error_enum_int;
10mod derive_event;
11mod derive_fn;
12mod derive_spec_fn;
13mod derive_struct;
14mod derive_struct_tuple;
15mod doc;
16mod map_type;
17mod path;
18mod symbol;
19mod syn_ext;
20
21use derive_args::{derive_args_impl, derive_args_type};
22use derive_client::{derive_client_impl, derive_client_type};
23use derive_enum::derive_type_enum;
24use derive_enum_int::derive_type_enum_int;
25use derive_error_enum_int::derive_type_error_enum_int;
26use derive_event::derive_event;
27use derive_fn::{derive_contract_function_registration_ctor, derive_pub_fn};
28use derive_spec_fn::derive_fn_spec;
29use derive_struct::derive_type_struct;
30use derive_struct_tuple::derive_type_struct_tuple;
31
32use darling::{ast::NestedMeta, FromMeta};
33use macro_string::MacroString;
34use proc_macro::TokenStream;
35use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
36use quote::{format_ident, quote, ToTokens};
37use sha2::{Digest, Sha256};
38use std::{fmt::Write, fs};
39use syn::{
40 parse_macro_input, parse_str, spanned::Spanned, Data, DeriveInput, Error, Expr, Fields,
41 ItemImpl, ItemStruct, LitStr, Path, Type, Visibility,
42};
43use syn_ext::HasFnsItem;
44
45use soroban_spec_rust::{generate_from_wasm, GenerateFromFileError};
46
47use stellar_xdr::curr as stellar_xdr;
48use stellar_xdr::{Limits, ScMetaEntry, ScMetaV0, StringM, WriteXdr};
49
50pub(crate) const DEFAULT_XDR_RW_LIMITS: Limits = Limits {
51 depth: 500,
52 len: 0x1000000,
53};
54
55#[proc_macro]
56pub fn internal_symbol_short(input: TokenStream) -> TokenStream {
57 let input = parse_macro_input!(input as LitStr);
58 let crate_path: Path = syn::parse_str("crate").unwrap();
59 symbol::short(&crate_path, &input).into()
60}
61
62#[proc_macro]
63pub fn symbol_short(input: TokenStream) -> TokenStream {
64 let input = parse_macro_input!(input as LitStr);
65 let crate_path: Path = syn::parse_str("soroban_sdk").unwrap();
66 symbol::short(&crate_path, &input).into()
67}
68
69pub(crate) fn default_crate_path() -> Path {
70 parse_str("soroban_sdk").unwrap()
71}
72
73#[derive(Debug, FromMeta)]
74struct ContractSpecArgs {
75 name: Type,
76 export: Option<bool>,
77}
78
79#[proc_macro_attribute]
80pub fn contractspecfn(metadata: TokenStream, input: TokenStream) -> TokenStream {
81 let args = match NestedMeta::parse_meta_list(metadata.into()) {
82 Ok(v) => v,
83 Err(e) => {
84 return TokenStream::from(darling::Error::from(e).write_errors());
85 }
86 };
87 let args = match ContractSpecArgs::from_list(&args) {
88 Ok(v) => v,
89 Err(e) => return e.write_errors().into(),
90 };
91 let input2: TokenStream2 = input.clone().into();
92 let item = parse_macro_input!(input as HasFnsItem);
93 let methods: Vec<_> = item.fns();
94 let export = args.export.unwrap_or(true);
95
96 let derived: Result<proc_macro2::TokenStream, proc_macro2::TokenStream> = methods
97 .iter()
98 .map(|m| derive_fn_spec(&args.name, m.ident, m.attrs, m.inputs, m.output, export))
99 .collect();
100
101 match derived {
102 Ok(derived_ok) => quote! {
103 #input2
104 #derived_ok
105 }
106 .into(),
107 Err(derived_err) => quote! {
108 #input2
109 #derived_err
110 }
111 .into(),
112 }
113}
114
115#[derive(Debug, FromMeta)]
116struct ContractArgs {
117 #[darling(default = "default_crate_path")]
118 crate_path: Path,
119}
120
121#[proc_macro_attribute]
122pub fn contract(metadata: TokenStream, input: TokenStream) -> TokenStream {
123 let args = match NestedMeta::parse_meta_list(metadata.into()) {
124 Ok(v) => v,
125 Err(e) => {
126 return TokenStream::from(darling::Error::from(e).write_errors());
127 }
128 };
129 let args = match ContractArgs::from_list(&args) {
130 Ok(v) => v,
131 Err(e) => return e.write_errors().into(),
132 };
133
134 let input2: TokenStream2 = input.clone().into();
135
136 let item = parse_macro_input!(input as ItemStruct);
137
138 let ty = &item.ident;
139 let ty_str = quote!(#ty).to_string();
140
141 let client_ident = format!("{ty_str}Client");
142 let fn_set_registry_ident = format_ident!("__{}_fn_set_registry", ty_str.to_lowercase());
143 let crate_path = &args.crate_path;
144 let client = derive_client_type(&args.crate_path, &ty_str, &client_ident);
145 let args_ident = format!("{ty_str}Args");
146 let contract_args = derive_args_type(&ty_str, &args_ident);
147 let mut output = quote! {
148 #input2
149 #contract_args
150 #client
151 };
152 if cfg!(feature = "testutils") {
153 output.extend(quote! {
154 mod #fn_set_registry_ident {
155 use super::*;
156
157 extern crate std;
158 use std::sync::Mutex;
159 use std::collections::BTreeMap;
160
161 pub type F = #crate_path::testutils::ContractFunctionF;
162
163 static FUNCS: Mutex<BTreeMap<&'static str, &'static F>> = Mutex::new(BTreeMap::new());
164
165 pub fn register(name: &'static str, func: &'static F) {
166 FUNCS.lock().unwrap().insert(name, func);
167 }
168
169 pub fn call(name: &str, env: #crate_path::Env, args: &[#crate_path::Val]) -> Option<#crate_path::Val> {
170 let fopt: Option<&'static F> = FUNCS.lock().unwrap().get(name).map(|f| f.clone());
171 fopt.map(|f| f(env, args))
172 }
173 }
174
175 impl #crate_path::testutils::ContractFunctionRegister for #ty {
176 fn register(name: &'static str, func: &'static #fn_set_registry_ident::F) {
177 #fn_set_registry_ident::register(name, func);
178 }
179 }
180
181 #[doc(hidden)]
182 impl #crate_path::testutils::ContractFunctionSet for #ty {
183 fn call(&self, func: &str, env: #crate_path::Env, args: &[#crate_path::Val]) -> Option<#crate_path::Val> {
184 #fn_set_registry_ident::call(func, env, args)
185 }
186 }
187 });
188 }
189 output.into()
190}
191
192#[derive(Debug, FromMeta)]
193struct ContractImplArgs {
194 #[darling(default = "default_crate_path")]
195 crate_path: Path,
196}
197
198#[proc_macro_attribute]
199pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
200 let args = match NestedMeta::parse_meta_list(metadata.into()) {
201 Ok(v) => v,
202 Err(e) => {
203 return TokenStream::from(darling::Error::from(e).write_errors());
204 }
205 };
206 let args = match ContractImplArgs::from_list(&args) {
207 Ok(v) => v,
208 Err(e) => return e.write_errors().into(),
209 };
210 let crate_path = &args.crate_path;
211 let crate_path_str = quote!(#crate_path).to_string();
212
213 let imp = parse_macro_input!(input as ItemImpl);
214 let trait_ident = imp.trait_.as_ref().and_then(|x| x.1.get_ident());
215 let ty = &imp.self_ty;
216 let ty_str = quote!(#ty).to_string();
217
218 let args_ident = if let Type::Path(path) = &**ty {
221 path.path
222 .segments
223 .last()
224 .map(|name| format!("{}Args", name.ident))
225 } else {
226 None
227 }
228 .unwrap_or_else(|| "Args".to_string());
229
230 let client_ident = if let Type::Path(path) = &**ty {
233 path.path
234 .segments
235 .last()
236 .map(|name| format!("{}Client", name.ident))
237 } else {
238 None
239 }
240 .unwrap_or_else(|| "Client".to_string());
241
242 let pub_methods: Vec<_> = syn_ext::impl_pub_methods(&imp).collect();
243 let derived: Result<proc_macro2::TokenStream, proc_macro2::TokenStream> = pub_methods
244 .iter()
245 .map(|m| {
246 let ident = &m.sig.ident;
247 let call = quote! { <super::#ty>::#ident };
248 derive_pub_fn(
249 crate_path,
250 &ty,
251 &call,
252 ident,
253 &m.attrs,
254 &m.sig.inputs,
255 trait_ident,
256 &client_ident,
257 )
258 })
259 .collect();
260
261 match derived {
262 Ok(derived_ok) => {
263 let mut output = quote! {
264 #[#crate_path::contractargs(name = #args_ident, impl_only = true)]
265 #[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
266 #[#crate_path::contractspecfn(name = #ty_str)]
267 #imp
268 #derived_ok
269 };
270 let cfs = derive_contract_function_registration_ctor(
271 crate_path,
272 ty,
273 trait_ident,
274 pub_methods.into_iter(),
275 );
276 output.extend(quote! { #cfs });
277 output.into()
278 }
279 Err(derived_err) => quote! {
280 #imp
281 #derived_err
282 }
283 .into(),
284 }
285}
286
287#[derive(Debug, FromMeta)]
288struct MetadataArgs {
289 key: String,
290 #[darling(with = darling::util::parse_expr::preserve_str_literal)]
291 val: Expr,
292}
293
294#[proc_macro]
295pub fn contractmeta(metadata: TokenStream) -> TokenStream {
296 let args = match NestedMeta::parse_meta_list(metadata.into()) {
297 Ok(v) => v,
298 Err(e) => {
299 return TokenStream::from(darling::Error::from(e).write_errors());
300 }
301 };
302 let args = match MetadataArgs::from_list(&args) {
303 Ok(v) => v,
304 Err(e) => return e.write_errors().into(),
305 };
306
307 let gen = {
308 let key: StringM = match args.key.clone().try_into() {
309 Ok(k) => k,
310 Err(e) => {
311 return Error::new(Span::call_site(), e.to_string())
312 .into_compile_error()
313 .into()
314 }
315 };
316
317 let val = args.val.to_token_stream().into();
318 let MacroString(val) = parse_macro_input!(val);
319 let val: StringM = match val.try_into() {
320 Ok(k) => k,
321 Err(e) => {
322 return Error::new(Span::call_site(), e.to_string())
323 .into_compile_error()
324 .into()
325 }
326 };
327
328 let meta_v0 = ScMetaV0 { key, val };
329 let meta_entry = ScMetaEntry::ScMetaV0(meta_v0);
330 let metadata_xdr: Vec<u8> = match meta_entry.to_xdr(DEFAULT_XDR_RW_LIMITS) {
331 Ok(v) => v,
332 Err(e) => {
333 return Error::new(Span::call_site(), e.to_string())
334 .into_compile_error()
335 .into()
336 }
337 };
338
339 let metadata_xdr_lit = proc_macro2::Literal::byte_string(metadata_xdr.as_slice());
340 let metadata_xdr_len = metadata_xdr.len();
341
342 let ident = format_ident!(
343 "__CONTRACT_KEY_{}",
344 args.key.as_bytes().iter().fold(String::new(), |mut s, b| {
345 let _ = write!(s, "{b:02x}");
346 s
347 })
348 );
349 quote! {
350 #[doc(hidden)]
351 #[cfg_attr(target_family = "wasm", link_section = "contractmetav0")]
352 static #ident: [u8; #metadata_xdr_len] = *#metadata_xdr_lit;
353 }
354 };
355
356 quote! {
357 #gen
358 }
359 .into()
360}
361
362#[proc_macro_attribute]
363pub fn contractevent(metadata: TokenStream, input: TokenStream) -> TokenStream {
364 derive_event(metadata.into(), input.into()).into()
365}
366
367#[derive(Debug, FromMeta)]
368struct ContractTypeArgs {
369 #[darling(default = "default_crate_path")]
370 crate_path: Path,
371 lib: Option<String>,
372 export: Option<bool>,
373}
374
375#[proc_macro_attribute]
376pub fn contracttype(metadata: TokenStream, input: TokenStream) -> TokenStream {
377 let args = match NestedMeta::parse_meta_list(metadata.into()) {
378 Ok(v) => v,
379 Err(e) => {
380 return TokenStream::from(darling::Error::from(e).write_errors());
381 }
382 };
383 let args = match ContractTypeArgs::from_list(&args) {
384 Ok(v) => v,
385 Err(e) => return e.write_errors().into(),
386 };
387 let input = parse_macro_input!(input as DeriveInput);
388 let vis = &input.vis;
389 let ident = &input.ident;
390 let attrs = &input.attrs;
391 let gen_spec = if let Some(export) = args.export {
394 export
395 } else {
396 matches!(input.vis, Visibility::Public(_))
397 };
398 let derived = match &input.data {
399 Data::Struct(s) => match s.fields {
400 Fields::Named(_) => {
401 derive_type_struct(&args.crate_path, vis, ident, attrs, s, gen_spec, &args.lib)
402 }
403 Fields::Unnamed(_) => derive_type_struct_tuple(
404 &args.crate_path,
405 vis,
406 ident,
407 attrs,
408 s,
409 gen_spec,
410 &args.lib,
411 ),
412 Fields::Unit => Error::new(
413 s.fields.span(),
414 "unit structs are not supported as contract types",
415 )
416 .to_compile_error(),
417 },
418 Data::Enum(e) => {
419 let count_of_variants = e.variants.len();
420 let count_of_int_variants = e
421 .variants
422 .iter()
423 .filter(|v| v.discriminant.is_some())
424 .count();
425 if count_of_int_variants == 0 {
426 derive_type_enum(&args.crate_path, vis, ident, attrs, e, gen_spec, &args.lib)
427 } else if count_of_int_variants == count_of_variants {
428 derive_type_enum_int(&args.crate_path, vis, ident, attrs, e, gen_spec, &args.lib)
429 } else {
430 Error::new(input.span(), "enums are supported as contract types only when all variants have an explicit integer literal, or when all variants are unit or single field")
431 .to_compile_error()
432 }
433 }
434 Data::Union(u) => Error::new(
435 u.union_token.span(),
436 "unions are unsupported as contract types",
437 )
438 .to_compile_error(),
439 };
440 quote! {
441 #input
442 #derived
443 }
444 .into()
445}
446
447#[proc_macro_attribute]
448pub fn contracterror(metadata: TokenStream, input: TokenStream) -> TokenStream {
449 let args = match NestedMeta::parse_meta_list(metadata.into()) {
450 Ok(v) => v,
451 Err(e) => {
452 return TokenStream::from(darling::Error::from(e).write_errors());
453 }
454 };
455 let args = match ContractTypeArgs::from_list(&args) {
456 Ok(v) => v,
457 Err(e) => return e.write_errors().into(),
458 };
459 let input = parse_macro_input!(input as DeriveInput);
460 let ident = &input.ident;
461 let attrs = &input.attrs;
462 let gen_spec = if let Some(export) = args.export {
465 export
466 } else {
467 matches!(input.vis, Visibility::Public(_))
468 };
469 let derived = match &input.data {
470 Data::Enum(e) => {
471 if e.variants.iter().all(|v| v.discriminant.is_some()) {
472 derive_type_error_enum_int(&args.crate_path, ident, attrs, e, gen_spec, &args.lib)
473 } else {
474 Error::new(input.span(), "enums are supported as contract errors only when all variants have an explicit integer literal")
475 .to_compile_error()
476 }
477 }
478 Data::Struct(s) => Error::new(
479 s.struct_token.span(),
480 "structs are unsupported as contract errors",
481 )
482 .to_compile_error(),
483 Data::Union(u) => Error::new(
484 u.union_token.span(),
485 "unions are unsupported as contract errors",
486 )
487 .to_compile_error(),
488 };
489 quote! {
490 #input
491 #derived
492 }
493 .into()
494}
495
496#[derive(Debug, FromMeta)]
497struct ContractFileArgs {
498 file: String,
499 sha256: darling::util::SpannedValue<String>,
500}
501
502#[proc_macro]
503pub fn contractfile(metadata: TokenStream) -> TokenStream {
504 let args = match NestedMeta::parse_meta_list(metadata.into()) {
505 Ok(v) => v,
506 Err(e) => {
507 return TokenStream::from(darling::Error::from(e).write_errors());
508 }
509 };
510 let args = match ContractFileArgs::from_list(&args) {
511 Ok(v) => v,
512 Err(e) => return e.write_errors().into(),
513 };
514
515 let file_abs = path::abs_from_rel_to_manifest(&args.file);
517 let wasm = match fs::read(file_abs) {
518 Ok(wasm) => wasm,
519 Err(e) => {
520 return Error::new(Span::call_site(), e.to_string())
521 .into_compile_error()
522 .into()
523 }
524 };
525
526 let sha256 = Sha256::digest(&wasm);
528 let sha256 = format!("{:x}", sha256);
529 if *args.sha256 != sha256 {
530 return Error::new(
531 args.sha256.span(),
532 format!("sha256 does not match, expected: {}", sha256),
533 )
534 .into_compile_error()
535 .into();
536 }
537
538 let contents_lit = Literal::byte_string(&wasm);
540 quote! { #contents_lit }.into()
541}
542
543#[derive(Debug, FromMeta)]
544struct ContractArgsArgs {
545 name: String,
546 #[darling(default)]
547 impl_only: bool,
548}
549
550#[proc_macro_attribute]
551pub fn contractargs(metadata: TokenStream, input: TokenStream) -> TokenStream {
552 let args = match NestedMeta::parse_meta_list(metadata.into()) {
553 Ok(v) => v,
554 Err(e) => {
555 return TokenStream::from(darling::Error::from(e).write_errors());
556 }
557 };
558 let args = match ContractArgsArgs::from_list(&args) {
559 Ok(v) => v,
560 Err(e) => return e.write_errors().into(),
561 };
562 let input2: TokenStream2 = input.clone().into();
563 let item = parse_macro_input!(input as HasFnsItem);
564 let methods: Vec<_> = item.fns();
565 let args_type = (!args.impl_only).then(|| derive_args_type(&item.name(), &args.name));
566 let args_impl = derive_args_impl(&args.name, &methods);
567 quote! {
568 #input2
569 #args_type
570 #args_impl
571 }
572 .into()
573}
574
575#[derive(Debug, FromMeta)]
576struct ContractClientArgs {
577 #[darling(default = "default_crate_path")]
578 crate_path: Path,
579 name: String,
580 #[darling(default)]
581 impl_only: bool,
582}
583
584#[proc_macro_attribute]
585pub fn contractclient(metadata: TokenStream, input: TokenStream) -> TokenStream {
586 let args = match NestedMeta::parse_meta_list(metadata.into()) {
587 Ok(v) => v,
588 Err(e) => {
589 return TokenStream::from(darling::Error::from(e).write_errors());
590 }
591 };
592 let args = match ContractClientArgs::from_list(&args) {
593 Ok(v) => v,
594 Err(e) => return e.write_errors().into(),
595 };
596 let input2: TokenStream2 = input.clone().into();
597 let item = parse_macro_input!(input as HasFnsItem);
598 let methods: Vec<_> = item.fns();
599 let client_type =
600 (!args.impl_only).then(|| derive_client_type(&args.crate_path, &item.name(), &args.name));
601 let client_impl = derive_client_impl(&args.crate_path, &args.name, &methods);
602 quote! {
603 #input2
604 #client_type
605 #client_impl
606 }
607 .into()
608}
609
610#[derive(Debug, FromMeta)]
611struct ContractImportArgs {
612 file: String,
613 #[darling(default)]
614 sha256: darling::util::SpannedValue<Option<String>>,
615}
616#[proc_macro]
617pub fn contractimport(metadata: TokenStream) -> TokenStream {
618 let args = match NestedMeta::parse_meta_list(metadata.into()) {
619 Ok(v) => v,
620 Err(e) => {
621 return TokenStream::from(darling::Error::from(e).write_errors());
622 }
623 };
624 let args = match ContractImportArgs::from_list(&args) {
625 Ok(v) => v,
626 Err(e) => return e.write_errors().into(),
627 };
628
629 let file_abs = path::abs_from_rel_to_manifest(&args.file);
631 let wasm = match fs::read(file_abs) {
632 Ok(wasm) => wasm,
633 Err(e) => {
634 return Error::new(Span::call_site(), e.to_string())
635 .into_compile_error()
636 .into()
637 }
638 };
639
640 match generate_from_wasm(&wasm, &args.file, args.sha256.as_deref()) {
642 Ok(code) => quote! { #code },
643 Err(e @ GenerateFromFileError::VerifySha256 { .. }) => {
644 Error::new(args.sha256.span(), e.to_string()).into_compile_error()
645 }
646 Err(e) => Error::new(Span::call_site(), e.to_string()).into_compile_error(),
647 }
648 .into()
649}