From 0a55dd7d4f17b3e836fac100347fce2f44530013 Mon Sep 17 00:00:00 2001 From: Peter White Date: Mon, 24 Jun 2024 14:13:06 -0600 Subject: [PATCH 1/2] feat(new): update templates in new contract with ContractType --- README.md | 2 +- crates/pop-cli/src/commands/new/contract.rs | 62 ++++++++++-- crates/pop-contracts/README.md | 2 +- crates/pop-contracts/src/lib.rs | 2 +- crates/pop-contracts/src/templates.rs | 106 +++++++++++++++++++- 5 files changed, 159 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3aecf433..67ab5e39 100644 --- a/README.md +++ b/README.md @@ -309,7 +309,7 @@ cargo test Pop CLI would not be possible without these awesome crates! - Local network deployment powered by [zombienet-sdk](https://github.com/paritytech/zombienet-sdk) -- [cargo contract](https://github.com/paritytech/cargo-contract) a setup and deployment tool for developing Wasm based +- [cargo contract](https://github.com/use-ink/cargo-contract) a setup and deployment tool for developing Wasm based Smart Contracts via ink! ## License diff --git a/crates/pop-cli/src/commands/new/contract.rs b/crates/pop-cli/src/commands/new/contract.rs index ab151d00..dd2d8dac 100644 --- a/crates/pop-cli/src/commands/new/contract.rs +++ b/crates/pop-cli/src/commands/new/contract.rs @@ -1,13 +1,14 @@ // SPDX-License-Identifier: GPL-3.0 use crate::style::Theme; +use anyhow::Result; use clap::{ builder::{PossibleValue, PossibleValuesParser, TypedValueParser}, Args, }; use cliclack::{clear_screen, confirm, input, intro, log::success, outro, outro_cancel, set_theme}; use console::style; -use pop_contracts::{create_smart_contract, is_valid_contract_name, Template}; +use pop_contracts::{create_smart_contract, is_valid_contract_name, ContractType, Template}; use std::{env::current_dir, fs, path::PathBuf, str::FromStr}; use strum::VariantArray; @@ -15,6 +16,12 @@ use strum::VariantArray; pub struct NewContractCommand { #[arg(help = "Name of the contract")] pub(crate) name: Option, + #[arg( + help = "Contract type.", + default_value = ContractType::Examples.as_ref(), + value_parser = crate::enum_variants!(ContractType) + )] + pub(crate) contract_type: Option, #[arg(short = 'p', long, help = "Path for the contract project, [default: current directory]")] pub(crate) path: Option, #[arg( @@ -42,15 +49,29 @@ impl NewContractCommand { .clone() .expect("name can not be none as fallback above is interactive input; qed"); is_valid_contract_name(name)?; + let contract_type = &contract_config.contract_type.clone().unwrap_or_default(); let template = match &contract_config.template { Some(template) => template.clone(), - None => Template::Standard, // Default template + None => contract_type.default_type(), // Default contract type }; - generate_contract_from_template(name, contract_config.path, &template)?; + + is_template_supported(contract_type, &template)?; + + generate_contract_from_template(name, contract_config.path, &contract_type, &template)?; Ok(()) } } +fn is_template_supported(contract_type: &ContractType, template: &Template) -> Result<()> { + if !template.matches(contract_type) { + return Err(anyhow::anyhow!(format!( + "The contract type \"{:?}\" doesn't support the {:?} template.", + contract_type, template + ))); + }; + return Ok(()); +} + async fn guide_user_to_generate_contract() -> anyhow::Result { intro(format!("{}: Generate a contract", style(" Pop CLI ").black().on_magenta()))?; let name: String = input("Name of your contract?") @@ -61,25 +82,50 @@ async fn guide_user_to_generate_contract() -> anyhow::Result .placeholder("./") .default_input("./") .interact()?; - let mut prompt = cliclack::select("Select a template provider: ".to_string()); - for (i, template) in Template::templates().iter().enumerate() { + + let mut contract_type_prompt = cliclack::select("Select a contract type: ".to_string()); + for (i, contract_type) in ContractType::types().iter().enumerate() { if i == 0 { - prompt = prompt.initial_value(template); + contract_type_prompt = contract_type_prompt.initial_value(contract_type); } - prompt = prompt.item(template, template.name(), format!("{}", template.description(),)); + contract_type_prompt = contract_type_prompt.item( + contract_type, + contract_type.name(), + format!( + "{} {} available option(s)", + contract_type.description(), + contract_type.templates().len(), + ), + ); } - let template = prompt.interact()?; + let contract_type = contract_type_prompt.interact()?; + + let template = display_select_options(contract_type)?; + clear_screen()?; Ok(NewContractCommand { name: Some(name), path: Some(PathBuf::from(path)), + contract_type: Some(contract_type.clone()), template: Some(template.clone()), }) } +fn display_select_options(contract_type: &ContractType) -> Result<&Template> { + let mut prompt = cliclack::select("Select the contract:".to_string()); + for (i, template) in contract_type.templates().into_iter().enumerate() { + if i == 0 { + prompt = prompt.initial_value(template); + } + prompt = prompt.item(template, template.name(), template.description()); + } + Ok(prompt.interact()?) +} + fn generate_contract_from_template( name: &String, path: Option, + contract_type: &ContractType, template: &Template, ) -> anyhow::Result<()> { intro(format!( diff --git a/crates/pop-contracts/README.md b/crates/pop-contracts/README.md index f6b68111..a53c8080 100644 --- a/crates/pop-contracts/README.md +++ b/crates/pop-contracts/README.md @@ -12,7 +12,7 @@ let template = Template::Standard; let name = '...'; let contract_path = ...; -create_smart_contract(name, &contract_path, template)?; +create_smart_contract(name, &contract_path, &template)?; ``` Build an existing Smart Contract: diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 41612d02..a6acb010 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -15,7 +15,7 @@ pub use call::{ call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, }; pub use new::{create_smart_contract, is_valid_contract_name}; -pub use templates::Template; +pub use templates::{ContractType, Template}; pub use test::{test_e2e_smart_contract, test_smart_contract}; pub use up::{ dry_run_gas_estimate_instantiate, instantiate_smart_contract, set_up_deployment, UpOpts, diff --git a/crates/pop-contracts/src/templates.rs b/crates/pop-contracts/src/templates.rs index 9f30d230..bba6bdad 100644 --- a/crates/pop-contracts/src/templates.rs +++ b/crates/pop-contracts/src/templates.rs @@ -6,6 +6,61 @@ use strum::{ }; use strum_macros::{AsRefStr, Display, EnumMessage, EnumProperty, EnumString, VariantArray}; +/// Supported template providers. +#[derive( + AsRefStr, Clone, Default, Debug, Display, EnumMessage, EnumString, Eq, PartialEq, VariantArray, +)] +pub enum ContractType { + #[default] + #[strum( + ascii_case_insensitive, + serialize = "examples", + message = "Examples", + detailed_message = "Contract examples for ink!." + )] + Examples, + #[strum( + ascii_case_insensitive, + serialize = "erc", + message = "ERC", + detailed_message = "ERC-based contracts in ink!." + )] + Erc, +} + +impl ContractType { + /// Get the list of providers supported. + pub fn types() -> &'static [ContractType] { + ContractType::VARIANTS + } + + /// Get provider's name. + pub fn name(&self) -> &str { + self.get_message().unwrap_or_default() + } + + /// Get the default template of the provider. + pub fn default_type(&self) -> Template { + match &self { + ContractType::Examples => Template::Standard, + ContractType::Erc => Template::ERC20, + } + } + + /// Get the providers detailed description message. + pub fn description(&self) -> &str { + self.get_detailed_message().unwrap_or_default() + } + + /// Get the list of templates of the provider. + pub fn templates(&self) -> Vec<&Template> { + Template::VARIANTS + .iter() + .filter(|t| t.get_str("ContractType") == Some(self.name())) + .collect() + } +} + #[derive( AsRefStr, Clone, @@ -26,7 +81,8 @@ pub enum Template { #[strum( serialize = "standard", message = "Standard", - detailed_message = "ink!'s 'Hello World': Flipper" + detailed_message = "ink!'s 'Hello World': Flipper", + props(ContractType = "Examples") )] Standard, /// The implementation of the ERC-20 standard in ink! @@ -34,7 +90,7 @@ pub enum Template { serialize = "erc20", message = "Erc20", detailed_message = "The implementation of the ERC-20 standard in ink!", - props(Repository = "https://github.com/use-ink/ink-examples") + props(ContractType = "ERC", Repository = "https://github.com/use-ink/ink-examples") )] ERC20, /// The implementation of the ERC-721 standard in ink! @@ -42,7 +98,7 @@ pub enum Template { serialize = "erc721", message = "Erc721", detailed_message = "The implementation of the ERC-721 standard in ink!", - props(Repository = "https://github.com/use-ink/ink-examples") + props(ContractType = "ERC", Repository = "https://github.com/use-ink/ink-examples") )] ERC721, /// The implementation of the ERC-1155 standard in ink! @@ -50,7 +106,7 @@ pub enum Template { serialize = "erc1155", message = "Erc1155", detailed_message = "The implementation of the ERC-1155 standard in ink!", - props(Repository = "https://github.com/use-ink/ink-examples") + props(ContractType = "ERC", Repository = "https://github.com/use-ink/ink-examples") )] ERC1155, } @@ -74,6 +130,12 @@ impl Template { pub fn templates() -> &'static [Template] { Template::VARIANTS } + + /// Check the template belongs to a `provider`. + pub fn matches(&self, contract_type: &ContractType) -> bool { + // Match explicitly on provider name (message) + self.get_str("ContractType") == Some(contract_type.name()) + } } #[cfg(test)] @@ -107,6 +169,20 @@ mod tests { ]) } + #[test] + fn test_is_template_correct() { + for template in Template::VARIANTS { + if matches!(template, Template::Standard) { + assert_eq!(template.matches(&ContractType::Examples), true); + assert_eq!(template.matches(&ContractType::Erc), false); + } + if matches!(template, Template::ERC20 | Template::ERC721 | Template::ERC1155) { + assert_eq!(template.matches(&ContractType::Examples), false); + assert_eq!(template.matches(&ContractType::Erc), true); + } + } + } + #[test] fn test_convert_string_to_template() { let template_names = templates_names(); @@ -143,4 +219,26 @@ mod tests { assert_eq!(template.description(), templates_description[template]); } } + + #[test] + fn test_default_template_of_type() { + let mut contract_type = ContractType::Examples; + assert_eq!(contract_type.default_type(), Template::Standard); + contract_type = ContractType::Erc; + assert_eq!(contract_type.default_type(), Template::ERC20); + } + + #[test] + fn test_templates_of_type() { + let mut provider = ContractType::Examples; + assert_eq!(provider.templates(), [&Template::Standard]); + provider = ContractType::Erc; + assert_eq!(provider.templates(), [&Template::ERC20, &Template::ERC721, &Template::ERC1155]); + } + + #[test] + fn test_convert_string_to_type() { + assert_eq!(ContractType::from_str("Examples").unwrap(), ContractType::Examples); + assert_eq!(ContractType::from_str("Erc").unwrap_or_default(), ContractType::Erc); + } } From c8fbe4818191a6f635401fd1b8ac32d7e1452fa1 Mon Sep 17 00:00:00 2001 From: Peter White Date: Mon, 24 Jun 2024 15:52:35 -0600 Subject: [PATCH 2/2] fix(new): shorter name and test fix --- crates/pop-cli/src/commands/new/contract.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/pop-cli/src/commands/new/contract.rs b/crates/pop-cli/src/commands/new/contract.rs index dd2d8dac..b22a6374 100644 --- a/crates/pop-cli/src/commands/new/contract.rs +++ b/crates/pop-cli/src/commands/new/contract.rs @@ -17,11 +17,13 @@ pub struct NewContractCommand { #[arg(help = "Name of the contract")] pub(crate) name: Option, #[arg( - help = "Contract type.", default_value = ContractType::Examples.as_ref(), + short = 'c', + long, + help = "Contract type.", value_parser = crate::enum_variants!(ContractType) )] - pub(crate) contract_type: Option, + pub(crate) c_type: Option, #[arg(short = 'p', long, help = "Path for the contract project, [default: current directory]")] pub(crate) path: Option, #[arg( @@ -49,7 +51,7 @@ impl NewContractCommand { .clone() .expect("name can not be none as fallback above is interactive input; qed"); is_valid_contract_name(name)?; - let contract_type = &contract_config.contract_type.clone().unwrap_or_default(); + let contract_type = &contract_config.c_type.clone().unwrap_or_default(); let template = match &contract_config.template { Some(template) => template.clone(), None => contract_type.default_type(), // Default contract type @@ -57,7 +59,7 @@ impl NewContractCommand { is_template_supported(contract_type, &template)?; - generate_contract_from_template(name, contract_config.path, &contract_type, &template)?; + generate_contract_from_template(name, contract_config.path, &template)?; Ok(()) } } @@ -106,7 +108,7 @@ async fn guide_user_to_generate_contract() -> anyhow::Result Ok(NewContractCommand { name: Some(name), path: Some(PathBuf::from(path)), - contract_type: Some(contract_type.clone()), + c_type: Some(contract_type.clone()), template: Some(template.clone()), }) } @@ -125,15 +127,15 @@ fn display_select_options(contract_type: &ContractType) -> Result<&Template> { fn generate_contract_from_template( name: &String, path: Option, - contract_type: &ContractType, template: &Template, ) -> anyhow::Result<()> { intro(format!( - "{}: Generating \"{}\" using a {} template!", + "{}: Generating \"{}\" using {}!", style(" Pop CLI ").black().on_magenta(), name, template.name(), ))?; + let contract_path = check_destination_path(path, name)?; fs::create_dir_all(contract_path.as_path())?; let spinner = cliclack::spinner(); @@ -220,6 +222,8 @@ mod tests { "new", "contract", "test_contract", + "-c", + "erc", "-p", &dir_path, "-t",