diff --git a/CHANGELOG.md b/CHANGELOG.md index 316b47608d..f71715fcc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ The minor version will be incremented upon a breaking change and the patch versi - ts: Add ability to access workspace programs independent of the casing used, e.g. `anchor.workspace.myProgram`, `anchor.workspace.MyProgram`... ([#2579](https://github.com/coral-xyz/anchor/pull/2579)). - spl: Export `mpl-token-metadata` crate ([#2583](https://github.com/coral-xyz/anchor/pull/2583)). - spl: Add `TokenRecordAccount` for pNFTs ([#2597](https://github.com/coral-xyz/anchor/pull/2597)). -- ts: Add support for unnamed(tuple) enum in accounts([#2601](https://github.com/coral-xyz/anchor/pull/2601)). +- ts: Add support for unnamed(tuple) enum in accounts ([#2601](https://github.com/coral-xyz/anchor/pull/2601)). +- cli: Add program template with multiple files for instructions, state... ([#2602](https://github.com/coral-xyz/anchor/pull/2602)). ### Fixes diff --git a/cli/src/lib.rs b/cli/src/lib.rs index aa60701c01..0094d847f9 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -20,6 +20,7 @@ use heck::{ToKebabCase, ToSnakeCase}; use regex::{Regex, RegexBuilder}; use reqwest::blocking::multipart::{Form, Part}; use reqwest::blocking::Client; +use rust_template::ProgramTemplate; use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value as JsonValue}; @@ -69,15 +70,23 @@ pub struct Opts { pub enum Command { /// Initializes a workspace. Init { + /// Workspace name name: String, + /// Use JavaScript instead of TypeScript #[clap(short, long)] javascript: bool, + /// Use Solidity instead of Rust #[clap(short, long)] solidity: bool, + /// Don't initialize git #[clap(long)] no_git: bool, + /// Use `jest` instead of `mocha` for tests #[clap(long)] jest: bool, + /// Rust program template to use + #[clap(value_enum, short, long, default_value = "single")] + template: ProgramTemplate, }, /// Builds the workspace. #[clap(name = "build", alias = "b")] @@ -207,9 +216,14 @@ pub enum Command { }, /// Creates a new program. New { + /// Program name + name: String, + /// Use Solidity instead of Rust #[clap(short, long)] solidity: bool, - name: String, + /// Rust program template to use + #[clap(value_enum, short, long, default_value = "single")] + template: ProgramTemplate, }, /// Commands for interacting with interface definitions. Idl { @@ -456,8 +470,21 @@ pub fn entry(opts: Opts) -> Result<()> { solidity, no_git, jest, - } => init(&opts.cfg_override, name, javascript, solidity, no_git, jest), - Command::New { solidity, name } => new(&opts.cfg_override, solidity, name), + template, + } => init( + &opts.cfg_override, + name, + javascript, + solidity, + no_git, + jest, + template, + ), + Command::New { + solidity, + name, + template, + } => new(&opts.cfg_override, solidity, name, template), Command::Build { idl, idl_ts, @@ -604,6 +631,7 @@ fn init( solidity: bool, no_git: bool, jest: bool, + template: ProgramTemplate, ) -> Result<()> { if Config::discover(cfg_override)?.is_some() { return Err(anyhow!("Workspace already initialized")); @@ -682,17 +710,11 @@ fn init( // Build the program. if solidity { - fs::create_dir("solidity")?; - - new_solidity_program(&project_name)?; + solidity_template::create_program(&project_name)?; } else { - // Build virtual manifest for rust programs - fs::write("Cargo.toml", rust_template::virtual_manifest())?; - - fs::create_dir("programs")?; - - new_rust_program(&project_name)?; + rust_template::create_program(&project_name, template)?; } + // Build the test suite. fs::create_dir("tests")?; // Build the migrations directory. @@ -783,7 +805,12 @@ fn install_node_modules(cmd: &str) -> Result { } // Creates a new program crate in the `programs/` directory. -fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()> { +fn new( + cfg_override: &ConfigOverride, + solidity: bool, + name: String, + template: ProgramTemplate, +) -> Result<()> { with_workspace(cfg_override, |cfg| { match cfg.path().parent() { None => { @@ -802,10 +829,10 @@ fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<() name.clone(), ProgramDeployment { address: if solidity { - new_solidity_program(&name)?; + solidity_template::create_program(&name)?; solidity_template::default_program_id() } else { - new_rust_program(&name)?; + rust_template::create_program(&name, template)?; rust_template::get_or_create_program_id(&name) }, path: None, @@ -823,26 +850,32 @@ fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<() }) } -// Creates a new rust program crate in the current directory with `name`. -fn new_rust_program(name: &str) -> Result<()> { - if !PathBuf::from("Cargo.toml").exists() { - fs::write("Cargo.toml", rust_template::virtual_manifest())?; +/// Array of (path, content) tuple. +pub type Files = Vec<(PathBuf, String)>; + +/// Create files from the given (path, content) tuple array. +/// +/// # Example +/// +/// ```ignore +/// crate_files(vec![("programs/my_program/src/lib.rs".into(), "// Content".into())])?; +/// ``` +pub fn create_files(files: &Files) -> Result<()> { + for (path, content) in files { + let path = Path::new(path); + if path.exists() { + continue; + } + + match path.extension() { + Some(_) => { + fs::create_dir_all(path.parent().unwrap())?; + fs::write(path, content)?; + } + None => fs::create_dir_all(path)?, + } } - fs::create_dir_all(format!("programs/{name}/src/"))?; - let mut cargo_toml = File::create(format!("programs/{name}/Cargo.toml"))?; - cargo_toml.write_all(rust_template::cargo_toml(name).as_bytes())?; - let mut xargo_toml = File::create(format!("programs/{name}/Xargo.toml"))?; - xargo_toml.write_all(rust_template::xargo_toml().as_bytes())?; - let mut lib_rs = File::create(format!("programs/{name}/src/lib.rs"))?; - lib_rs.write_all(rust_template::lib_rs(name).as_bytes())?; - Ok(()) -} -// Creates a new solidity program in the current directory with `name`. -fn new_solidity_program(name: &str) -> Result<()> { - fs::create_dir_all("solidity")?; - let mut lib_rs = File::create(format!("solidity/{name}.sol"))?; - lib_rs.write_all(solidity_template::solidity(name).as_bytes())?; Ok(()) } @@ -4224,6 +4257,7 @@ mod tests { false, false, false, + ProgramTemplate::default(), ) .unwrap(); } @@ -4241,6 +4275,7 @@ mod tests { false, false, false, + ProgramTemplate::default(), ) .unwrap(); } @@ -4258,6 +4293,7 @@ mod tests { false, false, false, + ProgramTemplate::default(), ) .unwrap(); } diff --git a/cli/src/rust_template.rs b/cli/src/rust_template.rs index ed4a840dcd..8ec63ee19a 100644 --- a/cli/src/rust_template.rs +++ b/cli/src/rust_template.rs @@ -1,7 +1,8 @@ -use crate::config::ProgramWorkspace; use crate::VERSION; +use crate::{config::ProgramWorkspace, create_files, Files}; use anchor_syn::idl::types::Idl; use anyhow::Result; +use clap::{Parser, ValueEnum}; use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase}; use solana_sdk::{ pubkey::Pubkey, @@ -10,22 +11,140 @@ use solana_sdk::{ }; use std::{fmt::Write, path::Path}; -/// Read the program keypair file or create a new one if it doesn't exist. -pub fn get_or_create_program_id(name: &str) -> Pubkey { - let keypair_path = Path::new("target") - .join("deploy") - .join(format!("{}-keypair.json", name.to_snake_case())); +/// Program initialization template +#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)] +pub enum ProgramTemplate { + /// Program with a single `lib.rs` file + #[default] + Single, + /// Program with multiple files for instructions, state... + Multiple, +} - read_keypair_file(&keypair_path) - .unwrap_or_else(|_| { - let keypair = Keypair::new(); - write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair"); - keypair - }) - .pubkey() +/// Create a program from the given name and template. +pub fn create_program(name: &str, template: ProgramTemplate) -> Result<()> { + let program_path = Path::new("programs").join(name); + let common_files = vec![ + ("Cargo.toml".into(), workspace_manifest().into()), + (program_path.join("Cargo.toml"), cargo_toml(name)), + (program_path.join("Xargo.toml"), xargo_toml().into()), + ]; + + let template_files = match template { + ProgramTemplate::Single => create_program_template_single(name, &program_path), + ProgramTemplate::Multiple => create_program_template_multiple(name, &program_path), + }; + + create_files(&[common_files, template_files].concat()) +} + +/// Create a program with a single `lib.rs` file. +fn create_program_template_single(name: &str, program_path: &Path) -> Files { + vec![( + program_path.join("src").join("lib.rs"), + format!( + r#"use anchor_lang::prelude::*; + +declare_id!("{}"); + +#[program] +pub mod {} {{ + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> {{ + Ok(()) + }} +}} + +#[derive(Accounts)] +pub struct Initialize {{}} +"#, + get_or_create_program_id(name), + name.to_snake_case(), + ), + )] +} + +/// Create a program with multiple files for instructions, state... +fn create_program_template_multiple(name: &str, program_path: &Path) -> Files { + let src_path = program_path.join("src"); + vec![ + ( + src_path.join("lib.rs"), + format!( + r#"pub mod constants; +pub mod error; +pub mod instructions; +pub mod state; + +use anchor_lang::prelude::*; + +pub use constants::*; +pub use instructions::*; +pub use state::*; + +declare_id!("{}"); + +#[program] +pub mod {} {{ + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> {{ + initialize::handler(ctx) + }} +}} +"#, + get_or_create_program_id(name), + name.to_snake_case(), + ), + ), + ( + src_path.join("constants.rs"), + r#"use anchor_lang::prelude::*; + +#[constant] +pub const SEED: &str = "anchor"; +"# + .into(), + ), + ( + src_path.join("error.rs"), + r#"use anchor_lang::prelude::*; + +#[error_code] +pub enum ErrorCode { + #[msg("Custom error message")] + CustomError, +} +"# + .into(), + ), + ( + src_path.join("instructions").join("mod.rs"), + r#"pub mod initialize; + +pub use initialize::*; +"# + .into(), + ), + ( + src_path.join("instructions").join("initialize.rs"), + r#"use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct Initialize {} + +pub fn handler(ctx: Context) -> Result<()> { + Ok(()) +} +"# + .into(), + ), + (src_path.join("state").join("mod.rs"), r#""#.into()), + ] } -pub fn virtual_manifest() -> &'static str { +const fn workspace_manifest() -> &'static str { r#"[workspace] members = [ "programs/*" @@ -42,33 +161,7 @@ codegen-units = 1 "# } -pub fn credentials(token: &str) -> String { - format!( - r#"[registry] -token = "{token}" -"# - ) -} - -pub fn idl_ts(idl: &Idl) -> Result { - let mut idl = idl.clone(); - for acc in idl.accounts.iter_mut() { - acc.name = acc.name.to_lower_camel_case(); - } - let idl_json = serde_json::to_string_pretty(&idl)?; - Ok(format!( - r#"export type {} = {}; - -export const IDL: {} = {}; -"#, - idl.name.to_upper_camel_case(), - idl_json, - idl.name.to_upper_camel_case(), - idl_json - )) -} - -pub fn cargo_toml(name: &str) -> String { +fn cargo_toml(name: &str) -> String { format!( r#"[package] name = "{0}" @@ -96,6 +189,53 @@ anchor-lang = "{2}" ) } +fn xargo_toml() -> &'static str { + r#"[target.bpfel-unknown-unknown.dependencies.std] +features = [] +"# +} + +/// Read the program keypair file or create a new one if it doesn't exist. +pub fn get_or_create_program_id(name: &str) -> Pubkey { + let keypair_path = Path::new("target") + .join("deploy") + .join(format!("{}-keypair.json", name.to_snake_case())); + + read_keypair_file(&keypair_path) + .unwrap_or_else(|_| { + let keypair = Keypair::new(); + write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair"); + keypair + }) + .pubkey() +} + +pub fn credentials(token: &str) -> String { + format!( + r#"[registry] +token = "{token}" +"# + ) +} + +pub fn idl_ts(idl: &Idl) -> Result { + let mut idl = idl.clone(); + for acc in idl.accounts.iter_mut() { + acc.name = acc.name.to_lower_camel_case(); + } + let idl_json = serde_json::to_string_pretty(&idl)?; + Ok(format!( + r#"export type {} = {}; + +export const IDL: {} = {}; +"#, + idl.name.to_upper_camel_case(), + idl_json, + idl.name.to_upper_camel_case(), + idl_json + )) +} + pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String { format!( r#" @@ -181,35 +321,6 @@ module.exports = async function (provider) { "# } -pub fn xargo_toml() -> &'static str { - r#"[target.bpfel-unknown-unknown.dependencies.std] -features = [] -"# -} - -pub fn lib_rs(name: &str) -> String { - format!( - r#"use anchor_lang::prelude::*; - -declare_id!("{}"); - -#[program] -pub mod {} {{ - use super::*; - - pub fn initialize(ctx: Context) -> Result<()> {{ - Ok(()) - }} -}} - -#[derive(Accounts)] -pub struct Initialize {{}} -"#, - get_or_create_program_id(name), - name.to_snake_case(), - ) -} - pub fn mocha(name: &str) -> String { format!( r#"const anchor = require("@coral-xyz/anchor"); diff --git a/cli/src/solidity_template.rs b/cli/src/solidity_template.rs index 48d81de9c7..f1211bda67 100644 --- a/cli/src/solidity_template.rs +++ b/cli/src/solidity_template.rs @@ -1,10 +1,20 @@ -use crate::config::ProgramWorkspace; use crate::VERSION; +use crate::{config::ProgramWorkspace, create_files}; use anchor_syn::idl::types::Idl; use anyhow::Result; use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase}; use solana_sdk::pubkey::Pubkey; use std::fmt::Write; +use std::path::Path; + +/// Create a solidity program. +pub fn create_program(name: &str) -> Result<()> { + let files = vec![( + Path::new("solidity").join(name).with_extension("sol"), + solidity(name), + )]; + create_files(&files) +} pub fn default_program_id() -> Pubkey { "F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC"