diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a02186874..e3291e673 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -263,4 +263,4 @@ jobs: - name: Run integration tests env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: cargo test --no-default-features --features parachain --test parachain + run: cargo test --no-default-features --features parachain,experimental --test parachain diff --git a/Cargo.lock b/Cargo.lock index 76625afd9..7f18a8b43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1814,6 +1814,16 @@ dependencies = [ "toml 0.8.20", ] +[[package]] +name = "cargo_toml" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" +dependencies = [ + "serde", + "toml 0.8.20", +] + [[package]] name = "cc" version = "1.2.17" @@ -4276,6 +4286,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "fs_rollback" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f2ac32da3859259a7f1b24c86b1a40ca6671af078df6df254f25e9438b7e9" +dependencies = [ + "rustilities", + "same-file", + "tempfile", + "thiserror 2.0.12", +] + [[package]] name = "funty" version = "2.0.0" @@ -8857,6 +8879,7 @@ dependencies = [ "dirs", "duct", "env_logger 0.11.7", + "fs_rollback", "git2", "mockito", "open", @@ -8865,16 +8888,21 @@ dependencies = [ "pop-contracts", "pop-parachains", "pop-telemetry", + "proc-macro2", "regex", "reqwest", + "rust_writer", + "rustilities", "serde", "serde_json", + "similar", "sp-core 32.0.0", "sp-weights", "strum 0.26.3", "strum_macros 0.26.4", "subxt", "subxt-signer", + "syn 2.0.100", "tempfile", "tokio", "toml 0.5.11", @@ -8889,7 +8917,7 @@ version = "0.8.1" dependencies = [ "anyhow", "bytes", - "cargo_toml", + "cargo_toml 0.20.5", "contract-build 5.0.3", "contract-extrinsics 5.0.3", "duct", @@ -8903,6 +8931,7 @@ dependencies = [ "scale-info", "serde", "serde_json", + "similar", "sp-core 32.0.0", "strum 0.26.3", "strum_macros 0.26.4", @@ -9998,6 +10027,33 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rust_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf4bb56f2de882403080044bd15dbafbdceb5437580638da25d257de8fbd86e" +dependencies = [ + "prettyplease", + "proc-macro2", + "regex", + "rust_writer_proc", + "rustilities", + "syn 2.0.100", + "thiserror 2.0.12", +] + +[[package]] +name = "rust_writer_proc" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54abd3b6b8f7e68143a45c862405827fc453433e46c5d892d59421607bea8074" +dependencies = [ + "proc-macro2", + "quote", + "rustilities", + "syn 2.0.100", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -10049,6 +10105,19 @@ dependencies = [ "nom", ] +[[package]] +name = "rustilities" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c4a622609b034008b203bd62d1500f1e03889758aed046ddd68337a98c8cbf" +dependencies = [ + "cargo_toml 0.21.0", + "proc-macro2", + "syn 2.0.100", + "thiserror 2.0.12", + "toml_edit", +] + [[package]] name = "rustix" version = "0.36.17" diff --git a/Cargo.toml b/Cargo.toml index 89e198a8d..ab40ecbc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,16 @@ dirs = { version = "5.0", default-features = false } duct = { version = "0.13", default-features = false } env_logger = { version = "0.11.7", default-features = false } flate2 = "1.0.30" +fs_rollback = "3.0.1" git2 = { version = "0.18", default-features = true, features = ["vendored-openssl"] } glob = { version = "0.3.1", default-features = false } log = { version = "0.4.20", default-features = false } mockito = { version = "1.4.0", default-features = false } +proc-macro2 = "1.0.86" +rustilities = "2.2.1" +rust_writer = "1.0.4" +similar = "2.7.0" +syn = "2.0.100" tar = { version = "0.4.40", default-features = false } tempfile = { version = "3.10", default-features = false } thiserror = { version = "1.0.58", default-features = false } diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index dcc9c27e3..892151b07 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -20,10 +20,15 @@ console.workspace = true dirs.workspace = true duct.workspace = true env_logger.workspace = true +fs_rollback = { workspace = true, optional = true } os_info.workspace = true +proc-macro2 = { workspace = true, optional = true} reqwest.workspace = true +rustilities = { workspace = true, features = ["fmt", "manifest"], optional = true } +rust_writer = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +syn = { workspace = true, features = ["parsing"], optional = true } strum.workspace = true strum_macros.workspace = true tempfile.workspace = true @@ -57,14 +62,16 @@ tower-http = { workspace = true, features = ["fs", "cors"], optional = true } assert_cmd.workspace = true contract-extrinsics.workspace = true mockito.workspace = true +rustilities = { workspace = true, features = ["fmt", "manifest", "parsing"]} subxt.workspace = true subxt-signer.workspace = true +similar.workspace = true [features] default = ["parachain", "telemetry", "wasm-contracts"] contract = ["wasm-contracts"] contracts = ["polkavm-contracts"] -experimental = ["hashing"] +experimental = ["hashing", "syn", "rust_writer", "rustilities", "proc-macro2", "fs_rollback"] hashing = ["dep:sp-core"] parachain = ["dep:pop-parachains", "dep:git2", "dep:regex", "dep:sp-core", "dep:tracing-subscriber", "wallet-integration"] v6 = [] @@ -72,4 +79,4 @@ polkavm-contracts = ["pop-contracts/v6", "dep:pop-contracts", "dep:sp-core", "de telemetry = ["dep:pop-telemetry"] v5 = [] wasm-contracts = ["pop-contracts/v5", "dep:pop-contracts", "dep:sp-core", "dep:sp-weights", "wallet-integration", "v5"] -wallet-integration = ["dep:axum", "dep:open", "dep:tower-http"] \ No newline at end of file +wallet-integration = ["dep:axum", "dep:open", "dep:tower-http"] diff --git a/crates/pop-cli/src/commands/add/mod.rs b/crates/pop-cli/src/commands/add/mod.rs new file mode 100644 index 000000000..d78bf1152 --- /dev/null +++ b/crates/pop-cli/src/commands/add/mod.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 + +use clap::{Args, Subcommand}; + +pub mod pallet; + +/// Arguments for adding a new feature to existing code +#[derive(Args)] +#[command(args_conflicts_with_subcommands = true)] +pub struct AddArgs { + #[command(subcommand)] + pub command: Command, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Add a new pallet to an existing runtime + #[clap(alias = "P")] + Pallet(pallet::AddPalletCommand), +} diff --git a/crates/pop-cli/src/commands/add/pallet.rs b/crates/pop-cli/src/commands/add/pallet.rs new file mode 100644 index 000000000..5f3bb531d --- /dev/null +++ b/crates/pop-cli/src/commands/add/pallet.rs @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{ + cli::{traits::Cli as _, Cli}, + common::writer::{self, PalletConfigRelatedPaths, RuntimeUsedMacro}, +}; +use clap::{error::ErrorKind, Args, Command}; +use common_pallets::CommonPallets; +use fs_rollback::Rollback; +use rust_writer::{ + ast::{ + finder, + finder::{Finder, ToFind}, + implementors::{ItemToFile, ItemToMod, TokenStreamToMacro}, + mutator, + mutator::{Mutator, ToMutate}, + }, + preserver::Preserver, +}; +use rustilities::manifest::{ManifestDependencyConfig, ManifestDependencyOrigin}; +use std::{env, path::PathBuf}; +use strum::{EnumMessage, IntoEnumIterator}; +use syn::{parse_quote, visit_mut::VisitMut}; + +mod common_pallets; + +#[mutator(ItemToFile, ItemToFile)] +#[finder(ItemToFile, ItemToFile)] +#[impl_from] +struct PalletImplBlockImplementor; + +#[derive(Args, Debug, Clone)] +pub struct AddPalletCommand { + #[arg(long, short, help = "The pallet added to the runtime.")] + pub(crate) pallet: Option, + #[arg( + short, + long, + help = "pop add pallet should be called from a runtime crate or from a workspace containing a runtime crate. If this command is called from somewhere else, this argument allows to specify the path to the runtime crate." + )] + pub(crate) runtime_path: Option, + #[arg( + long, + help = "pop add pallet will place the impl blocks for your pallets' Config traits inside a dedicated file under the configs directory. Use this argument to place them somewhere else." + )] + pub(crate) pallet_impl_path: Option, + #[arg(long, help = "The release of the polkadot-sdk used by your project.")] + pub(crate) release_tag: Option, +} + +const POLKADOT_SDK_URL: &str = "https://github.com/paritytech/polkadot-sdk"; + +const INVALID_DIR_MSG: &str = "Make sure to run this command either in a runtime crate contained in a workspace, in the workspace itself or to specify the path to the runtime crate using -r."; + +impl AddPalletCommand { + pub(crate) async fn execute(self) -> anyhow::Result<()> { + Cli.intro("Add a new pallet to your runtime")?; + let mut cmd = Command::new(""); + + let (pallet, release_tag) = match (self.pallet, self.release_tag) { + (Some(pallet), Some(release_tag)) => (pallet, release_tag), + (None, None) => { + let mut pallet_prompt = + cliclack::select("Select a pallet to add to your runtime: ".to_owned()); + for (i, pallet) in CommonPallets::iter().enumerate() { + if i == 0 { + pallet_prompt = pallet_prompt.initial_value(pallet); + } + pallet_prompt = pallet_prompt.item( + pallet, + pallet.get_message().expect("all variants of CommonPallets have message; qed;"), + pallet.get_detailed_message().expect("all variants of CommonPallets have detailed_message; qed;"), + ); + } + let mut release_tag_prompt = cliclack::input("Which release_tag should use your pallet?") + .placeholder("stable2412"); + (pallet_prompt.interact()?, release_tag_prompt.interact()?) + }, + _ => cmd + .error( + ErrorKind::Io, + "If you specify pallet/release_tag via the command line, both fields must be specified" + ) + .exit(), + }; + + let runtime_path = if let Some(path) = &self.runtime_path { + pop_common::helpers::prefix_with_current_dir_if_needed(&path) + } else { + let working_dir = match env::current_dir() { + Ok(working_dir) => working_dir, + _ => cmd.error(ErrorKind::Io, "Cannot modify the working crate").exit(), + }; + // Give the chance to use the command either from a workspace containing a runtime or + // from a runtime crate if path not specified + if working_dir.join("runtime").exists() { + pop_common::helpers::prefix_with_current_dir_if_needed(working_dir.join("runtime")) + } else { + pop_common::helpers::prefix_with_current_dir_if_needed(&working_dir) + } + }; + + let spinner = cliclack::spinner(); + spinner.start("Updating runtime..."); + + let pallet_name = pallet + .get_message() + .expect("All pallets in common_pallets::CommonPallets have a defined message; qed;"); + + let pallet_config_related_paths = writer::compute_pallet_related_paths(&runtime_path); + + let PalletConfigRelatedPaths { runtime_lib_path, configs_folder_path, .. } = + pallet_config_related_paths.clone(); + + let runtime_lib_content = std::fs::read_to_string(&runtime_lib_path)?; + + if !runtime_lib_content.contains("construct_runtime!") && + !runtime_lib_content.contains("mod runtime") + { + cmd.error(ErrorKind::InvalidValue, INVALID_DIR_MSG).exit(); + } + + let pallet_config_path = configs_folder_path.join(format!("{}.rs", pallet_name)); + + let mut rollback = Rollback::default(); + + let runtime_manifest = rustilities::manifest::find_innermost_manifest(&runtime_path) + .ok_or(anyhow::anyhow!(INVALID_DIR_MSG))?; + + let workspace_manifest = pop_common::find_workspace_toml(&runtime_path) + .ok_or(anyhow::anyhow!(INVALID_DIR_MSG))?; + + rollback.note_file(&runtime_lib_path)?; + + rollback.note_file(&runtime_manifest)?; + + rollback.note_file(&workspace_manifest)?; + + if let Some(ref pallet_impl_path) = self.pallet_impl_path { + // The impl path may be the runtime lib, so the path may be already noted. + match rollback.note_file(pallet_impl_path) { + Ok(()) => (), + Err(fs_rollback::Error::AlreadyNoted(_)) => (), + Err(err) => return Err(err.into()), + } + } + + let roll_pallet_impl_path = match self.pallet_impl_path { + Some(ref pallet_impl_path) => rollback + .get_noted_file(pallet_impl_path) + .expect("The file has been noted above;qed;"), + None => { + rollback = writer::create_new_pallet_impl_path_structure( + rollback, + &pallet_config_related_paths, + &pallet_config_path, + &pallet_name, + )?; + + rollback + .get_new_file(&pallet_config_path) + .expect("create_new_pallet_impl_path_structure noted this file; qed;") + }, + }; + + let roll_runtime_lib_path = rollback + .get_noted_file(&runtime_lib_path) + .expect("The file has been noted above; qed;"); + + let roll_runtime_manifest = rollback + .get_noted_file(&runtime_manifest) + .expect("The file has been noted above; qed;"); + + let roll_workspace_manifest = rollback + .get_noted_file(&workspace_manifest) + .expect("The file has been noted above; qed;"); + + // Add the pallet to the runtime module + let construct_runtime_preserver = Preserver::new("construct_runtime!"); + let mod_runtime_preserver = Preserver::new("mod runtime"); + let mut preserved_ast = rust_writer::preserver::preserve_and_parse( + roll_runtime_lib_path, + &[&construct_runtime_preserver, &mod_runtime_preserver], + )?; + + // Parse the runtime to find which of the runtime macros is being used and the highest + // pallet index used (if needed, otherwise 0). + let used_macro = writer::find_used_runtime_macro(&preserved_ast)?; + match used_macro { + RuntimeUsedMacro::Runtime => { + let highest_index = writer::find_highest_pallet_index(&preserved_ast)?; + let pallet_to_runtime_implementor: ItemToMod = + ("runtime", pallet.get_pallet_declaration_runtime_module(highest_index)).into(); + + let mut finder = Finder::default().to_find(&pallet_to_runtime_implementor); + if finder.find(&preserved_ast) { + return Err(anyhow::anyhow!(format!( + "{} is already in use.", + pallet.get_crate_name() + ))); + } else { + let mut mutator = Mutator::default().to_mutate(&pallet_to_runtime_implementor); + mutator.mutate(&mut preserved_ast)?; + rust_writer::preserver::resolve_preserved( + &preserved_ast, + roll_runtime_lib_path, + )?; + } + }, + RuntimeUsedMacro::ConstructRuntime => { + let pallet_to_construct_runtime_implementor: TokenStreamToMacro = ( + parse_quote!(construct_runtime), + Some(parse_quote!(Runtime)), + pallet.get_pallet_declaration_construct_runtime(), + ) + .into(); + let mut finder = + Finder::default().to_find(&pallet_to_construct_runtime_implementor); + if finder.find(&preserved_ast) { + return Err(anyhow::anyhow!(format!( + "{} is already in use.", + pallet.get_crate_name() + ))); + } else { + let mut mutator = + Mutator::default().to_mutate(&pallet_to_construct_runtime_implementor); + mutator.mutate(&mut preserved_ast)?; + rust_writer::preserver::resolve_preserved( + &preserved_ast, + roll_runtime_lib_path, + )?; + } + }, + } + + // Add the pallet impl block and its related use statements + let use_preserver = Preserver::new("use"); + let pub_use_preserver = Preserver::new("pub use"); + + let mut preserved_ast = rust_writer::preserver::preserve_and_parse( + roll_pallet_impl_path, + &[&use_preserver, &pub_use_preserver], + )?; + + for use_statement in pallet.get_impl_needed_use_statements() { + let use_statement: ItemToFile = use_statement.into(); + let mut finder = Finder::default().to_find(&use_statement); + if !finder.find(&preserved_ast) { + let mut mutator = Mutator::default().to_mutate(&use_statement); + mutator.mutate(&mut preserved_ast)?; + } + } + + let pallet_impl_block_implementor: PalletImplBlockImplementor = ( + ItemToFile { item: pallet.get_needed_parameter_types() }, + ItemToFile { item: pallet.get_needed_impl_block() }, + ) + .into(); + + let mut mutator: PalletImplBlockImplementorMutatorWrapper = + Mutator::default().to_mutate(&pallet_impl_block_implementor).into(); + + mutator.mutate(&mut preserved_ast, None)?; + + rust_writer::preserver::resolve_preserved(&preserved_ast, roll_pallet_impl_path)?; + + // Update the manifests to add the pallet crate + rustilities::manifest::add_crate_to_dependencies( + roll_workspace_manifest, + &pallet.get_crate_name(), + ManifestDependencyConfig::new( + ManifestDependencyOrigin::git(POLKADOT_SDK_URL, &release_tag), + false, + vec![], + false, + ), + )?; + + rustilities::manifest::add_crate_to_dependencies( + roll_runtime_manifest, + &pallet.get_crate_name(), + ManifestDependencyConfig::new( + ManifestDependencyOrigin::workspace(), + false, + vec![], + false, + ), + )?; + + pop_common::manifest::add_pallet_features_to_manifest( + roll_runtime_manifest, + pallet.get_crate_name(), + )?; + + rollback.commit()?; + + if let Some(mut workspace_toml) = pop_common::manifest::find_workspace_toml(&runtime_path) { + workspace_toml.pop(); + rustilities::fmt::format_dir(&workspace_toml)?; + } else { + rustilities::fmt::format_dir(&runtime_path)?; + } + + spinner.stop("Your runtime has been updated and it's ready to use 🚀"); + Ok(()) + } +} diff --git a/crates/pop-cli/src/commands/add/pallet/common_pallets.rs b/crates/pop-cli/src/commands/add/pallet/common_pallets.rs new file mode 100644 index 000000000..2ae29146f --- /dev/null +++ b/crates/pop-cli/src/commands/add/pallet/common_pallets.rs @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: GPL-3.0 +use clap::ValueEnum; +use proc_macro2::{Literal, TokenStream}; +use strum_macros::{EnumIter, EnumMessage}; +use syn::{parse_quote, Item}; + +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumMessage, ValueEnum, Eq)] +pub(crate) enum CommonPallets { + /// A simple, secure module for dealing with fungible assets. + #[strum( + message = "assets", + detailed_message = "A simple, secure module for dealing with fungible assets.." + )] + Assets, + /// The Contracts module provides functionality for the runtime to deploy and execute + /// WebAssembly smart-contracts. + #[strum( + message = "contracts", + detailed_message = "The Contracts module provides functionality for the runtime to deploy and execute WebAssembly smart-contracts." + )] + Contracts, + /// Experimental module that provides functionality for the runtime to deploy and execute + /// PolkaVM smart-contracts. + #[strum( + message = "revive", + detailed_message = "Experimental module that provides functionality for the runtime to deploy and execute PolkaVM smart-contracts." + )] + Revive, + /// allows for a single account (called the "sudo key") to execute dispatchable functions that + /// require a Root call or designate a new account to replace them as the sudo key. + #[strum( + message = "sudo", + detailed_message = " allows for a single account (called the \"sudo key\") to execute dispatchable functions that require a Root call or designate a new account to replace them as the sudo key." + )] + Sudo, + /// A stateless module with helpers for dispatch management which does no re-authentication. + #[strum( + message = "utility", + detailed_message = "A stateless module with helpers for dispatch management which does no re-authentication." + )] + Utility, +} + +impl CommonPallets { + pub(crate) fn get_crate_name(&self) -> String { + match self { + CommonPallets::Assets => "pallet-assets".to_owned(), + CommonPallets::Contracts => "pallet-contracts".to_owned(), + CommonPallets::Revive => "pallet-revive".to_owned(), + CommonPallets::Sudo => "pallet-sudo".to_owned(), + CommonPallets::Utility => "pallet-utility".to_owned(), + } + } + + pub(crate) fn get_pallet_declaration_construct_runtime(&self) -> TokenStream { + match self { + CommonPallets::Assets => parse_quote! { Assets: pallet_assets, }, + CommonPallets::Contracts => parse_quote! { Contracts: pallet_contracts, }, + CommonPallets::Revive => parse_quote! { Revive: pallet_revive, }, + CommonPallets::Sudo => parse_quote! { Sudo: pallet_sudo, }, + CommonPallets::Utility => parse_quote! { Utility: pallet_utility, }, + } + } + + pub(crate) fn get_pallet_declaration_runtime_module(&self, highest_index: Literal) -> Item { + match self { + CommonPallets::Assets => parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(#highest_index)] + pub type Assets = pallet_assets; + }, + CommonPallets::Contracts => parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(#highest_index)] + pub type Contracts = pallet_contracts; + }, + CommonPallets::Revive => parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(#highest_index)] + pub type Revive = pallet_revive; + }, + CommonPallets::Sudo => parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(#highest_index)] + pub type Sudo = pallet_sudo; + }, + CommonPallets::Utility => parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(#highest_index)] + pub type Utility = pallet_utility; + }, + } + } + + pub(crate) fn get_impl_needed_use_statements(&self) -> Vec { + match self { + CommonPallets::Assets => vec![ + parse_quote!( + ///TEMP_DOC + use crate::{ + AccountId, Balances, Runtime, RuntimeEvent, RuntimeHoldReason, RuntimeCall, + }; + ), + parse_quote!( + use frame_support::{parameter_types, derive_impl, traits::AsEnsureOriginWithArg}; + ), + parse_quote!( + use frame_system::{EnsureRoot, EnsureSigned}; + ), + ], + CommonPallets::Contracts => vec![ + parse_quote!( + ///TEMP_DOC + use crate::{Runtime, Balances, RuntimeEvent, RuntimeHoldReason, RuntimeCall}; + ), + parse_quote!( + use frame_support::{parameter_types, derive_impl}; + ), + ], + CommonPallets::Revive => vec![ + parse_quote!( + ///TEMP_DOC + use crate::{Runtime, Balances, RuntimeEvent, RuntimeHoldReason, RuntimeCall}; + ), + parse_quote!( + use frame_support::{parameter_types, derive_impl}; + ), + ], + CommonPallets::Utility => vec![parse_quote!( + ///TEMP_DOC + use crate::{OriginCaller, RuntimeCall, RuntimeEvent}; + )], + CommonPallets::Sudo => vec![], + } + } + + pub(crate) fn get_needed_parameter_types(&self) -> Item { + match self { + CommonPallets::Assets => Item::Verbatim(TokenStream::new()), + CommonPallets::Contracts => parse_quote! { + ///TEMP_DOC + parameter_types!{ + pub Schedule: pallet_contracts::Schedule = >::default(); + } + }, + CommonPallets::Revive => Item::Verbatim(TokenStream::new()), + CommonPallets::Sudo => Item::Verbatim(TokenStream::new()), + CommonPallets::Utility => Item::Verbatim(TokenStream::new()), + } + } + + pub(crate) fn get_needed_impl_block(&self) -> Item { + match self { + CommonPallets::Assets => parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)] + impl pallet_assets::Config for Runtime{ + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + } + }, + CommonPallets::Contracts => parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_contracts::config_preludes::TestDefaultConfig)] + impl pallet_contracts::Config for Runtime{ + type Currency = Balances; + type Schedule = Schedule; + type CallStack = [pallet_contracts::Frame; 5]; + } + }, + CommonPallets::Revive => parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_revive::config_preludes::TestDefaultConfig)] + impl pallet_revive::Config for Runtime{ + type Currency = Balances; + type AddressMapper = pallet_revive::AccountId32Mapper; + } + }, + CommonPallets::Sudo => parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_sudo::config_preludes::TestDefaultConfig)] + impl pallet::Config for Runtime{} + }, + CommonPallets::Utility => parse_quote! { + ///TEMP_DOC + impl pallet_utility::Config for Runtime{ + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_utility::weights::SubstrateWeight; + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_crate_name_works() { + assert_eq!(CommonPallets::Assets.get_crate_name(), "pallet-assets"); + assert_eq!(CommonPallets::Contracts.get_crate_name(), "pallet-contracts"); + assert_eq!(CommonPallets::Revive.get_crate_name(), "pallet-revive"); + assert_eq!(CommonPallets::Sudo.get_crate_name(), "pallet-sudo"); + assert_eq!(CommonPallets::Utility.get_crate_name(), "pallet-utility"); + } + + #[test] + fn get_pallet_declaration_construct_runtime_works() { + assert!(rustilities::parsing::syntactic_token_stream_compare( + CommonPallets::Assets.get_pallet_declaration_construct_runtime(), + parse_quote! { Assets: pallet_assets, } + )); + assert!(rustilities::parsing::syntactic_token_stream_compare( + CommonPallets::Contracts.get_pallet_declaration_construct_runtime(), + parse_quote! { Contracts: pallet_contracts, } + )); + assert!(rustilities::parsing::syntactic_token_stream_compare( + CommonPallets::Revive.get_pallet_declaration_construct_runtime(), + parse_quote! { Revive: pallet_revive, } + )); + assert!(rustilities::parsing::syntactic_token_stream_compare( + CommonPallets::Sudo.get_pallet_declaration_construct_runtime(), + parse_quote! { Sudo: pallet_sudo, } + )); + assert!(rustilities::parsing::syntactic_token_stream_compare( + CommonPallets::Utility.get_pallet_declaration_construct_runtime(), + parse_quote! { Utility: pallet_utility, } + )); + } + + #[test] + fn get_pallet_declaration_runtime_module_works() { + assert_eq!( + CommonPallets::Assets.get_pallet_declaration_runtime_module(parse_quote!(1)), + parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(1)] + pub type Assets = pallet_assets; + } + ); + assert_eq!( + CommonPallets::Contracts.get_pallet_declaration_runtime_module(parse_quote!(1)), + parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(1)] + pub type Contracts = pallet_contracts; + } + ); + assert_eq!( + CommonPallets::Revive.get_pallet_declaration_runtime_module(parse_quote!(1)), + parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(1)] + pub type Revive = pallet_revive; + } + ); + assert_eq!( + CommonPallets::Sudo.get_pallet_declaration_runtime_module(parse_quote!(1)), + parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(1)] + pub type Sudo = pallet_sudo; + } + ); + assert_eq!( + CommonPallets::Utility.get_pallet_declaration_runtime_module(parse_quote!(1)), + parse_quote! { + ///TEMP_DOC + #[runtime::pallet_index(1)] + pub type Utility = pallet_utility; + } + ); + } + + #[test] + fn get_impl_needed_use_statements_works() { + assert_eq!( + CommonPallets::Assets.get_impl_needed_use_statements(), + vec![ + parse_quote! { + ///TEMP_DOC + use crate::{ + AccountId, Balances, Runtime, RuntimeEvent, RuntimeHoldReason, RuntimeCall, + }; + }, + parse_quote!( + use frame_support::{parameter_types, derive_impl, traits::AsEnsureOriginWithArg}; + ), + parse_quote!( + use frame_system::{EnsureRoot, EnsureSigned}; + ) + ] + ); + assert_eq!( + CommonPallets::Contracts.get_impl_needed_use_statements(), + vec![ + parse_quote! { + ///TEMP_DOC + use crate::{Runtime, Balances, RuntimeEvent, RuntimeHoldReason, RuntimeCall}; + }, + parse_quote!( + use frame_support::{parameter_types, derive_impl}; + ) + ] + ); + assert_eq!( + CommonPallets::Revive.get_impl_needed_use_statements(), + vec![ + parse_quote! { + ///TEMP_DOC + use crate::{Runtime, Balances, RuntimeEvent, RuntimeHoldReason, RuntimeCall}; + }, + parse_quote!( + use frame_support::{parameter_types, derive_impl}; + ) + ] + ); + assert_eq!(CommonPallets::Sudo.get_impl_needed_use_statements(), vec![]); + assert_eq!( + CommonPallets::Utility.get_impl_needed_use_statements(), + vec![parse_quote!( + ///TEMP_DOC + use crate::{OriginCaller, RuntimeCall, RuntimeEvent}; + ),] + ); + } + + #[test] + fn get_needed_parameter_types_works() { + assert_eq!( + CommonPallets::Assets.get_needed_parameter_types(), + Item::Verbatim(TokenStream::new()) + ); + assert_eq!( + CommonPallets::Contracts.get_needed_parameter_types(), + parse_quote! { + ///TEMP_DOC + parameter_types!{ + pub Schedule: pallet_contracts::Schedule = >::default(); + } + } + ); + assert_eq!( + CommonPallets::Revive.get_needed_parameter_types(), + Item::Verbatim(TokenStream::new()) + ); + assert_eq!( + CommonPallets::Sudo.get_needed_parameter_types(), + Item::Verbatim(TokenStream::new()) + ); + assert_eq!( + CommonPallets::Utility.get_needed_parameter_types(), + Item::Verbatim(TokenStream::new()) + ); + } + + #[test] + fn get_needed_impl_block_works() { + assert_eq!( + CommonPallets::Assets.get_needed_impl_block(), + parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)] + impl pallet_assets::Config for Runtime { + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + } + } + ); + assert_eq!( + CommonPallets::Contracts.get_needed_impl_block(), + parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_contracts::config_preludes::TestDefaultConfig)] + impl pallet_contracts::Config for Runtime{ + type Currency = Balances; + type Schedule = Schedule; + type CallStack = [pallet_contracts::Frame; 5]; + } + } + ); + assert_eq!( + CommonPallets::Revive.get_needed_impl_block(), + parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_revive::config_preludes::TestDefaultConfig)] + impl pallet_revive::Config for Runtime{ + type Currency = Balances; + type AddressMapper = pallet_revive::AccountId32Mapper; + } + } + ); + assert_eq!( + CommonPallets::Sudo.get_needed_impl_block(), + parse_quote! { + ///TEMP_DOC + #[derive_impl(pallet_sudo::config_preludes::TestDefaultConfig)] + impl pallet::Config for Runtime{} + } + ); + assert_eq!( + CommonPallets::Utility.get_needed_impl_block(), + parse_quote! { + ///TEMP_DOC + impl pallet_utility::Config for Runtime{ + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_utility::weights::SubstrateWeight; + } + } + ); + } +} diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index f43f6fa5a..1d8d8da59 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -10,6 +10,8 @@ use std::fmt::{Display, Formatter, Result}; #[cfg(feature = "parachain")] use {crate::common::Project::Network, up::network::Relay::*}; +#[cfg(all(feature = "parachain", feature = "experimental"))] +pub(crate) mod add; #[cfg(feature = "parachain")] pub(crate) mod bench; pub(crate) mod build; @@ -59,6 +61,10 @@ pub(crate) enum Command { /// Remove generated/cached artifacts. #[clap(alias = "C")] Clean(clean::CleanArgs), + /// Add a new feature to your existing polkadot-sdk project + #[cfg(all(feature = "parachain", feature = "experimental"))] + #[clap(name = "add", alias = "a")] + Add(add::AddArgs), } /// Help message for the build command. @@ -242,6 +248,10 @@ impl Command { }, } }, + #[cfg(all(feature = "parachain", feature = "experimental"))] + Self::Add(args) => match args.command { + add::Command::Pallet(cmd) => cmd.execute().await.map(|_| Null), + }, } } } @@ -308,6 +318,8 @@ impl Display for Command { Self::Clean(_) => write!(f, "clean"), #[cfg(feature = "parachain")] Self::Bench(args) => write!(f, "bench {}", args.command), + #[cfg(all(feature = "parachain", feature = "experimental"))] + Self::Add(args) => write!(f, "add {:?}", args.command), #[cfg(feature = "hashing")] Command::Hash(args) => write!(f, "hash {}", args.command), } diff --git a/crates/pop-cli/src/common/mod.rs b/crates/pop-cli/src/common/mod.rs index f19fffe0e..ee90452b9 100644 --- a/crates/pop-cli/src/common/mod.rs +++ b/crates/pop-cli/src/common/mod.rs @@ -21,6 +21,8 @@ pub mod runtime; pub mod try_runtime; #[cfg(feature = "wallet-integration")] pub mod wallet; +#[cfg(all(feature = "parachain", feature = "experimental"))] +pub(crate) mod writer; use std::fmt::{Display, Formatter, Result}; use strum::VariantArray; diff --git a/crates/pop-cli/src/common/writer.rs b/crates/pop-cli/src/common/writer.rs new file mode 100644 index 000000000..efda32a9c --- /dev/null +++ b/crates/pop-cli/src/common/writer.rs @@ -0,0 +1,532 @@ +// SPDX-License-Identifier: GPL-3.0 + +use fs_rollback::Rollback; +use proc_macro2::{Literal, Span}; +use rust_writer::{ + ast::{ + finder::{Finder, ToFind}, + implementors::ItemToFile, + mutator::{Mutator, ToMutate}, + }, + preserver::Preserver, +}; +use std::{ + cmp, + path::{Path, PathBuf}, +}; +use syn::{ + parse_quote, File, Ident, Item, ItemMacro, ItemMod, ItemType, Macro, Meta, MetaList, + Path as syn_Path, +}; + +// The different ways available to construct a runtime +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum RuntimeUsedMacro { + // The macro #[runtime] + Runtime, + // The macro construct_runtime! + ConstructRuntime, +} + +#[derive(Debug, Clone)] +pub(crate) struct PalletConfigRelatedPaths { + pub(crate) runtime_lib_path: PathBuf, + pub(crate) configs_rs_path: PathBuf, + pub(crate) configs_folder_path: PathBuf, + pub(crate) configs_mod_path: PathBuf, +} + +// Not more than 256 pallets are included in a runtime +pub(crate) type PalletIndex = u8; + +// Find the highest implemented pallet index in the outer enum if using the macro +// #[runtime]. +pub(crate) fn find_highest_pallet_index(ast: &File) -> anyhow::Result { + let mut highest_index: PalletIndex = 0; + let mut found = false; + for item in &ast.items { + match item { + Item::Mod(ItemMod { ident, content, .. }) + if *ident == "runtime" && content.is_some() => + { + let (_, items) = + content.as_ref().expect("content is always Some thanks to the match guard"); + for item in items { + if let Item::Type(ItemType { attrs, .. }) = item { + if let Some(pallet_index_attribute) = attrs.iter().find(|attribute| { + if let Meta::List(MetaList { + path: syn_Path { segments, .. }, .. + }) = &attribute.meta + { + segments.iter().any(|segment| segment.ident == "pallet_index") + } else { + false + } + }) { + // As the attribute at this point is for sure + // #[runtime::pallet_index(n)], meta is a MetaList where tokens + // is a TokenStream of exactly one element: the literal n. + let mut pallet_index = 0u8; + if let Meta::List(MetaList { tokens, .. }) = + &pallet_index_attribute.meta + { + pallet_index = tokens + .clone() + .into_iter() + .next() + .expect("This iterator has one element due to the attribute shape; qed;") + .to_string() + .parse::() + .expect("The macro #[runtime::pallet_index(n)] is only valid if n is a valid number, so we can parse it to PalletIndex; qed;"); + } + // Despite the pallets will likely be ordered by call_index in the + // runtime, that's not necessarily true, so we keep the highest index in + // order to give the added pallet the next index + highest_index = cmp::max(highest_index, pallet_index); + found = true; + } + } + } + }, + _ => continue, + } + } + + if !found { + return Err(anyhow::anyhow!( + format! {"Unable to find the highest pallet index in runtime file"}, + )); + } + Ok(Literal::u8_unsuffixed(highest_index.saturating_add(1))) +} + +// Determine whether a runtime's ast uses the construct_runtime! macro or the #[runtime] macro. +pub(crate) fn find_used_runtime_macro(ast: &File) -> anyhow::Result { + for item in &ast.items { + match item { + Item::Mod(ItemMod { ident, .. }) if *ident == "runtime" => { + return Ok(RuntimeUsedMacro::Runtime); + }, + Item::Macro(ItemMacro { + mac: Macro { path: syn_Path { segments, .. }, .. }, .. + }) if segments.iter().any(|segment| segment.ident == "construct_runtime") => { + return Ok(RuntimeUsedMacro::ConstructRuntime); + }, + _ => (), + } + } + Err(anyhow::anyhow!(format!("Unable to find a runtime declaration in runtime file"))) +} + +pub(crate) fn compute_pallet_related_paths(runtime_path: &Path) -> PalletConfigRelatedPaths { + let runtime_src_path = runtime_path.join("src"); + let runtime_lib_path = runtime_src_path.join("lib.rs"); + let configs_rs_path = runtime_src_path.join("configs.rs"); + let configs_folder_path = runtime_src_path.join("configs"); + let configs_mod_path = configs_folder_path.join("mod.rs"); + PalletConfigRelatedPaths { + runtime_lib_path, + configs_rs_path, + configs_folder_path, + configs_mod_path, + } +} + +// Creates the structure for the path to the file when the new impl block will be added and add it +// to an existing rollback. +pub(crate) fn create_new_pallet_impl_path_structure<'a>( + mut rollback: Rollback<'a>, + pallet_config_related_paths: &'a PalletConfigRelatedPaths, + pallet_config_path: &'a Path, + pallet_name: &str, +) -> anyhow::Result> { + let PalletConfigRelatedPaths { + runtime_lib_path, + configs_rs_path, + configs_folder_path, + configs_mod_path, + } = pallet_config_related_paths; + let pallet_name_ident = Ident::new(pallet_name, Span::call_site()); + + let mod_preserver = Preserver::new("mod"); + let pub_mod_preserver = Preserver::new("pub mod"); + + let pallet_mod_implementor = ItemToFile { item: parse_quote!(mod #pallet_name_ident;) }; + + match (configs_rs_path.is_file(), configs_mod_path.is_file()) { + // The runtime is using a configs module without the mod.rs sintax + (true, false) => { + if rollback.get_noted_file(configs_rs_path).is_none() { + rollback.note_file(configs_rs_path)?; + } + + let roll_configs_rs_path = rollback + .get_noted_file(configs_rs_path) + .expect("This file has been noted above; qed;"); + let mut preserved_ast = rust_writer::preserver::preserve_and_parse( + roll_configs_rs_path, + &[&mod_preserver, &pub_mod_preserver], + )?; + + let mut finder = Finder::default().to_find(&pallet_mod_implementor); + let pallet_already_declared = finder.find(&preserved_ast); + if !pallet_already_declared { + let mut mutator = Mutator::default().to_mutate(&pallet_mod_implementor); + mutator.mutate(&mut preserved_ast)?; + rust_writer::preserver::resolve_preserved(&preserved_ast, roll_configs_rs_path)?; + } + + rollback.new_file(pallet_config_path)?; + Ok(rollback) + }, + // The runtime is using a configs module with the mod.rs syntax + (false, true) => { + if rollback.get_noted_file(configs_mod_path).is_none() { + rollback.note_file(configs_mod_path)?; + } + + let roll_configs_mod_path = rollback + .get_noted_file(configs_mod_path) + .expect("This file has been noted above; qed;"); + let mut preserved_ast = rust_writer::preserver::preserve_and_parse( + roll_configs_mod_path, + &[&mod_preserver, &pub_mod_preserver], + )?; + + let mut finder = Finder::default().to_find(&pallet_mod_implementor); + let pallet_already_declared = finder.find(&preserved_ast); + if !pallet_already_declared { + let mut mutator = Mutator::default().to_mutate(&pallet_mod_implementor); + mutator.mutate(&mut preserved_ast)?; + rust_writer::preserver::resolve_preserved(&preserved_ast, roll_configs_mod_path)?; + } + + rollback.new_file(pallet_config_path)?; + Ok(rollback) + }, + // The runtime isn't using a configs module yet, we opt for the configs.rs + // convention + (false, false) => { + let configs_mod_implementor = ItemToFile { + item: parse_quote!( + pub mod configs; + ), + }; + if rollback.get_noted_file(runtime_lib_path).is_none() { + rollback.note_file(runtime_lib_path)?; + } + + let roll_runtime_lib_path = rollback + .get_noted_file(runtime_lib_path) + .expect("This file has been noted above; qed;"); + let mut preserved_ast = rust_writer::preserver::preserve_and_parse( + roll_runtime_lib_path, + &[&mod_preserver, &pub_mod_preserver], + )?; + + let mut finder = Finder::default().to_find(&configs_mod_implementor); + let configs_already_declared = finder.find(&preserved_ast); + if !configs_already_declared { + let mut mutator = Mutator::default().to_mutate(&configs_mod_implementor); + mutator.mutate(&mut preserved_ast)?; + rust_writer::preserver::resolve_preserved(&preserved_ast, roll_runtime_lib_path)?; + } + + rollback.new_file(configs_rs_path)?; + rollback.new_dir(configs_folder_path)?; + rollback.new_file(pallet_config_path)?; + + let roll_configs_rs_path = rollback + .get_new_file(configs_rs_path) + .expect("The new file has been noted above; qed"); + + // New file so we can mutate it directly. + let mut ast = rust_writer::preserver::preserve_and_parse(roll_configs_rs_path, &[])?; + let mut mutator = Mutator::default().to_mutate(&pallet_mod_implementor); + mutator.mutate(&mut ast)?; + rust_writer::preserver::resolve_preserved(&ast, roll_configs_rs_path)?; + + Ok(rollback) + }, + (true, true) => unreachable!("Both approaches not supported by the compiler; qed;"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use pop_parachains::{Config, Parachain}; + use similar::{ChangeTag, TextDiff}; + use std::path::PathBuf; + + fn setup_template_runtime_v2_macro() -> Result { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let config = Config { + symbol: "DOT".to_string(), + decimals: 18, + initial_endowment: "1000000".to_string(), + }; + pop_parachains::instantiate_standard_template( + &Parachain::Standard, + temp_dir.path(), + config, + None, + )?; + Ok(temp_dir) + } + + fn setup_template_construct_runtime_macro() -> Result { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + pop_parachains::instantiate_openzeppelin_template( + &Parachain::OpenZeppelinGeneric, + temp_dir.path(), + Some("v2.0.3".to_owned()), + )?; + Ok(temp_dir) + } + + #[test] + fn find_highest_pallet_index_works() { + let temp_dir = + setup_template_runtime_v2_macro().expect("Failed to setup template and instantiate"); + + let ast = syn::parse_file( + &std::fs::read_to_string(temp_dir.path().join("runtime").join("src").join("lib.rs")) + .expect("File should be readable; qed;"), + ) + .expect("File should be parseable; qed;"); + + let highest_index = find_highest_pallet_index(&ast) + .expect("find_highest_pallet_index is supposed to be Ok"); + + // The highest index in the template is 33 + assert_eq!(highest_index.to_string(), "34"); + } + + #[test] + fn find_highest_pallet_index_fails_if_input_doesnt_use_runtime_macro() { + let temp_dir = setup_template_construct_runtime_macro() + .expect("Failed to setup template and instantiate"); + + let ast = syn::parse_file( + &std::fs::read_to_string(temp_dir.path().join("runtime").join("src").join("lib.rs")) + .expect("File should be readable; qed;"), + ) + .expect("File should be parseable; qed;"); + + let failed_call = find_highest_pallet_index(&ast); + assert!( + matches!(failed_call, Err(msg) if msg.to_string() == "Unable to find the highest pallet index in runtime file") + ); + } + + #[test] + fn find_used_runtime_macro_with_construct_runtime_works_well() { + let temp_dir = setup_template_construct_runtime_macro() + .expect("Failed to setup template and instantiate"); + + let ast = syn::parse_file( + &std::fs::read_to_string(temp_dir.path().join("runtime").join("src").join("lib.rs")) + .expect("File should be readable; qed;"), + ) + .expect("File should be parseable; qed;"); + + let used_macro = + find_used_runtime_macro(&ast).expect("find_used_runtime_macro is supposed to be Ok"); + + assert_eq!(used_macro, RuntimeUsedMacro::ConstructRuntime); + } + + #[test] + fn find_used_runtime_macro_with_runtime_macro_works_well() { + let temp_dir = + setup_template_runtime_v2_macro().expect("Failed to setup template and instantiate"); + + let ast = syn::parse_file( + &std::fs::read_to_string(temp_dir.path().join("runtime").join("src").join("lib.rs")) + .expect("File should be readable; qed;"), + ) + .expect("File should be parseable; qed;"); + + let used_macro = + find_used_runtime_macro(&ast).expect("find_used_runtime_macro is supposed to be Ok"); + + assert_eq!(used_macro, RuntimeUsedMacro::Runtime); + } + + #[test] + fn find_used_runtime_macro_fails_if_input_isnt_runtime_file() { + let temp_dir = + setup_template_runtime_v2_macro().expect("Failed to setup template and instantiate"); + + let ast = syn::parse_file( + &std::fs::read_to_string( + temp_dir.path().join("runtime").join("src").join("configs").join("mod.rs"), + ) + .expect("File should be readable; qed;"), + ) + .expect("File should be parseable; qed;"); + + let failed_call = find_used_runtime_macro(&ast); + + assert!( + matches!(failed_call, Err(msg) if msg.to_string() == "Unable to find a runtime declaration in runtime file") + ); + } + + #[test] + fn compute_pallet_related_paths_works() { + let original_path = PathBuf::from("test"); + let paths = compute_pallet_related_paths(&original_path); + + assert_eq!(paths.runtime_lib_path, PathBuf::from("test/src/lib.rs")); + assert_eq!(paths.configs_rs_path, PathBuf::from("test/src/configs.rs")); + assert_eq!(paths.configs_folder_path, PathBuf::from("test/src/configs")); + assert_eq!(paths.configs_mod_path, PathBuf::from("test/src/configs/mod.rs")); + } + + #[test] + fn create_new_pallet_impl_path_structure_configs_mod_template() { + let temp_dir = setup_template_construct_runtime_macro() + .expect("Failed to setup template and instantiate"); + + let paths = compute_pallet_related_paths(&temp_dir.path().join("runtime")); + + let pallet_config_path = paths.configs_folder_path.join("test.rs"); + let pallet_name = "test"; + + let mut rollback = Rollback::default(); + + assert!(!pallet_config_path.exists()); + let configs_mod_before = std::fs::read_to_string(&paths.configs_mod_path).unwrap(); + + rollback = create_new_pallet_impl_path_structure( + rollback, + &paths, + &pallet_config_path, + pallet_name, + ) + .expect("Failed to create new pallet impl path structure"); + + rollback.commit().expect("Failed to commit changes"); + + let configs_mod_after = std::fs::read_to_string(&paths.configs_mod_path).unwrap(); + + let configs_mod_diff = TextDiff::from_lines(&configs_mod_before, &configs_mod_after); + + let expected_inserted_lines = vec!["mod test;\n"]; + let mut inserted_lines = vec![]; + + for change in configs_mod_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => panic!("no deletion expected"), + ChangeTag::Insert => inserted_lines.push(change.value()), + _ => (), + } + } + + assert!(pallet_config_path.exists()); + assert_eq!(expected_inserted_lines, inserted_lines); + } + + #[test] + fn create_new_pallet_impl_path_structure_configs_file_template() { + let temp_dir = setup_template_construct_runtime_macro() + .expect("Failed to setup template and instantiate"); + + let paths = compute_pallet_related_paths(&temp_dir.path().join("runtime")); + + let pallet_config_path = &paths.configs_folder_path.join("test.rs"); + let pallet_name = "test"; + + let mut rollback = Rollback::default(); + + // Create a configs.rs file at the runtime level and delete the mod.rs file, to get a + // template where the file configs.rs exists and then is used as the configs module root + std::fs::remove_file(&paths.configs_mod_path).unwrap(); + std::fs::File::create(&paths.configs_rs_path).unwrap(); + + assert!(!pallet_config_path.exists()); + let configs_rs_before = std::fs::read_to_string(&paths.configs_rs_path).unwrap(); + + rollback = create_new_pallet_impl_path_structure( + rollback, + &paths, + &pallet_config_path, + pallet_name, + ) + .expect("Failed to create new pallet impl path structure"); + + rollback.commit().expect("Failed to commit changes"); + + let configs_rs_after = std::fs::read_to_string(&paths.configs_rs_path).unwrap(); + + let configs_rs_diff = TextDiff::from_lines(&configs_rs_before, &configs_rs_after); + + let expected_inserted_lines = vec!["mod test;\n"]; + let mut inserted_lines = vec![]; + + for change in configs_rs_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => panic!("no deletion expected"), + ChangeTag::Insert => inserted_lines.push(change.value()), + _ => (), + } + } + + assert!(pallet_config_path.exists()); + assert_eq!(expected_inserted_lines, inserted_lines); + } + + #[test] + fn create_new_pallet_impl_path_structure_without_configs_template() { + let temp_dir = setup_template_construct_runtime_macro() + .expect("Failed to setup template and instantiate"); + + let paths = compute_pallet_related_paths(&temp_dir.path().join("runtime")); + + let pallet_config_path = &paths.configs_folder_path.join("test.rs"); + let pallet_name = "test"; + + let mut rollback = Rollback::default(); + + // Remove configs from the template and clean the lib path (the only interesting thing here + // is that pub mod configs; is added to that file). + std::fs::remove_dir_all(&paths.configs_folder_path).unwrap(); + std::fs::File::create(&paths.runtime_lib_path).unwrap(); + + assert!(!pallet_config_path.exists()); + let runtime_lib_before = std::fs::read_to_string(&paths.runtime_lib_path).unwrap(); + + rollback = create_new_pallet_impl_path_structure( + rollback, + &paths, + &pallet_config_path, + pallet_name, + ) + .expect("Failed to create new pallet impl path structure"); + + rollback.commit().expect("Failed to commit changes"); + + let runtime_lib_after = std::fs::read_to_string(&paths.runtime_lib_path).unwrap(); + + let runtime_lib_diff = TextDiff::from_lines(&runtime_lib_before, &runtime_lib_after); + + let expected_inserted_lines = vec!["pub mod configs;\n"]; + let mut inserted_lines = vec![]; + + for change in runtime_lib_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => panic!("no deletion expected"), + ChangeTag::Insert => inserted_lines.push(change.value()), + _ => (), + } + } + + assert!(pallet_config_path.exists()); + assert!(&paths.configs_folder_path.is_dir()); + assert_eq!(std::fs::read_to_string(&paths.configs_rs_path).unwrap(), "mod test;\n"); + assert_eq!(expected_inserted_lines, inserted_lines); + } +} diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index 0aefc0b51..1717ba5fa 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -6,6 +6,7 @@ use anyhow::Result; use assert_cmd::cargo::cargo_bin; use pop_common::{find_free_port, templates::Template}; use pop_parachains::Parachain; +use similar::{ChangeTag, TextDiff}; use std::{ffi::OsStr, fs, path::Path, process::Command, thread::sleep, time::Duration}; use strum::VariantArray; @@ -37,9 +38,9 @@ fn generate_all_the_templates() -> Result<()> { Ok(()) } -/// Test the parachain lifecycle: new, build, up, call. -#[test] -fn parachain_lifecycle() -> Result<()> { +/// Test the parachain lifecycle: new, add pallet ,build, up, call. +#[tokio::test] +async fn parachain_lifecycle() -> Result<()> { // For testing locally: set to `true` const LOCAL_TESTING: bool = false; @@ -58,6 +59,8 @@ fn parachain_lifecycle() -> Result<()> { "new", "parachain", "test_parachain", + "--release-tag", + "polkadot-stable2412", "--symbol", "POP", "--decimals", @@ -71,6 +74,123 @@ fn parachain_lifecycle() -> Result<()> { assert!(working_dir.exists()); } + // pop add correctly adds pallet-contracts to the template + let runtime_path = working_dir.join("runtime"); + + let workspace_manifest_path = working_dir.join("Cargo.toml"); + let runtime_manifest_path = runtime_path.join("Cargo.toml"); + let runtime_lib_path = runtime_path.join("src").join("lib.rs"); + let pallet_configs_path = runtime_path.join("src").join("configs"); + let pallet_configs_mod_path = pallet_configs_path.join("mod.rs"); + let contracts_pallet_config_path = pallet_configs_path.join("contracts.rs"); + + assert!(!contracts_pallet_config_path.exists()); + + let runtime_lib_content_before = std::fs::read_to_string(&runtime_lib_path).unwrap(); + let pallet_configs_mod_content_before = + std::fs::read_to_string(&pallet_configs_mod_path).unwrap(); + let workspace_manifest_content_before = + std::fs::read_to_string(&workspace_manifest_path).unwrap(); + let runtime_manifest_content_before = std::fs::read_to_string(&runtime_manifest_path).unwrap(); + + let mut command = + pop(&working_dir, &["add", "pallet", "-p", "contracts", "--release-tag", "stable2412"]); + assert!(command.spawn()?.wait()?.success()); + + let runtime_lib_content_after = std::fs::read_to_string(&runtime_lib_path).unwrap(); + let pallet_configs_mod_content_after = + std::fs::read_to_string(&pallet_configs_mod_path).unwrap(); + let workspace_manifest_content_after = + std::fs::read_to_string(&workspace_manifest_path).unwrap(); + let runtime_manifest_content_after = std::fs::read_to_string(&runtime_manifest_path).unwrap(); + let contracts_pallet_config_content = + std::fs::read_to_string(&contracts_pallet_config_path).unwrap(); + + let runtime_lib_diff = + TextDiff::from_lines(&runtime_lib_content_before, &runtime_lib_content_after); + let pallet_configs_mod_diff = + TextDiff::from_lines(&pallet_configs_mod_content_before, &pallet_configs_mod_content_after); + let workspace_manifest_diff = + TextDiff::from_lines(&workspace_manifest_content_before, &workspace_manifest_content_after); + let runtime_manifest_diff = + TextDiff::from_lines(&runtime_manifest_content_before, &runtime_manifest_content_after); + + let expected_inserted_lines_runtime_lib = vec![ + "\n", + " #[runtime::pallet_index(34)]\n", + " pub type Contracts = pallet_contracts;\n", + ]; + let expected_inserted_lines_configs_mod = vec!["mod contracts;\n"]; + let expected_inserted_lines_workspace_manifest = + vec!["pallet-contracts = { git = \"https://github.com/paritytech/polkadot-sdk\", branch = \"stable2412\", default-features = false }\n"]; + + let expected_inserted_lines_runtime_manifest = vec![ + "pallet-contracts = { workspace = true, default-features = false }\n", + " \"xcm/std\", \"pallet-contracts/std\",\n", + " \"xcm-executor/runtime-benchmarks\", \"pallet-contracts/runtime-benchmarks\",\n", + " \"sp-runtime/try-runtime\", \"pallet-contracts/try-runtime\",\n", + ]; + + let mut inserted_lines_runtime_lib = Vec::with_capacity(3); + let mut inserted_lines_configs_mod = Vec::with_capacity(1); + let mut inserted_lines_workspace_manifest = Vec::with_capacity(1); + let mut inserted_lines_runtime_manifest = Vec::with_capacity(1); + + for change in runtime_lib_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => panic!("no deletion expected"), + ChangeTag::Insert => inserted_lines_runtime_lib.push(change.value()), + _ => (), + } + } + + for change in pallet_configs_mod_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => panic!("no deletion expected"), + ChangeTag::Insert => inserted_lines_configs_mod.push(change.value()), + _ => (), + } + } + + for change in workspace_manifest_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => panic!("no deletion expected"), + ChangeTag::Insert => inserted_lines_workspace_manifest.push(change.value()), + _ => (), + } + } + + for change in runtime_manifest_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Insert => inserted_lines_runtime_manifest.push(change.value()), + _ => (), + } + } + + assert_eq!(expected_inserted_lines_runtime_lib, inserted_lines_runtime_lib); + assert_eq!(expected_inserted_lines_configs_mod, inserted_lines_configs_mod); + assert_eq!(expected_inserted_lines_workspace_manifest, inserted_lines_workspace_manifest); + assert_eq!(expected_inserted_lines_runtime_manifest, inserted_lines_runtime_manifest); + + assert_eq!( + contracts_pallet_config_content, + r#"use crate::{Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason}; +use frame_support::{derive_impl, parameter_types}; + +parameter_types! { + pub Schedule : pallet_contracts::Schedule < Runtime > = < pallet_contracts::Schedule + < Runtime >> ::default(); +} + +#[derive_impl(pallet_contracts::config_preludes::TestDefaultConfig)] +impl pallet_contracts::Config for Runtime { + type Currency = Balances; + type Schedule = Schedule; + type CallStack = [pallet_contracts::Frame; 5]; +} +"# + ); + // pop build --release let mut command = pop(&working_dir, &["build", "--release"]); assert!(command.spawn()?.wait()?.success()); diff --git a/crates/pop-common/Cargo.toml b/crates/pop-common/Cargo.toml index fa156664e..27cc86b42 100644 --- a/crates/pop-common/Cargo.toml +++ b/crates/pop-common/Cargo.toml @@ -39,3 +39,4 @@ url.workspace = true [dev-dependencies] mockito.workspace = true tempfile.workspace = true +similar.workspace = true diff --git a/crates/pop-common/src/errors.rs b/crates/pop-common/src/errors.rs index ed28883ad..8729f70a3 100644 --- a/crates/pop-common/src/errors.rs +++ b/crates/pop-common/src/errors.rs @@ -45,6 +45,9 @@ pub enum Error { /// An error occurred while executing a test command. #[error("Failed to execute test command: {0}")] TestCommand(String), + /// An error coming from the toml_edit crate + #[error("toml_edit: {0}")] + TomlEdit(#[from] toml_edit::TomlError), /// The command is unsupported. #[error("Unsupported command: {0}")] UnsupportedCommand(String), diff --git a/crates/pop-common/src/helpers.rs b/crates/pop-common/src/helpers.rs index 369b15bd2..77db26419 100644 --- a/crates/pop-common/src/helpers.rs +++ b/crates/pop-common/src/helpers.rs @@ -44,15 +44,18 @@ pub fn get_project_name_from_path<'a>(path: &'a Path, default: &'a str) -> &'a s /// /// # Arguments /// * `path` - The path to be prefixed if needed. -pub fn prefix_with_current_dir_if_needed(path: PathBuf) -> PathBuf { - let components = &path.components().collect::>(); - if !components.is_empty() { - // If the first component is a normal component, we prefix the path with the current dir - if let Component::Normal(_) = components[0] { - return as AsRef>::as_ref(&Component::CurDir).join(path); +pub fn prefix_with_current_dir_if_needed>(path: P) -> PathBuf { + fn do_prefix_with_current_dir(path: &Path) -> PathBuf { + let components = path.components().collect::>(); + if !components.is_empty() { + // If the first component is a normal component, we prefix the path with the current dir + if let Component::Normal(_) = components[0] { + return as AsRef>::as_ref(&Component::CurDir).join(path); + } } + path.to_path_buf() } - path + do_prefix_with_current_dir(path.as_ref()) } /// Returns the relative path from `base` to `full` if `full` is inside `base`. diff --git a/crates/pop-common/src/manifest.rs b/crates/pop-common/src/manifest.rs index a45dc10a1..d37647b53 100644 --- a/crates/pop-common/src/manifest.rs +++ b/crates/pop-common/src/manifest.rs @@ -157,9 +157,71 @@ pub fn add_feature(project: &Path, (key, items): (String, Vec)) -> anyho Ok(()) } +/// Add pallet features (std, runtime-benchmarks, try-runtime) to manifest. +pub fn add_pallet_features_to_manifest>( + manifest_path: P, + pallet_crate_name: String, +) -> Result<(), Error> { + fn do_add_pallet_features_to_manifest( + manifest_path: &Path, + pallet_crate_name: String, + ) -> Result<(), Error> { + let cargo_toml_content = std::fs::read_to_string(manifest_path)?; + let mut doc = cargo_toml_content.parse::()?; + + if !doc.as_table().contains_key("features") { + return Err(Error::Config( + "The runtime manifest does not contain a [features] section".to_owned(), + )); + } + + let features_to_add = vec![ + ("std", format!("{}/std", pallet_crate_name)), + ("runtime-benchmarks", format!("{}/runtime-benchmarks", pallet_crate_name)), + ("try-runtime", format!("{}/try-runtime", pallet_crate_name)), + ]; + + let features_table = + doc.get_mut("features").and_then(|item| item.as_table_mut()).ok_or_else(|| { + Error::Config( + "The runtime manifest does not contain a valid [features] table".to_owned(), + ) + })?; + + for (feature, dep_feature) in features_to_add { + let feature_item = features_table.get_mut(feature).ok_or_else(|| { + Error::Config(format!( + "Feature `{}` does not exist in the runtime manifest", + feature + )) + })?; + + if feature_item.is_array() { + let array = feature_item + .as_array_mut() + .expect("feature_item is an array, so as_array_mut is always some; qed"); + if !array.iter().any(|v| v.as_str() == Some(&dep_feature)) { + array.push(dep_feature); + } + } else { + return Err(Error::Config(format!( + "Feature `{}` is not an array in the runtime manifest", + feature + ))); + } + } + + std::fs::write(manifest_path, doc.to_string())?; + Ok(()) + } + + do_add_pallet_features_to_manifest(manifest_path.as_ref(), pallet_crate_name) +} + #[cfg(test)] mod tests { use super::*; + use similar::{ChangeTag, TextDiff}; use std::fs::{write, File}; use tempfile::TempDir; @@ -561,4 +623,161 @@ mod tests { read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable"); assert_eq!(initial_toml_content, final_toml_content); } + + #[test] + fn add_pallet_features_to_manifest_works() { + let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml( + r#"[workspace] +resolver = "2" +members = ["member1"] + +[features] +default = ["std"] + +std = [ +"pallet-balances/std", +"pallet-aura/std" +] + +runtime-benchmarks = [ +"pallet-balances/runtime-benchmarks", +"pallet-aura/runtime-benchmarks" +] + +try-runtime = [ +"pallet-balances/try-runtime", +"pallet-aura/try-runtime" +] +"#, + ); + + let manifest_path = test_builder.workspace_cargo_toml.clone().unwrap(); + + let manifest_content_before = std::fs::read_to_string(&manifest_path).unwrap(); + + assert!( + add_pallet_features_to_manifest(&manifest_path, "pallet-contracts".to_owned()).is_ok() + ); + + let manifest_content_after = std::fs::read_to_string(&manifest_path).unwrap(); + + let manifest_diff = TextDiff::from_lines(&manifest_content_before, &manifest_content_after); + + let expected_inserted_lines = vec![ + ", \"pallet-contracts/std\"]\n", + ", \"pallet-contracts/runtime-benchmarks\"]\n", + ", \"pallet-contracts/try-runtime\"]\n", + ]; + + let mut inserted_lines = Vec::with_capacity(3); + + for change in manifest_diff.iter_all_changes() { + match change.tag() { + ChangeTag::Insert => inserted_lines.push(change.value()), + _ => (), + } + } + + assert_eq!(inserted_lines, expected_inserted_lines); + } + + #[test] + fn add_pallet_features_to_manifest_missing_features_section_should_fail() { + let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml( + r#"[workspace] +resolver = "2" +members = ["member1"] + +[package] +name = "dummy" +version = "0.1.0" +"#, + ); + let manifest_path = test_builder.workspace_cargo_toml.clone().unwrap(); + + let res = add_pallet_features_to_manifest(&manifest_path, "pallet-contracts".to_owned()); + assert!( + res.is_err(), + "Expected error because the manifest does not contain a [features] section" + ); + let err_msg = format!("{}", res.unwrap_err()); + assert!( + err_msg.contains("does not contain a [features] section"), + "Error message did not contain the expected text, got: {}", + err_msg + ); + } + + #[test] + fn add_pallet_features_to_manifest_missing_feature_key_should_fail() { + // Here, we purposefully omit the "std" feature. + let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml( + r#"[workspace] +resolver = "2" +members = ["member1"] + +[features] +default = ["std"] + +runtime-benchmarks = [ + "pallet-balances/runtime-benchmarks", + "pallet-aura/runtime-benchmarks" +] + +try-runtime = [ + "pallet-balances/try-runtime", + "pallet-aura/try-runtime" +] +"#, + ); + let manifest_path = test_builder.workspace_cargo_toml.clone().unwrap(); + + let res = add_pallet_features_to_manifest(&manifest_path, "pallet-contracts".to_owned()); + assert!( + res.is_err(), + "Expected error because the 'std' feature key is missing in the manifest" + ); + let err_msg = format!("{}", res.unwrap_err()); + assert!( + err_msg.contains("Feature `std` does not exist"), + "Error message did not contain the expected text, got: {}", + err_msg + ); + } + + #[test] + fn add_pallet_features_to_manifest_non_array_feature_should_fail() { + // Here, the "std" feature is defined as a string instead of an array. + let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml( + r#"[workspace] +resolver = "2" +members = ["member1"] + +[features] +default = ["std"] + +std = "pallet-balances/std" + +runtime-benchmarks = [ + "pallet-balances/runtime-benchmarks", + "pallet-aura/runtime-benchmarks" +] + +try-runtime = [ + "pallet-balances/try-runtime", + "pallet-aura/try-runtime" +] +"#, + ); + let manifest_path = test_builder.workspace_cargo_toml.clone().unwrap(); + + let res = add_pallet_features_to_manifest(&manifest_path, "pallet-contracts".to_owned()); + assert!(res.is_err(), "Expected error because the 'std' feature is not an array"); + let err_msg = format!("{}", res.unwrap_err()); + assert!( + err_msg.contains("Feature `std` is not an array"), + "Error message did not contain the expected text, got: {}", + err_msg + ); + } } diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 6f1a65a53..82c7cfda8 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -53,7 +53,9 @@ pub use deployer_providers::{DeploymentProvider, SupportedChains}; pub use errors::Error; pub use indexmap::IndexSet; pub use new_pallet::{create_pallet_template, new_pallet_options::*, TemplatePalletConfig}; -pub use new_parachain::instantiate_template_dir; +pub use new_parachain::{ + instantiate_openzeppelin_template, instantiate_standard_template, instantiate_template_dir, +}; pub use relay::{clear_dmpq, RelayChain, Reserved}; pub use try_runtime::{ binary::*, parse, parse_try_state_string, run_try_runtime, shared_parameters::*, state, diff --git a/crates/pop-parachains/src/new_parachain.rs b/crates/pop-parachains/src/new_parachain.rs index 7d3ebd067..b5219e1fe 100644 --- a/crates/pop-parachains/src/new_parachain.rs +++ b/crates/pop-parachains/src/new_parachain.rs @@ -39,6 +39,7 @@ pub fn instantiate_template_dir( Ok(tag) } +/// Instantiate a standard template. pub fn instantiate_standard_template( template: &Parachain, target: &Path, @@ -79,6 +80,7 @@ pub fn instantiate_standard_template( Ok(tag) } +/// Instantiate open zeppelin template. pub fn instantiate_openzeppelin_template( template: &Parachain, target: &Path,