1use proc_macro::TokenStream;
4use quote::{format_ident, quote};
5use syn::{
6 Attribute, Data, DeriveInput, Expr, Fields, Ident, Lit, Result, Token, Type, parse_macro_input,
7 spanned::Spanned,
8};
9
10#[proc_macro_derive(Tlb, attributes(tlb))]
11pub fn derive_tlb(input: TokenStream) -> TokenStream {
12 let input = parse_macro_input!(input as DeriveInput);
13 expand_tlb(input)
14 .unwrap_or_else(syn::Error::into_compile_error)
15 .into()
16}
17
18#[proc_macro_derive(Contract, attributes(contract))]
19pub fn derive_contract(input: TokenStream) -> TokenStream {
20 let input = parse_macro_input!(input as DeriveInput);
21 expand_contract(input)
22 .unwrap_or_else(syn::Error::into_compile_error)
23 .into()
24}
25
26fn expand_contract(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
27 let Data::Struct(data) = &input.data else {
28 return Err(syn::Error::new_spanned(
29 input.ident,
30 "Contract derive supports named structs with exactly one data field",
31 ));
32 };
33 let Fields::Named(fields) = &data.fields else {
34 return Err(syn::Error::new_spanned(
35 &data.fields,
36 "Contract derive supports named structs with exactly one data field",
37 ));
38 };
39 if fields.named.len() != 1 {
40 return Err(syn::Error::new_spanned(
41 &data.fields,
42 "Contract derive requires exactly one named field: data",
43 ));
44 }
45 let field = fields.named.first().expect("field count checked");
46 let Some(field_name) = &field.ident else {
47 return Err(syn::Error::new_spanned(
48 field,
49 "Contract derive requires a named data field",
50 ));
51 };
52 if field_name != "data" {
53 return Err(syn::Error::new_spanned(
54 field_name,
55 "Contract derive requires the field to be named data",
56 ));
57 }
58
59 let config = contract_config(&input.attrs)?;
60 let code_tokens = config.code_tokens()?;
61 let workchain = config.workchain;
62 let name = &input.ident;
63 let data_ty = &field.ty;
64 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
65
66 Ok(quote! {
67 impl #impl_generics ::tonutils::contracts::ContractBlueprint for #name #ty_generics #where_clause {
68 type Data = #data_ty;
69
70 fn data(&self) -> &Self::Data {
71 &self.data
72 }
73
74 fn code_boc(&self) -> ::std::borrow::Cow<'static, [u8]> {
75 #code_tokens
76 }
77
78 fn workchain(&self) -> i8 {
79 #workchain
80 }
81 }
82 })
83}
84
85fn expand_tlb(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
86 match input.data {
87 Data::Struct(data) => expand_struct(&input.ident, &input.attrs, &data.fields),
88 Data::Enum(data) => {
89 expand_enum(&input.ident, &data.variants.into_iter().collect::<Vec<_>>())
90 }
91 Data::Union(_) => Err(syn::Error::new_spanned(
92 input.ident,
93 "TL-B derive does not support unions",
94 )),
95 }
96}
97
98fn expand_struct(
99 name: &Ident,
100 attrs: &[Attribute],
101 fields: &Fields,
102) -> Result<proc_macro2::TokenStream> {
103 let tag = tlb_tag(attrs)?;
104 let field_specs = field_specs(fields)?;
105 let store_fields = field_specs.iter().map(|field| {
106 let access = &field.access;
107 field.store_tokens(quote!(&self.#access))
108 });
109 let load_fields = field_specs.iter().map(|field| {
110 let binding = &field.binding;
111 let load = field.load_tokens();
112 quote!(let #binding = #load;)
113 });
114 let construct = construct_struct(name, fields, &field_specs);
115 let store_tag = tag
116 .as_deref()
117 .map(|tag| quote!(::tonutils::tlb::store_tag(builder, #tag)?;))
118 .unwrap_or_default();
119 let load_tag = tag
120 .as_deref()
121 .map(|tag| quote!(::tonutils::tlb::expect_tag(slice, stringify!(#name), #tag)?;))
122 .unwrap_or_default();
123
124 Ok(quote! {
125 impl ::tonutils::tlb::TlbSerialize for #name {
126 fn store_tlb(&self, builder: &mut ::tonutils::tvm::Builder) -> ::tonutils::tlb::Result<()> {
127 #store_tag
128 #(#store_fields)*
129 Ok(())
130 }
131 }
132
133 impl ::tonutils::tlb::TlbDeserialize for #name {
134 fn load_tlb(slice: &mut ::tonutils::tvm::Slice) -> ::tonutils::tlb::Result<Self> {
135 #load_tag
136 #(#load_fields)*
137 Ok(#construct)
138 }
139 }
140 })
141}
142
143fn expand_enum(name: &Ident, variants: &[syn::Variant]) -> Result<proc_macro2::TokenStream> {
144 let mut store_arms = Vec::new();
145 let mut load_arms = Vec::new();
146 let mut expected_tags = Vec::new();
147 let max_tag_len = variants
148 .iter()
149 .filter_map(|variant| tlb_tag(&variant.attrs).ok().flatten())
150 .map(|tag| tag.len())
151 .max()
152 .unwrap_or(0);
153
154 for variant in variants {
155 let variant_name = &variant.ident;
156 let tag = tlb_tag(&variant.attrs)?.ok_or_else(|| {
157 syn::Error::new_spanned(
158 variant_name,
159 "TL-B enum variants require #[tlb(tag = \"...\")]",
160 )
161 })?;
162 expected_tags.push(tag.clone());
163 let specs = field_specs(&variant.fields)?;
164 let bindings = specs.iter().map(|field| &field.binding).collect::<Vec<_>>();
165 let pattern = match &variant.fields {
166 Fields::Named(_) => quote!(#name::#variant_name { #(#bindings),* }),
167 Fields::Unnamed(_) => quote!(#name::#variant_name(#(#bindings),*)),
168 Fields::Unit => quote!(#name::#variant_name),
169 };
170 let store_fields = specs.iter().map(|field| {
171 let binding = &field.binding;
172 field.store_tokens(quote!(#binding))
173 });
174 store_arms.push(quote! {
175 #pattern => {
176 ::tonutils::tlb::store_tag(builder, #tag)?;
177 #(#store_fields)*
178 }
179 });
180
181 let load_fields = specs.iter().map(|field| {
182 let binding = &field.binding;
183 let load = field.load_tokens();
184 quote!(let #binding = #load;)
185 });
186 let construct = construct_variant(name, variant_name, &variant.fields, &specs);
187 load_arms.push(quote! {
188 #tag => {
189 #(#load_fields)*
190 return Ok(#construct);
191 }
192 });
193 }
194 let expected = expected_tags.join("|");
195
196 Ok(quote! {
197 impl ::tonutils::tlb::TlbSerialize for #name {
198 fn store_tlb(&self, builder: &mut ::tonutils::tvm::Builder) -> ::tonutils::tlb::Result<()> {
199 match self {
200 #(#store_arms),*
201 }
202 Ok(())
203 }
204 }
205
206 impl ::tonutils::tlb::TlbDeserialize for #name {
207 fn load_tlb(slice: &mut ::tonutils::tvm::Slice) -> ::tonutils::tlb::Result<Self> {
208 let mut actual = String::new();
209 while actual.len() < #max_tag_len {
210 let bit = slice.load_bit()?;
211 actual.push(if bit { '1' } else { '0' });
212 match actual.as_str() {
213 #(#load_arms)*
214 _ => {}
215 }
216 }
217 Err(::tonutils::tlb::TlbError::TagMismatch {
218 constructor: stringify!(#name),
219 expected_bits: #expected,
220 actual_bits: actual,
221 })
222 }
223 }
224 })
225}
226
227#[derive(Clone)]
228struct FieldSpec {
229 binding: Ident,
230 access: proc_macro2::TokenStream,
231 ty: Type,
232 bits: Option<usize>,
233 referenced: bool,
234}
235
236impl FieldSpec {
237 fn store_tokens(&self, value: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
238 if self.referenced {
239 return quote!(::tonutils::tlb::store_ref_tlb(builder, #value)?;);
240 }
241 if let Some(bits) = self.bits {
242 return quote!(::tonutils::tlb::StoreBits::<#bits>::store_bits_tlb(#value, builder)?;);
243 }
244 quote!(::tonutils::tlb::TlbSerialize::store_tlb(#value, builder)?;)
245 }
246
247 fn load_tokens(&self) -> proc_macro2::TokenStream {
248 let ty = &self.ty;
249 if self.referenced {
250 return quote!(::tonutils::tlb::load_ref_tlb::<#ty>(slice, stringify!(#ty))?);
251 }
252 if let Some(bits) = self.bits {
253 return quote!(<#ty as ::tonutils::tlb::LoadBits<#bits>>::load_bits_tlb(slice)?);
254 }
255 quote!(<#ty as ::tonutils::tlb::TlbDeserialize>::load_tlb(slice)?)
256 }
257}
258
259fn field_specs(fields: &Fields) -> Result<Vec<FieldSpec>> {
260 fields
261 .iter()
262 .enumerate()
263 .map(|(index, field)| {
264 let binding = field
265 .ident
266 .clone()
267 .unwrap_or_else(|| format_ident!("field_{index}"));
268 let access = field
269 .ident
270 .as_ref()
271 .map(|ident| quote!(#ident))
272 .unwrap_or_else(|| {
273 let index = syn::Index::from(index);
274 quote!(#index)
275 });
276 Ok(FieldSpec {
277 binding,
278 access,
279 ty: field.ty.clone(),
280 bits: field_bits(field)?,
281 referenced: tlb_flag(&field.attrs, "reference")? || tlb_flag(&field.attrs, "ref")?,
282 })
283 })
284 .collect()
285}
286
287fn field_bits(field: &syn::Field) -> Result<Option<usize>> {
288 if is_float_primitive(&field.ty) {
289 return Err(syn::Error::new_spanned(
290 &field.ty,
291 "float primitive TL-B fields are not supported by the runtime",
292 ));
293 }
294 if let Some(bits) = tlb_bits(&field.attrs)? {
295 return Ok(Some(bits));
296 }
297 if let Some(bits) = inferred_unsigned_bits(&field.ty) {
298 return Ok(Some(bits));
299 }
300 if requires_explicit_bits(&field.ty) {
301 return Err(syn::Error::new_spanned(
302 &field.ty,
303 "signed integer and float TL-B fields require #[tlb(bits = N)]",
304 ));
305 }
306 Ok(None)
307}
308
309fn inferred_unsigned_bits(ty: &Type) -> Option<usize> {
310 match primitive_type_ident(ty)?.as_str() {
311 "u8" => Some(8),
312 "u16" => Some(16),
313 "u32" => Some(32),
314 "u64" => Some(64),
315 "u128" => Some(128),
316 _ => None,
317 }
318}
319
320fn requires_explicit_bits(ty: &Type) -> bool {
321 matches!(
322 primitive_type_ident(ty).as_deref(),
323 Some("i8" | "i16" | "i32" | "i64" | "i128" | "isize")
324 )
325}
326
327fn is_float_primitive(ty: &Type) -> bool {
328 matches!(primitive_type_ident(ty).as_deref(), Some("f32" | "f64"))
329}
330
331fn primitive_type_ident(ty: &Type) -> Option<String> {
332 let Type::Path(path) = ty else {
333 return None;
334 };
335 if path.qself.is_some() || path.path.segments.len() != 1 {
336 return None;
337 }
338 Some(path.path.segments.first()?.ident.to_string())
339}
340
341fn construct_struct(
342 name: &Ident,
343 fields: &Fields,
344 specs: &[FieldSpec],
345) -> proc_macro2::TokenStream {
346 let bindings = specs.iter().map(|field| &field.binding);
347 match fields {
348 Fields::Named(_) => quote!(#name { #(#bindings),* }),
349 Fields::Unnamed(_) => quote!(#name(#(#bindings),*)),
350 Fields::Unit => quote!(#name),
351 }
352}
353
354fn construct_variant(
355 name: &Ident,
356 variant: &Ident,
357 fields: &Fields,
358 specs: &[FieldSpec],
359) -> proc_macro2::TokenStream {
360 let bindings = specs.iter().map(|field| &field.binding);
361 match fields {
362 Fields::Named(_) => quote!(#name::#variant { #(#bindings),* }),
363 Fields::Unnamed(_) => quote!(#name::#variant(#(#bindings),*)),
364 Fields::Unit => quote!(#name::#variant),
365 }
366}
367
368fn tlb_tag(attrs: &[Attribute]) -> Result<Option<String>> {
369 let mut tag = None;
370 for attr in attrs.iter().filter(|attr| attr.path().is_ident("tlb")) {
371 attr.parse_nested_meta(|meta| {
372 if meta.path.is_ident("tag") {
373 let value = meta.value()?;
374 let lit: Lit = value.parse()?;
375 match lit {
376 Lit::Str(lit) => {
377 tag = Some(
378 normalize_tag_literal(&lit.value())
379 .map_err(|message| syn::Error::new(lit.span(), message))?,
380 )
381 }
382 _ => return Err(meta.error("tag must be a string literal")),
383 }
384 }
385 Ok(())
386 })?;
387 }
388 Ok(tag)
389}
390
391fn tlb_bits(attrs: &[Attribute]) -> Result<Option<usize>> {
392 let mut bits = None;
393 for attr in attrs.iter().filter(|attr| attr.path().is_ident("tlb")) {
394 attr.parse_nested_meta(|meta| {
395 if meta.path.is_ident("bits") {
396 let value = meta.value()?;
397 let lit: Lit = value.parse()?;
398 if let Lit::Int(lit) = lit {
399 bits = Some(lit.base10_parse()?);
400 return Ok(());
401 }
402 return Err(meta.error("bits must be an integer literal"));
403 }
404 Ok(())
405 })?;
406 }
407 Ok(bits)
408}
409
410fn tlb_flag(attrs: &[Attribute], name: &str) -> Result<bool> {
411 let mut found = false;
412 for attr in attrs.iter().filter(|attr| attr.path().is_ident("tlb")) {
413 attr.parse_nested_meta(|meta| {
414 if meta.path.is_ident(name) {
415 found = true;
416 } else if meta.input.peek(Token![=]) {
417 let _ = meta.value()?.parse::<Lit>()?;
418 }
419 Ok(())
420 })?;
421 }
422 Ok(found)
423}
424
425#[derive(Default)]
426struct ContractConfig {
427 code: Option<ContractCodeSource>,
428 workchain: i8,
429}
430
431enum ContractCodeSource {
432 Expr(Expr),
433 Hex(Vec<u8>),
434 File(syn::LitStr),
435}
436
437impl ContractConfig {
438 fn code_tokens(&self) -> Result<proc_macro2::TokenStream> {
439 let source = self.code.as_ref().ok_or_else(|| {
440 syn::Error::new(
441 proc_macro2::Span::call_site(),
442 "Contract derive requires one code source: code, code_hex, or code_file",
443 )
444 })?;
445 Ok(match source {
446 ContractCodeSource::Expr(expr) => {
447 quote!(::std::borrow::Cow::Borrowed(#expr))
448 }
449 ContractCodeSource::Hex(bytes) => {
450 let bytes = bytes.iter().copied();
451 quote!(::std::borrow::Cow::Borrowed(&[#(#bytes),*]))
452 }
453 ContractCodeSource::File(path) => {
454 quote!(::std::borrow::Cow::Borrowed(include_bytes!(#path)))
455 }
456 })
457 }
458}
459
460fn contract_config(attrs: &[Attribute]) -> Result<ContractConfig> {
461 let mut config = ContractConfig {
462 code: None,
463 workchain: 0,
464 };
465 let mut workchain_seen = false;
466
467 for attr in attrs.iter().filter(|attr| attr.path().is_ident("contract")) {
468 attr.parse_nested_meta(|meta| {
469 if meta.path.is_ident("code") {
470 let value = meta.value()?;
471 let expr: Expr = value.parse()?;
472 set_contract_code(
473 &mut config,
474 ContractCodeSource::Expr(expr),
475 meta.path.span(),
476 )?;
477 } else if meta.path.is_ident("code_hex") {
478 let value = meta.value()?;
479 let lit: Lit = value.parse()?;
480 let Lit::Str(lit) = lit else {
481 return Err(meta.error("code_hex must be a string literal"));
482 };
483 let bytes = parse_hex_bytes(&lit.value())
484 .map_err(|message| syn::Error::new(lit.span(), message))?;
485 set_contract_code(
486 &mut config,
487 ContractCodeSource::Hex(bytes),
488 meta.path.span(),
489 )?;
490 } else if meta.path.is_ident("code_file") {
491 let value = meta.value()?;
492 let lit: Lit = value.parse()?;
493 let Lit::Str(lit) = lit else {
494 return Err(meta.error("code_file must be a string literal"));
495 };
496 set_contract_code(&mut config, ContractCodeSource::File(lit), meta.path.span())?;
497 } else if meta.path.is_ident("workchain") {
498 if workchain_seen {
499 return Err(meta.error("workchain specified more than once"));
500 }
501 workchain_seen = true;
502 let value = meta.value()?;
503 let lit: Lit = value.parse()?;
504 let Lit::Int(lit) = lit else {
505 return Err(meta.error("workchain must be an integer literal"));
506 };
507 config.workchain = lit.base10_parse::<i8>()?;
508 } else {
509 return Err(meta.error("unsupported contract attribute"));
510 }
511 Ok(())
512 })?;
513 }
514 Ok(config)
515}
516
517fn set_contract_code(
518 config: &mut ContractConfig,
519 source: ContractCodeSource,
520 span: proc_macro2::Span,
521) -> Result<()> {
522 if config.code.is_some() {
523 return Err(syn::Error::new(
524 span,
525 "Contract derive accepts only one code source",
526 ));
527 }
528 config.code = Some(source);
529 Ok(())
530}
531
532fn parse_hex_bytes(raw: &str) -> std::result::Result<Vec<u8>, String> {
533 let hex = raw
534 .strip_prefix("0x")
535 .or_else(|| raw.strip_prefix("0X"))
536 .unwrap_or(raw)
537 .chars()
538 .filter(|ch| *ch != '_' && !ch.is_ascii_whitespace())
539 .collect::<String>();
540 if hex.is_empty() {
541 return Err("code_hex must not be empty".to_string());
542 }
543 if hex.len() % 2 != 0 {
544 return Err("code_hex must contain an even number of hex digits".to_string());
545 }
546
547 let mut bytes = Vec::with_capacity(hex.len() / 2);
548 for index in (0..hex.len()).step_by(2) {
549 let byte = u8::from_str_radix(&hex[index..index + 2], 16)
550 .map_err(|_| "code_hex must contain only hexadecimal digits".to_string())?;
551 bytes.push(byte);
552 }
553 Ok(bytes)
554}
555
556fn normalize_tag_literal(raw: &str) -> std::result::Result<String, String> {
557 if let Some(hex) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
558 return hex_tag_to_bits(hex);
559 }
560 if let Some(hex) = raw.strip_prefix('#') {
561 return hex_tag_to_bits(hex);
562 }
563 if let Some(bits) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
564 return binary_tag_to_bits(bits);
565 }
566 binary_tag_to_bits(raw)
567}
568
569fn binary_tag_to_bits(raw: &str) -> std::result::Result<String, String> {
570 let bits = raw.chars().filter(|ch| *ch != '_').collect::<String>();
571 if bits.is_empty() {
572 return Err("tag must not be empty".to_string());
573 }
574 if bits.chars().all(|ch| matches!(ch, '0' | '1')) {
575 Ok(bits)
576 } else {
577 Err("binary tag must contain only 0, 1, or _; use 0x... or #... for hex tags".to_string())
578 }
579}
580
581fn hex_tag_to_bits(raw: &str) -> std::result::Result<String, String> {
582 let hex = raw.chars().filter(|ch| *ch != '_').collect::<String>();
583 if hex.is_empty() {
584 return Err("hex tag must not be empty".to_string());
585 }
586
587 let mut bits = String::with_capacity(hex.len() * 4);
588 for ch in hex.chars() {
589 let value = ch
590 .to_digit(16)
591 .ok_or_else(|| "hex tag must contain only hexadecimal digits or _".to_string())?;
592 bits.push(if value & 0b1000 != 0 { '1' } else { '0' });
593 bits.push(if value & 0b0100 != 0 { '1' } else { '0' });
594 bits.push(if value & 0b0010 != 0 { '1' } else { '0' });
595 bits.push(if value & 0b0001 != 0 { '1' } else { '0' });
596 }
597 Ok(bits)
598}
599
600#[cfg(test)]
601mod tests {
602 use super::{
603 expand_contract, inferred_unsigned_bits, is_float_primitive, normalize_tag_literal,
604 parse_hex_bytes, requires_explicit_bits,
605 };
606 use syn::{DeriveInput, Type, parse_quote};
607
608 #[test]
609 fn tag_literals_accept_binary_and_hex_forms() {
610 assert_eq!(normalize_tag_literal("101").unwrap(), "101");
611 assert_eq!(normalize_tag_literal("0b10_01").unwrap(), "1001");
612 assert_eq!(
613 normalize_tag_literal("0x0f8a_7ea5").unwrap(),
614 "00001111100010100111111010100101"
615 );
616 assert_eq!(normalize_tag_literal("#A5").unwrap(), "10100101");
617 }
618
619 #[test]
620 fn tag_literals_reject_invalid_forms() {
621 assert!(normalize_tag_literal("").is_err());
622 assert!(normalize_tag_literal("102").is_err());
623 assert!(normalize_tag_literal("0x").is_err());
624 assert!(normalize_tag_literal("0xzz").is_err());
625 }
626
627 #[test]
628 fn unsigned_primitive_bits_are_inferred() {
629 let cases = [
630 (parse_quote!(u8), Some(8)),
631 (parse_quote!(u16), Some(16)),
632 (parse_quote!(u32), Some(32)),
633 (parse_quote!(u64), Some(64)),
634 (parse_quote!(u128), Some(128)),
635 (parse_quote!(usize), None),
636 (parse_quote!(Grams), None),
637 ];
638
639 for (ty, expected) in cases {
640 assert_eq!(inferred_unsigned_bits(&ty), expected);
641 }
642 }
643
644 #[test]
645 fn signed_and_float_primitives_require_explicit_bits() {
646 let required: [Type; 5] = [
647 parse_quote!(i8),
648 parse_quote!(i16),
649 parse_quote!(i32),
650 parse_quote!(i64),
651 parse_quote!(i128),
652 ];
653 for ty in required {
654 assert!(requires_explicit_bits(&ty));
655 }
656
657 assert!(!requires_explicit_bits(&parse_quote!(u64)));
658 assert!(!requires_explicit_bits(&parse_quote!(Grams)));
659 }
660
661 #[test]
662 fn float_primitives_are_rejected() {
663 assert!(is_float_primitive(&parse_quote!(f32)));
664 assert!(is_float_primitive(&parse_quote!(f64)));
665 assert!(!is_float_primitive(&parse_quote!(i64)));
666 }
667
668 #[test]
669 fn contract_code_hex_accepts_prefixes_underscores_and_whitespace() {
670 assert_eq!(
671 parse_hex_bytes("0xb5ee_9c72").unwrap(),
672 [0xb5, 0xee, 0x9c, 0x72]
673 );
674 assert_eq!(
675 parse_hex_bytes("b5 ee 9c 72").unwrap(),
676 [0xb5, 0xee, 0x9c, 0x72]
677 );
678 }
679
680 #[test]
681 fn contract_code_hex_rejects_empty_odd_and_invalid_values() {
682 assert!(parse_hex_bytes("").is_err());
683 assert!(parse_hex_bytes("abc").is_err());
684 assert!(parse_hex_bytes("zz").is_err());
685 }
686
687 #[test]
688 fn contract_derive_accepts_supported_code_sources() {
689 let const_input: DeriveInput = parse_quote! {
690 #[contract(code = CODE_BOC)]
691 struct Wallet {
692 data: WalletData,
693 }
694 };
695 assert!(expand_contract(const_input).is_ok());
696
697 let hex_input: DeriveInput = parse_quote! {
698 #[contract(code_hex = "b5ee9c72010101010002000000", workchain = -1)]
699 struct Wallet {
700 data: WalletData,
701 }
702 };
703 assert!(expand_contract(hex_input).is_ok());
704
705 let file_input: DeriveInput = parse_quote! {
706 #[contract(code_file = "wallet.code.boc")]
707 struct Wallet {
708 data: WalletData,
709 }
710 };
711 assert!(expand_contract(file_input).is_ok());
712 }
713
714 #[test]
715 fn contract_derive_rejects_ambiguous_or_unsupported_shapes() {
716 let missing_data: DeriveInput = parse_quote! {
717 #[contract(code = CODE_BOC)]
718 struct Wallet {
719 state: WalletData,
720 }
721 };
722 assert!(expand_contract(missing_data).is_err());
723
724 let extra_field: DeriveInput = parse_quote! {
725 #[contract(code = CODE_BOC)]
726 struct Wallet {
727 data: WalletData,
728 address: u32,
729 }
730 };
731 assert!(expand_contract(extra_field).is_err());
732
733 let unnamed: DeriveInput = parse_quote! {
734 #[contract(code = CODE_BOC)]
735 struct Wallet(WalletData);
736 };
737 assert!(expand_contract(unnamed).is_err());
738
739 let unit: DeriveInput = parse_quote! {
740 #[contract(code = CODE_BOC)]
741 struct Wallet;
742 };
743 assert!(expand_contract(unit).is_err());
744
745 let multiple_code_sources: DeriveInput = parse_quote! {
746 #[contract(code = CODE_BOC, code_hex = "00")]
747 struct Wallet {
748 data: WalletData,
749 }
750 };
751 assert!(expand_contract(multiple_code_sources).is_err());
752 }
753}