Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Different XCM builders, default one requires fee payment #2253

Merged
merged 11 commits into from
Nov 21, 2023
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions polkadot/xcm/procedural/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ Inflector = "0.11.4"

[dev-dependencies]
trybuild = { version = "1.0.74", features = ["diff"] }
xcm = { package = "staging-xcm", path = ".." }
287 changes: 255 additions & 32 deletions polkadot/xcm/procedural/src/builder_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,56 +17,83 @@
//! Derive macro for creating XCMs with a builder pattern

use inflector::Inflector;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
parse_macro_input, Data, DeriveInput, Error, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue,
Data, DataEnum, DeriveInput, Error, Expr, ExprLit, Fields, Ident, Lit, Meta, MetaNameValue,
Result, Variant,
};

pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let builder_impl = match &input.data {
Data::Enum(data_enum) => generate_methods_for_enum(input.ident, data_enum),
_ =>
return Error::new_spanned(&input, "Expected the `Instruction` enum")
.to_compile_error()
.into(),
pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
let data_enum = match &input.data {
Data::Enum(data_enum) => data_enum,
_ => return Err(Error::new_spanned(&input, "Expected the `Instruction` enum")),
};
let builder_raw_impl = generate_builder_raw_impl(&input.ident, data_enum);
let builder_impl = generate_builder_impl(&input.ident, data_enum)?;
let builder_unpaid_impl = generate_builder_unpaid_impl(&input.ident, data_enum)?;
let output = quote! {
pub struct XcmBuilder<Call>(Vec<Instruction<Call>>);
/// A trait for types that track state inside the XcmBuilder
pub trait XcmBuilderState {}

/// Access to all the instructions
pub enum AnythingGoes {}
/// You need to pay for execution
pub enum PaymentRequired {}
/// The holding register was loaded, now to buy execution
pub enum LoadedHolding {}
/// Need to explicitly state it won't pay for fees
pub enum ExplicitUnpaidRequired {}

impl XcmBuilderState for AnythingGoes {}
impl XcmBuilderState for PaymentRequired {}
impl XcmBuilderState for LoadedHolding {}
impl XcmBuilderState for ExplicitUnpaidRequired {}

/// Type used to build XCM programs
pub struct XcmBuilder<Call, S: XcmBuilderState> {
pub(crate) instructions: Vec<Instruction<Call>>,
pub state: core::marker::PhantomData<S>,
}

impl<Call> Xcm<Call> {
pub fn builder() -> XcmBuilder<Call> {
XcmBuilder::<Call>(Vec::new())
pub fn builder() -> XcmBuilder<Call, PaymentRequired> {
XcmBuilder::<Call, PaymentRequired> {
instructions: Vec::new(),
state: core::marker::PhantomData,
}
}
pub fn builder_unpaid() -> XcmBuilder<Call, ExplicitUnpaidRequired> {
XcmBuilder::<Call, ExplicitUnpaidRequired> {
instructions: Vec::new(),
state: core::marker::PhantomData,
}
}
pub fn builder_unsafe() -> XcmBuilder<Call, AnythingGoes> {
XcmBuilder::<Call, AnythingGoes> {
instructions: Vec::new(),
state: core::marker::PhantomData,
}
}
}
#builder_impl
#builder_unpaid_impl
#builder_raw_impl
};
output.into()
Ok(output)
}

fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> TokenStream2 {
fn generate_builder_raw_impl(name: &Ident, data_enum: &DataEnum) -> TokenStream2 {
let methods = data_enum.variants.iter().map(|variant| {
let variant_name = &variant.ident;
let method_name_string = &variant_name.to_string().to_snake_case();
let method_name = syn::Ident::new(&method_name_string, variant_name.span());
let docs: Vec<_> = variant
.attrs
.iter()
.filter_map(|attr| match &attr.meta {
Meta::NameValue(MetaNameValue {
value: Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }),
..
}) if attr.path().is_ident("doc") => Some(literal.value()),
_ => None,
})
.map(|doc| syn::parse_str::<TokenStream2>(&format!("/// {}", doc)).unwrap())
.collect();
let docs = get_doc_comments(&variant);
let method = match &variant.fields {
Fields::Unit => {
quote! {
pub fn #method_name(mut self) -> Self {
self.0.push(#name::<Call>::#variant_name);
self.instructions.push(#name::<Call>::#variant_name);
self
}
}
Expand All @@ -81,7 +108,7 @@ fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> Tok
let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect();
quote! {
pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self {
self.0.push(#name::<Call>::#variant_name(#(#arg_names),*));
self.instructions.push(#name::<Call>::#variant_name(#(#arg_names),*));
self
}
}
Expand All @@ -91,7 +118,7 @@ fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> Tok
let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
quote! {
pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self {
self.0.push(#name::<Call>::#variant_name { #(#arg_names),* });
self.instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
self
}
}
Expand All @@ -103,13 +130,209 @@ fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> Tok
}
});
let output = quote! {
impl<Call> XcmBuilder<Call> {
impl<Call> XcmBuilder<Call, AnythingGoes> {
#(#methods)*

pub fn build(self) -> Xcm<Call> {
Xcm(self.0)
Xcm(self.instructions)
}
}
};
output
}

fn generate_builder_impl(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2> {
// We first require an instruction that load the holding register
let load_holding_variants = data_enum
.variants
.iter()
.map(|variant| {
let maybe_builder_attr = variant.attrs.iter().find(|attr| match attr.meta {
Meta::List(ref list) => {
return list.path.is_ident("builder");
},
_ => false,
});
let builder_attr = match maybe_builder_attr {
Some(builder) => builder.clone(),
None => return Ok(None), /* It's not going to be an instruction that loads the
* holding register */
};
let Meta::List(ref list) = builder_attr.meta else { unreachable!("We checked before") };
let inner_ident: Ident = syn::parse2(list.tokens.clone().into()).map_err(|_| {
Error::new_spanned(&builder_attr, "Expected `builder(loads_holding)`")
})?;
let ident_to_match: Ident = syn::parse_quote!(loads_holding);
if inner_ident == ident_to_match {
Ok(Some(variant))
} else {
Err(Error::new_spanned(&builder_attr, "Expected `builder(loads_holding)`"))
}
})
.collect::<Result<Vec<_>>>()?;

let load_holding_methods = load_holding_variants
.into_iter()
.flatten()
.map(|variant| {
let variant_name = &variant.ident;
let method_name_string = &variant_name.to_string().to_snake_case();
let method_name = syn::Ident::new(&method_name_string, variant_name.span());
let docs = get_doc_comments(&variant);
let method = match &variant.fields {
Fields::Unnamed(fields) => {
let arg_names: Vec<_> = fields
.unnamed
.iter()
.enumerate()
.map(|(index, _)| format_ident!("arg{}", index))
.collect();
let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect();
quote! {
#(#docs)*
pub fn #method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, LoadedHolding> {
let mut new_instructions = self.instructions;
new_instructions.push(#name::<Call>::#variant_name(#(#arg_names),*));
XcmBuilder {
instructions: new_instructions,
state: core::marker::PhantomData,
}
}
}
},
Fields::Named(fields) => {
let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
quote! {
#(#docs)*
pub fn #method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, LoadedHolding> {
let mut new_instructions = self.instructions;
new_instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
XcmBuilder {
instructions: new_instructions,
state: core::marker::PhantomData,
}
}
}
},
_ =>
return Err(Error::new_spanned(
&variant,
"Instructions that load the holding register should take operands",
)),
};
Ok(method)
})
.collect::<std::result::Result<Vec<_>, _>>()?;

let first_impl = quote! {
impl<Call> XcmBuilder<Call, PaymentRequired> {
#(#load_holding_methods)*
}
};

// Then we require fees to be paid
let buy_execution_method = data_enum
.variants
.iter()
.find(|variant| variant.ident.to_string() == "BuyExecution")
.map_or(
Err(Error::new_spanned(&data_enum.variants, "No BuyExecution instruction")),
|variant| {
let variant_name = &variant.ident;
let method_name_string = &variant_name.to_string().to_snake_case();
let method_name = syn::Ident::new(&method_name_string, variant_name.span());
let docs = get_doc_comments(&variant);
let fields = match &variant.fields {
Fields::Named(fields) => {
let arg_names: Vec<_> =
fields.named.iter().map(|field| &field.ident).collect();
let arg_types: Vec<_> =
fields.named.iter().map(|field| &field.ty).collect();
quote! {
#(#docs)*
pub fn #method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, AnythingGoes> {
let mut new_instructions = self.instructions;
new_instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
XcmBuilder {
instructions: new_instructions,
state: core::marker::PhantomData,
}
}
}
},
_ =>
return Err(Error::new_spanned(
&variant,
"BuyExecution should have named fields",
)),
};
Ok(fields)
},
)?;

let second_impl = quote! {
impl<Call> XcmBuilder<Call, LoadedHolding> {
#buy_execution_method
}
};

let output = quote! {
#first_impl
#second_impl
};

Ok(output)
}

fn generate_builder_unpaid_impl(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2> {
let unpaid_execution_variant = data_enum
.variants
.iter()
.find(|variant| variant.ident.to_string() == "UnpaidExecution")
.ok_or(Error::new_spanned(&data_enum.variants, "No UnpaidExecution instruction"))?;
let unpaid_execution_ident = &unpaid_execution_variant.ident;
let unpaid_execution_method_name = Ident::new(
&unpaid_execution_ident.to_string().to_snake_case(),
unpaid_execution_ident.span(),
);
let docs = get_doc_comments(&unpaid_execution_variant);
let fields = match &unpaid_execution_variant.fields {
Fields::Named(fields) => fields,
_ =>
return Err(Error::new_spanned(
&unpaid_execution_variant,
"UnpaidExecution should have named fields",
)),
};
let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
Ok(quote! {
impl<Call> XcmBuilder<Call, ExplicitUnpaidRequired> {
#(#docs)*
pub fn #unpaid_execution_method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, AnythingGoes> {
let mut new_instructions = self.instructions;
new_instructions.push(#name::<Call>::#unpaid_execution_ident { #(#arg_names),* });
XcmBuilder {
instructions: new_instructions,
state: core::marker::PhantomData,
}
}
}
})
}

fn get_doc_comments(variant: &Variant) -> Vec<TokenStream2> {
variant
.attrs
.iter()
.filter_map(|attr| match &attr.meta {
Meta::NameValue(MetaNameValue {
value: Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }),
..
}) if attr.path().is_ident("doc") => Some(literal.value()),
_ => None,
})
.map(|doc| syn::parse_str::<TokenStream2>(&format!("/// {}", doc)).unwrap())
.collect()
}
6 changes: 5 additions & 1 deletion polkadot/xcm/procedural/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! Procedural macros used in XCM.

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

mod builder_pattern;
mod v2;
Expand Down Expand Up @@ -56,7 +57,10 @@ pub fn impl_conversion_functions_for_junctions_v3(input: TokenStream) -> TokenSt
/// .buy_execution(fees, weight_limit)
/// .deposit_asset(assets, beneficiary)
/// .build();
#[proc_macro_derive(Builder)]
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
builder_pattern::derive(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
Loading