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