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