diff --git a/Cargo.lock b/Cargo.lock index 122140683..1245ed39d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,38 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.22", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cbc" version = "0.1.2" @@ -947,12 +979,32 @@ dependencies = [ "cc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -993,6 +1045,54 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "contract-build" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1c9e0b024481d35d46e1043323ec8c1dc8b57f4a08c4ee5392c2aefb75859b" +dependencies = [ + "anyhow", + "blake2", + "cargo_metadata", + "clap", + "colored", + "contract-metadata", + "duct", + "heck", + "hex", + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "rustc_version 0.4.0", + "semver 1.0.22", + "serde", + "serde_json", + "strum 0.24.1", + "tempfile", + "term_size", + "toml", + "tracing", + "url", + "walkdir", + "wasm-opt", + "which", + "zip", +] + +[[package]] +name = "contract-metadata" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a88f62795e84270742796456086ddeebfa4cbd4e56f02777f792192d666725" +dependencies = [ + "anyhow", + "impl-serde", + "semver 1.0.22", + "serde", + "serde_json", + "url", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -1187,6 +1287,50 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "cxx" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2673ca5ae28334544ec2a6b18ebe666c42a2650abfb48abbd532ed409a44be2b" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df46fe0eb43066a332586114174c449a62c25689f85a08f28fdcc8e12c380b9" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.52", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "886acf875df67811c11cd015506b3392b9e1820b1627af1a6f4e93ccdfc74d11" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d151cc139c3080e07f448f93a1284577ab2283d2a44acd902c6fba9ec20b6de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "darling" version = "0.14.4" @@ -3319,6 +3463,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3887,6 +4040,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "parity-wasm" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" + [[package]] name = "parking" version = "2.2.0" @@ -4236,6 +4395,7 @@ dependencies = [ "clap", "cliclack", "console", + "contract-build", "dirs", "duct", "git2", @@ -5061,6 +5221,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + [[package]] name = "sct" version = "0.7.1" @@ -5168,6 +5334,9 @@ name = "semver" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -5882,6 +6051,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + [[package]] name = "strum" version = "0.25.0" @@ -5897,6 +6075,19 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "strum_macros" version = "0.25.3" @@ -6499,6 +6690,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "text_lines" version = "0.6.0" @@ -7339,6 +7549,46 @@ version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +[[package]] +name = "wasm-opt" +version = "0.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65a2799e08026234b07b44da6363703974e75be21430cef00756bbc438c8ff8a" +dependencies = [ + "anyhow", + "libc", + "strum 0.24.1", + "strum_macros 0.24.3", + "tempfile", + "thiserror", + "wasm-opt-cxx-sys", + "wasm-opt-sys", +] + +[[package]] +name = "wasm-opt-cxx-sys" +version = "0.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d26f86d1132245e8bcea8fac7f02b10fb885b6696799969c94d7d3c14db5e1" +dependencies = [ + "anyhow", + "cxx", + "cxx-build", + "wasm-opt-sys", +] + +[[package]] +name = "wasm-opt-sys" +version = "0.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497d069cd3420cdd52154a320b901114a20946878e2de62c670f9d906e472370" +dependencies = [ + "anyhow", + "cc", + "cxx", + "cxx-build", +] + [[package]] name = "wasm-streams" version = "0.4.0" @@ -7912,6 +8162,17 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", +] + [[package]] name = "zombienet-configuration" version = "0.1.0-alpha.1" diff --git a/Cargo.toml b/Cargo.toml index 509ec3472..ff75d5697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } walkdir = "2.4" # contracts +contract-build = { version = "3.2.0", optional = true } # parachains dirs = { version = "5.0", optional = true } @@ -42,9 +43,12 @@ url = { version = "2.5", optional = true } zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk", optional = true } zombienet-support = { git = "https://github.com/paritytech/zombienet-sdk", optional = true } + [features] default = ["contract", "parachain"] -contract = [] +contract = [ + "dep:contract-build" +] parachain = [ "dep:dirs", "dep:indexmap", @@ -56,4 +60,4 @@ parachain = [ "dep:url", "dep:zombienet-sdk", "dep:zombienet-support" -] \ No newline at end of file +] diff --git a/README.md b/README.md index 7d274b8a8..4cc73ec11 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Your one-stop entry into the exciting world of Blockchain development with *Polk ## Getting Started +### Parachains Use `pop` to either clone of your existing templates or instantiate a new parachain template: ```sh @@ -43,3 +44,39 @@ cd my-app cargo build --release ``` For running any parachain, we recommend using [zombienet](https://github.com/paritytech/zombienet). + + +### Contracts +Use `pop` to create a smart contract template: + +```sh +# Create a minimal smart contract template +pop new contract my_contract +``` + +Test the smart contract: +```sh +# Test an existing smart contract +pop test contract -p ./my_contract +``` + +Build the smart contract: +```sh +# Build an existing smart contract +pop build contract -p ./my_contract +``` + +### Build locally + +Build the tool locally with all the features: +```sh +cargo build --all-features +``` +Build the tool only for parachain functionality: +```sh +cargo build --features parachain +``` +Build the tool only for contracts functionality: +```sh +cargo build --features contract +``` \ No newline at end of file diff --git a/src/commands/build/contract.rs b/src/commands/build/contract.rs new file mode 100644 index 000000000..f1e56be96 --- /dev/null +++ b/src/commands/build/contract.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; + +use clap::Args; +use cliclack::log; + +use crate::engines::contract_engine::build_smart_contract; + +#[derive(Args)] +pub struct BuildContractCommand { + #[arg(short = 'p', long = "path", help = "Path for the contract project, [default: current directory]")] + pub(crate) path: Option, +} + +impl BuildContractCommand { + pub(crate) fn execute(&self) -> anyhow::Result<()> { + build_smart_contract(&self.path)?; + log::info("The smart contract has been successfully built.")?; + Ok(()) + } +} diff --git a/src/commands/build/mod.rs b/src/commands/build/mod.rs new file mode 100644 index 000000000..b758fc7d5 --- /dev/null +++ b/src/commands/build/mod.rs @@ -0,0 +1,19 @@ +use clap::{Args, Subcommand}; + +pub mod contract; + +#[derive(Args)] +#[command(args_conflicts_with_subcommands = true)] +pub(crate) struct BuildArgs { + #[command(subcommand)] + pub command: BuildCommands, +} + +#[derive(Subcommand)] +pub(crate) enum BuildCommands { + /// Compiles the contract, generates metadata, bundles both together in a + /// `.contract` file + #[cfg(feature = "contract")] + #[clap(alias = "c")] + Contract(contract::BuildContractCommand), +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a301bb8ec..1bc59b6c6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,2 +1,4 @@ pub(crate) mod new; +pub(crate) mod build; pub(crate) mod up; +pub(crate) mod test; diff --git a/src/commands/new/contract.rs b/src/commands/new/contract.rs new file mode 100644 index 000000000..83a0bd65c --- /dev/null +++ b/src/commands/new/contract.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +use clap::Args; +use cliclack::log; + +use crate::engines::contract_engine::create_smart_contract; + +#[derive(Args)] +pub struct NewContractCommand { + #[arg(help = "Name of the contract")] + pub(crate) name: String, + #[arg(short = 'p', long = "path", help = "Path for the contract project, [default: current directory]")] + pub(crate) path: Option, +} + +impl NewContractCommand { + pub(crate) fn execute(&self) -> anyhow::Result<()> { + create_smart_contract(self.name.clone(), &self.path)?; + log::info(format!( + "Smart contract created. Move to the dir {:?}", + self.path.clone().unwrap_or(PathBuf::from(format!("{}", self.name))).display() + ))?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_new_contract_command_execute() -> anyhow::Result<()> { + let command = NewContractCommand { + name: "test_contract".to_string(), + path: Some(PathBuf::new()) + }; + let result = command.execute(); + assert!(result.is_ok()); + + // Clean up + if let Err(err) = fs::remove_dir_all("test_contract") { + eprintln!("Failed to delete directory: {}", err); + } + Ok(()) + } +} diff --git a/src/commands/new/mod.rs b/src/commands/new/mod.rs index a8d234c10..f6ee1a503 100644 --- a/src/commands/new/mod.rs +++ b/src/commands/new/mod.rs @@ -1,5 +1,7 @@ use clap::{Args, Subcommand}; +#[cfg(feature = "contract")] +pub mod contract; #[cfg(feature = "parachain")] pub mod pallet; #[cfg(feature = "parachain")] @@ -16,8 +18,14 @@ pub struct NewArgs { pub enum NewCommands { /// Generate a new parachain template #[cfg(feature = "parachain")] + #[clap(alias = "p")] Parachain(parachain::NewParachainCommand), /// Generate a new pallet template #[cfg(feature = "parachain")] + #[clap(alias = "m")] // (m)odule, as p used above Pallet(pallet::NewPalletCommand), + /// Generate a new smart contract template + #[cfg(feature = "contract")] + #[clap(alias = "c")] + Contract(contract::NewContractCommand), } diff --git a/src/commands/test/contract.rs b/src/commands/test/contract.rs new file mode 100644 index 000000000..0419e8492 --- /dev/null +++ b/src/commands/test/contract.rs @@ -0,0 +1,27 @@ +use std::path::PathBuf; + +use clap::Args; +use cliclack::{clear_screen,intro}; + +use crate::style::style; +use crate::engines::contract_engine::test_smart_contract; + +#[derive(Args)] +pub(crate) struct TestContractCommand { + #[arg(short = 'p', long = "path", help = "Path for the contract project [default: current directory]")] + path: Option, +} + +impl TestContractCommand { + pub(crate) fn execute(&self) -> anyhow::Result<()> { + clear_screen()?; + intro(format!( + "{}: Starting unit tests", + style(" Pop CLI ").black().on_magenta() + ))?; + test_smart_contract(&self.path)?; + + Ok(()) + } +} + diff --git a/src/commands/test/mod.rs b/src/commands/test/mod.rs new file mode 100644 index 000000000..49309cf34 --- /dev/null +++ b/src/commands/test/mod.rs @@ -0,0 +1,18 @@ +use clap::{Args, Subcommand}; + +pub mod contract; + +#[derive(Args)] +#[command(args_conflicts_with_subcommands = true)] +pub(crate) struct TestArgs { + #[command(subcommand)] + pub command: TestCommands, +} + +#[derive(Subcommand)] +pub(crate) enum TestCommands { + /// Test the contract + #[cfg(feature = "contract")] + #[clap(alias = "c")] + Contract(contract::TestContractCommand), +} diff --git a/src/commands/up/mod.rs b/src/commands/up/mod.rs index 83876bef7..086a7d41b 100644 --- a/src/commands/up/mod.rs +++ b/src/commands/up/mod.rs @@ -14,5 +14,6 @@ pub(crate) struct UpArgs { pub(crate) enum UpCommands { #[cfg(feature = "parachain")] /// Deploy a parachain to a network. + #[clap(alias = "p")] Parachain(parachain::ZombienetCommand), } diff --git a/src/engines/contract_engine.rs b/src/engines/contract_engine.rs new file mode 100644 index 000000000..f01010fe7 --- /dev/null +++ b/src/engines/contract_engine.rs @@ -0,0 +1,78 @@ +use std::path::PathBuf; +use duct::cmd; + +use contract_build::{ + new_contract_project, execute, + ExecuteArgs, ManifestPath,Verbosity, BuildMode,Features,Network,BuildArtifacts, UnstableFlags, OptimizationPasses, OutputType, Target, +}; + +pub fn create_smart_contract(name: String, target: &Option) -> anyhow::Result<()> { + new_contract_project(&name, target.as_ref()) +} + +pub fn build_smart_contract(path: &Option) -> anyhow::Result<()> { + // If the user specify a path (not current directory) have to manually add Cargo.toml here or ask to the user the specific path + let manifest_path ; + if path.is_some(){ + let full_path: PathBuf = PathBuf::from(path.as_ref().unwrap().to_string_lossy().to_string() + "/Cargo.toml"); + manifest_path = ManifestPath::try_from(Some(full_path))?; + } + else { + manifest_path = ManifestPath::try_from(path.as_ref())?; + } + + let args = ExecuteArgs { + manifest_path, + verbosity: Verbosity::Default, + build_mode: BuildMode::Release, + features: Features::default(), + network: Network::Online, + build_artifact: BuildArtifacts::All, + unstable_flags: UnstableFlags::default(), + optimization_passes: Some(OptimizationPasses::default()), + keep_debug_symbols: false, + lint: false, + output_type: OutputType::Json, + skip_wasm_validation: false, + target: Target::Wasm, + }; + execute(args)?; + Ok(()) +} + + +pub fn test_smart_contract(path: &Option) -> anyhow::Result<()> { + cmd( + "cargo", + vec![ + "test", + ], + ) + .dir(path.clone().unwrap_or("./".into())) + .run()?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir; + use std::fs; + + #[test] + fn test_create_smart_contract() -> Result<(), Box> { + let temp_dir = tempdir::TempDir::new("test_folder")?; + let result: anyhow::Result<()> = create_smart_contract("test".to_string(),&Some(PathBuf::from(temp_dir.path()))); + assert!(result.is_ok()); + + // Verify that the generated smart contract contains the expected content + let generated_file_content = + fs::read_to_string(temp_dir.path().join("test/lib.rs"))?; + + assert!(generated_file_content.contains("#[ink::contract]")); + assert!(generated_file_content.contains("mod test {")); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/engines/mod.rs b/src/engines/mod.rs index dae0d91fa..0b9df1fc8 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -1,3 +1,4 @@ pub mod parachain_engine; pub mod pallet_engine; +pub mod contract_engine; pub mod generator; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9358f64f7..a04408f72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,16 @@ +#[cfg(any(feature = "parachain", feature = "contract"))] mod commands; -#[cfg(feature = "parachain")] +#[cfg(any(feature = "parachain", feature = "contract"))] mod engines; +#[cfg(any(feature = "parachain", feature = "contract"))] +mod style; + #[cfg(feature = "parachain")] mod git; #[cfg(feature = "parachain")] mod helpers; #[cfg(feature = "parachain")] mod parachains; -mod style; use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; @@ -23,9 +26,18 @@ pub struct Cli { #[derive(Subcommand)] #[command(subcommand_required = true)] enum Commands { + /// Build a parachain, a pallet or smart contract. + #[clap(alias = "n")] New(commands::new::NewArgs), + /// Compile a parachain or smart contract. + #[clap(alias = "b")] + Build(commands::build::BuildArgs), /// Deploy a parachain or smart contract. + #[clap(alias = "u")] Up(commands::up::UpArgs), + /// Test a smart contract. + #[clap(alias = "t")] + Test(commands::test::TestArgs), } #[tokio::main] @@ -37,11 +49,21 @@ async fn main() -> Result<()> { commands::new::NewCommands::Parachain(cmd) => cmd.execute(), #[cfg(feature = "parachain")] commands::new::NewCommands::Pallet(cmd) => cmd.execute(), + #[cfg(feature = "contract")] + commands::new::NewCommands::Contract(cmd) => cmd.execute(), + }, + Commands::Build(args) => match &args.command { + #[cfg(feature = "contract")] + commands::build::BuildCommands::Contract(cmd) => cmd.execute(), }, Commands::Up(args) => Ok(match &args.command { #[cfg(feature = "parachain")] commands::up::UpCommands::Parachain(cmd) => cmd.execute().await?, }), + Commands::Test(args) => match &args.command { + #[cfg(feature = "contract")] + commands::test::TestCommands::Contract(cmd) => cmd.execute(), + }, } }