diff --git a/Cargo.lock b/Cargo.lock
index 764d4ed01aa5..70b0d43120f1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21176,6 +21176,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
+ "trybuild",
]
[[package]]
diff --git a/polkadot/xcm/procedural/Cargo.toml b/polkadot/xcm/procedural/Cargo.toml
index 56df0d94f586..33c2a94be0e4 100644
--- a/polkadot/xcm/procedural/Cargo.toml
+++ b/polkadot/xcm/procedural/Cargo.toml
@@ -1,9 +1,11 @@
[package]
name = "xcm-procedural"
+description = "Procedural macros for XCM"
authors.workspace = true
edition.workspace = true
license.workspace = true
version = "1.0.0"
+publish = true
[lib]
proc-macro = true
@@ -13,3 +15,6 @@ proc-macro2 = "1.0.56"
quote = "1.0.28"
syn = "2.0.38"
Inflector = "0.11.4"
+
+[dev-dependencies]
+trybuild = { version = "1.0.74", features = ["diff"] }
diff --git a/polkadot/xcm/procedural/src/builder_pattern.rs b/polkadot/xcm/procedural/src/builder_pattern.rs
new file mode 100644
index 000000000000..ebad54e972b6
--- /dev/null
+++ b/polkadot/xcm/procedural/src/builder_pattern.rs
@@ -0,0 +1,115 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+//! 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,
+};
+
+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(),
+ };
+ let output = quote! {
+ pub struct XcmBuilder(Vec>);
+ impl Xcm {
+ pub fn builder() -> XcmBuilder {
+ XcmBuilder::(Vec::new())
+ }
+ }
+ #builder_impl
+ };
+ output.into()
+}
+
+fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::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::(&format!("/// {}", doc)).unwrap())
+ .collect();
+ let method = match &variant.fields {
+ Fields::Unit => {
+ quote! {
+ pub fn #method_name(mut self) -> Self {
+ self.0.push(#name::::#variant_name);
+ self
+ }
+ }
+ },
+ 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! {
+ pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self {
+ self.0.push(#name::::#variant_name(#(#arg_names),*));
+ self
+ }
+ }
+ },
+ 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! {
+ pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self {
+ self.0.push(#name::::#variant_name { #(#arg_names),* });
+ self
+ }
+ }
+ },
+ };
+ quote! {
+ #(#docs)*
+ #method
+ }
+ });
+ let output = quote! {
+ impl XcmBuilder {
+ #(#methods)*
+
+ pub fn build(self) -> Xcm {
+ Xcm(self.0)
+ }
+ }
+ };
+ output
+}
diff --git a/polkadot/xcm/procedural/src/lib.rs b/polkadot/xcm/procedural/src/lib.rs
index 2ebccadf50be..83cc6cdf98ff 100644
--- a/polkadot/xcm/procedural/src/lib.rs
+++ b/polkadot/xcm/procedural/src/lib.rs
@@ -18,6 +18,7 @@
use proc_macro::TokenStream;
+mod builder_pattern;
mod v2;
mod v3;
mod weight_info;
@@ -47,3 +48,15 @@ pub fn impl_conversion_functions_for_junctions_v3(input: TokenStream) -> TokenSt
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
+
+/// This is called on the `Instruction` enum, not on the `Xcm` struct,
+/// and allows for the following syntax for building XCMs:
+/// let message = Xcm::builder()
+/// .withdraw_asset(assets)
+/// .buy_execution(fees, weight_limit)
+/// .deposit_asset(assets, beneficiary)
+/// .build();
+#[proc_macro_derive(Builder)]
+pub fn derive_builder(input: TokenStream) -> TokenStream {
+ builder_pattern::derive(input)
+}
diff --git a/polkadot/xcm/procedural/tests/ui.rs b/polkadot/xcm/procedural/tests/ui.rs
new file mode 100644
index 000000000000..a6ec35d0862a
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui.rs
@@ -0,0 +1,32 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+//! UI tests for XCM procedural macros
+
+#[cfg(not(feature = "disable-ui-tests"))]
+#[test]
+fn ui() {
+ // Only run the ui tests when `RUN_UI_TESTS` is set.
+ if std::env::var("RUN_UI_TESTS").is_err() {
+ return;
+ }
+
+ // As trybuild is using `cargo check`, we don't need the real WASM binaries.
+ std::env::set_var("SKIP_WASM_BUILD", "1");
+
+ let t = trybuild::TestCases::new();
+ t.compile_fail("tests/ui/*.rs");
+}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern.rs
new file mode 100644
index 000000000000..e4bcda572ad7
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern.rs
@@ -0,0 +1,25 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+//! Test error when attaching the derive builder macro to something
+//! other than the XCM `Instruction` enum.
+
+use xcm_procedural::Builder;
+
+#[derive(Builder)]
+struct SomeStruct;
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr
new file mode 100644
index 000000000000..439b40f31cae
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr
@@ -0,0 +1,5 @@
+error: Expected the `Instruction` enum
+ --> tests/ui/builder_pattern.rs:23:1
+ |
+23 | struct SomeStruct;
+ | ^^^^^^^^^^^^^^^^^^
diff --git a/polkadot/xcm/src/v3/mod.rs b/polkadot/xcm/src/v3/mod.rs
index aa5bfe169ac1..7ec3f6f40af2 100644
--- a/polkadot/xcm/src/v3/mod.rs
+++ b/polkadot/xcm/src/v3/mod.rs
@@ -404,7 +404,14 @@ impl XcmContext {
///
/// This is the inner XCM format and is version-sensitive. Messages are typically passed using the
/// outer XCM format, known as `VersionedXcm`.
-#[derive(Derivative, Encode, Decode, TypeInfo, xcm_procedural::XcmWeightInfoTrait)]
+#[derive(
+ Derivative,
+ Encode,
+ Decode,
+ TypeInfo,
+ xcm_procedural::XcmWeightInfoTrait,
+ xcm_procedural::Builder,
+)]
#[derivative(Clone(bound = ""), Eq(bound = ""), PartialEq(bound = ""), Debug(bound = ""))]
#[codec(encode_bound())]
#[codec(decode_bound())]
diff --git a/polkadot/xcm/xcm-simulator/example/src/lib.rs b/polkadot/xcm/xcm-simulator/example/src/lib.rs
index 85b8ad1c5cb7..03e7c19a9148 100644
--- a/polkadot/xcm/xcm-simulator/example/src/lib.rs
+++ b/polkadot/xcm/xcm-simulator/example/src/lib.rs
@@ -649,4 +649,23 @@ mod tests {
);
});
}
+
+ #[test]
+ fn builder_pattern_works() {
+ let asset: MultiAsset = (Here, 100u128).into();
+ let beneficiary: MultiLocation = AccountId32 { id: [0u8; 32], network: None }.into();
+ let message: Xcm<()> = Xcm::builder()
+ .withdraw_asset(asset.clone().into())
+ .buy_execution(asset.clone(), Unlimited)
+ .deposit_asset(asset.clone().into(), beneficiary)
+ .build();
+ assert_eq!(
+ message,
+ Xcm(vec![
+ WithdrawAsset(asset.clone().into()),
+ BuyExecution { fees: asset.clone(), weight_limit: Unlimited },
+ DepositAsset { assets: asset.into(), beneficiary },
+ ])
+ );
+ }
}
diff --git a/prdoc/pr_2107.prdoc b/prdoc/pr_2107.prdoc
new file mode 100644
index 000000000000..0e33680555ac
--- /dev/null
+++ b/prdoc/pr_2107.prdoc
@@ -0,0 +1,24 @@
+# Schema: Parity PR Documentation Schema (prdoc)
+# See doc at https://github.com/paritytech/prdoc
+
+title: Add a builder pattern to create XCM programs
+
+doc:
+ - audience: Core Dev
+ description: |
+ XCMs can now be built using a builder pattern like so:
+ Xcm::builder()
+ .withdraw_asset(assets)
+ .buy_execution(fees, weight_limit)
+ .deposit_asset(assets, beneficiary)
+ .build();
+
+migrations:
+ db: []
+
+ runtime: []
+
+crates:
+ - name: xcm
+
+host_functions: []