xcm_procedural/
builder_pattern.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Derive macro for creating XCMs with a builder pattern
18
19use inflector::Inflector;
20use proc_macro2::TokenStream as TokenStream2;
21use quote::{format_ident, quote};
22use syn::{
23	Data, DataEnum, DeriveInput, Error, Expr, ExprLit, Fields, Ident, Lit, Meta, MetaNameValue,
24	Result, Variant,
25};
26
27pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
28	let data_enum = match &input.data {
29		Data::Enum(data_enum) => data_enum,
30		_ => return Err(Error::new_spanned(&input, "Expected the `Instruction` enum")),
31	};
32	let builder_raw_impl = generate_builder_raw_impl(&input.ident, data_enum);
33	let builder_impl = generate_builder_impl(&input.ident, data_enum)?;
34	let builder_unpaid_impl = generate_builder_unpaid_impl(&input.ident, data_enum)?;
35	let output = quote! {
36		/// A trait for types that track state inside the XcmBuilder
37		pub trait XcmBuilderState {}
38
39		/// Access to all the instructions
40		pub enum AnythingGoes {}
41		/// You need to pay for execution
42		pub enum PaymentRequired {}
43		/// The holding register was loaded, now to buy execution
44		pub enum LoadedHolding {}
45		/// Need to explicitly state it won't pay for fees
46		pub enum ExplicitUnpaidRequired {}
47
48		impl XcmBuilderState for AnythingGoes {}
49		impl XcmBuilderState for PaymentRequired {}
50		impl XcmBuilderState for LoadedHolding {}
51		impl XcmBuilderState for ExplicitUnpaidRequired {}
52
53		/// Type used to build XCM programs
54		pub struct XcmBuilder<Call, S: XcmBuilderState> {
55			pub(crate) instructions: Vec<Instruction<Call>>,
56			pub state: core::marker::PhantomData<S>,
57		}
58
59		impl<Call> Xcm<Call> {
60			pub fn builder() -> XcmBuilder<Call, PaymentRequired> {
61				XcmBuilder::<Call, PaymentRequired> {
62					instructions: Vec::new(),
63					state: core::marker::PhantomData,
64				}
65			}
66			pub fn builder_unpaid() -> XcmBuilder<Call, ExplicitUnpaidRequired> {
67				XcmBuilder::<Call, ExplicitUnpaidRequired> {
68					instructions: Vec::new(),
69					state: core::marker::PhantomData,
70				}
71			}
72			pub fn builder_unsafe() -> XcmBuilder<Call, AnythingGoes> {
73				XcmBuilder::<Call, AnythingGoes> {
74					instructions: Vec::new(),
75					state: core::marker::PhantomData,
76				}
77			}
78		}
79		#builder_impl
80		#builder_unpaid_impl
81		#builder_raw_impl
82	};
83	Ok(output)
84}
85
86fn generate_builder_raw_impl(name: &Ident, data_enum: &DataEnum) -> TokenStream2 {
87	let methods = data_enum.variants.iter().map(|variant| {
88		let variant_name = &variant.ident;
89		let method_name_string = &variant_name.to_string().to_snake_case();
90		let method_name = syn::Ident::new(method_name_string, variant_name.span());
91		let docs = get_doc_comments(variant);
92		let method = match &variant.fields {
93			Fields::Unit => {
94				quote! {
95					pub fn #method_name(mut self) -> Self {
96						self.instructions.push(#name::<Call>::#variant_name);
97						self
98					}
99				}
100			},
101			Fields::Unnamed(fields) => {
102				let arg_names: Vec<_> = fields
103					.unnamed
104					.iter()
105					.enumerate()
106					.map(|(index, _)| format_ident!("arg{}", index))
107					.collect();
108				let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect();
109				quote! {
110					pub fn #method_name(mut self, #(#arg_names: impl Into<#arg_types>),*) -> Self {
111						#(let #arg_names = #arg_names.into();)*
112						self.instructions.push(#name::<Call>::#variant_name(#(#arg_names),*));
113						self
114					}
115				}
116			},
117			Fields::Named(fields) => {
118				let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
119				let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
120				quote! {
121					pub fn #method_name(mut self, #(#arg_names: impl Into<#arg_types>),*) -> Self {
122						#(let #arg_names = #arg_names.into();)*
123						self.instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
124						self
125					}
126				}
127			},
128		};
129		quote! {
130			#(#docs)*
131			#method
132		}
133	});
134	let output = quote! {
135		impl<Call> XcmBuilder<Call, AnythingGoes> {
136			#(#methods)*
137
138			pub fn build(self) -> Xcm<Call> {
139				Xcm(self.instructions)
140			}
141		}
142	};
143	output
144}
145
146fn generate_builder_impl(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2> {
147	// We first require an instruction that load the holding register
148	let load_holding_variants = data_enum
149		.variants
150		.iter()
151		.map(|variant| {
152			let maybe_builder_attr = variant.attrs.iter().find(|attr| match attr.meta {
153				Meta::List(ref list) => list.path.is_ident("builder"),
154				_ => false,
155			});
156			let builder_attr = match maybe_builder_attr {
157				Some(builder) => builder.clone(),
158				None => return Ok(None), /* It's not going to be an instruction that loads the
159				                          * holding register */
160			};
161			let Meta::List(ref list) = builder_attr.meta else { unreachable!("We checked before") };
162			let inner_ident: Ident = syn::parse2(list.tokens.clone()).map_err(|_| {
163				Error::new_spanned(&builder_attr, "Expected `builder(loads_holding)`")
164			})?;
165			let ident_to_match: Ident = syn::parse_quote!(loads_holding);
166			if inner_ident == ident_to_match {
167				Ok(Some(variant))
168			} else {
169				Err(Error::new_spanned(&builder_attr, "Expected `builder(loads_holding)`"))
170			}
171		})
172		.collect::<Result<Vec<_>>>()?;
173
174	let load_holding_methods = load_holding_variants
175		.into_iter()
176		.flatten()
177		.map(|variant| {
178			let variant_name = &variant.ident;
179			let method_name_string = &variant_name.to_string().to_snake_case();
180			let method_name = syn::Ident::new(method_name_string, variant_name.span());
181			let docs = get_doc_comments(variant);
182			let method = match &variant.fields {
183				Fields::Unnamed(fields) => {
184					let arg_names: Vec<_> = fields
185						.unnamed
186						.iter()
187						.enumerate()
188						.map(|(index, _)| format_ident!("arg{}", index))
189						.collect();
190					let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect();
191					quote! {
192						#(#docs)*
193						pub fn #method_name(self, #(#arg_names: impl Into<#arg_types>),*) -> XcmBuilder<Call, LoadedHolding> {
194							let mut new_instructions = self.instructions;
195							#(let #arg_names = #arg_names.into();)*
196							new_instructions.push(#name::<Call>::#variant_name(#(#arg_names),*));
197							XcmBuilder {
198								instructions: new_instructions,
199								state: core::marker::PhantomData,
200							}
201						}
202					}
203				},
204				Fields::Named(fields) => {
205					let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
206					let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
207					quote! {
208						#(#docs)*
209						pub fn #method_name(self, #(#arg_names: impl Into<#arg_types>),*) -> XcmBuilder<Call, LoadedHolding> {
210							let mut new_instructions = self.instructions;
211							#(let #arg_names = #arg_names.into();)*
212							new_instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
213							XcmBuilder {
214								instructions: new_instructions,
215								state: core::marker::PhantomData,
216							}
217						}
218					}
219				},
220				_ =>
221					return Err(Error::new_spanned(
222						variant,
223						"Instructions that load the holding register should take operands",
224					)),
225			};
226			Ok(method)
227		})
228		.collect::<std::result::Result<Vec<_>, _>>()?;
229
230	let first_impl = quote! {
231		impl<Call> XcmBuilder<Call, PaymentRequired> {
232			#(#load_holding_methods)*
233		}
234	};
235
236	// Some operations are allowed after the holding register is loaded
237	let allowed_after_load_holding_methods: Vec<TokenStream2> = data_enum
238		.variants
239		.iter()
240		.filter(|variant| variant.ident == "ClearOrigin")
241		.map(|variant| {
242			let variant_name = &variant.ident;
243			let method_name_string = &variant_name.to_string().to_snake_case();
244			let method_name = syn::Ident::new(method_name_string, variant_name.span());
245			let docs = get_doc_comments(variant);
246			let method = match &variant.fields {
247				Fields::Unit => {
248					quote! {
249						#(#docs)*
250						pub fn #method_name(mut self) -> XcmBuilder<Call, LoadedHolding> {
251							self.instructions.push(#name::<Call>::#variant_name);
252							self
253						}
254					}
255				},
256				_ => return Err(Error::new_spanned(variant, "ClearOrigin should have no fields")),
257			};
258			Ok(method)
259		})
260		.collect::<std::result::Result<Vec<_>, _>>()?;
261
262	// Then we require fees to be paid
263	let buy_execution_method = data_enum
264		.variants
265		.iter()
266		.find(|variant| variant.ident == "BuyExecution")
267		.map_or(
268			Err(Error::new_spanned(&data_enum.variants, "No BuyExecution instruction")),
269			|variant| {
270				let variant_name = &variant.ident;
271				let method_name_string = &variant_name.to_string().to_snake_case();
272				let method_name = syn::Ident::new(method_name_string, variant_name.span());
273				let docs = get_doc_comments(variant);
274				let fields = match &variant.fields {
275					Fields::Named(fields) => {
276						let arg_names: Vec<_> =
277							fields.named.iter().map(|field| &field.ident).collect();
278						let arg_types: Vec<_> =
279							fields.named.iter().map(|field| &field.ty).collect();
280						quote! {
281							#(#docs)*
282							pub fn #method_name(self, #(#arg_names: impl Into<#arg_types>),*) -> XcmBuilder<Call, AnythingGoes> {
283								let mut new_instructions = self.instructions;
284								#(let #arg_names = #arg_names.into();)*
285								new_instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
286								XcmBuilder {
287									instructions: new_instructions,
288									state: core::marker::PhantomData,
289								}
290							}
291						}
292					},
293					_ =>
294						return Err(Error::new_spanned(
295							variant,
296							"BuyExecution should have named fields",
297						)),
298				};
299				Ok(fields)
300			},
301		)?;
302
303	let second_impl = quote! {
304		impl<Call> XcmBuilder<Call, LoadedHolding> {
305			#(#allowed_after_load_holding_methods)*
306			#buy_execution_method
307		}
308	};
309
310	let output = quote! {
311		#first_impl
312		#second_impl
313	};
314
315	Ok(output)
316}
317
318fn generate_builder_unpaid_impl(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2> {
319	let unpaid_execution_variant = data_enum
320		.variants
321		.iter()
322		.find(|variant| variant.ident == "UnpaidExecution")
323		.ok_or(Error::new_spanned(&data_enum.variants, "No UnpaidExecution instruction"))?;
324	let unpaid_execution_ident = &unpaid_execution_variant.ident;
325	let unpaid_execution_method_name = Ident::new(
326		&unpaid_execution_ident.to_string().to_snake_case(),
327		unpaid_execution_ident.span(),
328	);
329	let docs = get_doc_comments(unpaid_execution_variant);
330	let fields = match &unpaid_execution_variant.fields {
331		Fields::Named(fields) => fields,
332		_ =>
333			return Err(Error::new_spanned(
334				unpaid_execution_variant,
335				"UnpaidExecution should have named fields",
336			)),
337	};
338	let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
339	let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
340	Ok(quote! {
341		impl<Call> XcmBuilder<Call, ExplicitUnpaidRequired> {
342			#(#docs)*
343			pub fn #unpaid_execution_method_name(self, #(#arg_names: impl Into<#arg_types>),*) -> XcmBuilder<Call, AnythingGoes> {
344				let mut new_instructions = self.instructions;
345				#(let #arg_names = #arg_names.into();)*
346				new_instructions.push(#name::<Call>::#unpaid_execution_ident { #(#arg_names),* });
347				XcmBuilder {
348					instructions: new_instructions,
349					state: core::marker::PhantomData,
350				}
351			}
352		}
353	})
354}
355
356fn get_doc_comments(variant: &Variant) -> Vec<TokenStream2> {
357	variant
358		.attrs
359		.iter()
360		.filter_map(|attr| match &attr.meta {
361			Meta::NameValue(MetaNameValue {
362				value: Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }),
363				..
364			}) if attr.path().is_ident("doc") => Some(literal.value()),
365			_ => None,
366		})
367		.map(|doc| syn::parse_str::<TokenStream2>(&format!("/// {}", doc)).unwrap())
368		.collect()
369}