diff --git a/cli/src/component.rs b/cli/src/component.rs new file mode 100644 index 0000000..ba816b7 --- /dev/null +++ b/cli/src/component.rs @@ -0,0 +1,113 @@ +use crate::{ + discover_cluster_url, + rust_template::create_component, + templates::component::{component_type, component_type_import}, + workspace::with_workspace, +}; +use anchor_cli::config::{ConfigOverride, ProgramDeployment}; +use anchor_lang_idl::types::Idl; +use anyhow::{anyhow, Result}; +use std::{ + fs::{self, File, OpenOptions}, + io::Write, + path::Path, + process::Stdio, +}; + +// Create a new component from the template +pub fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> { + with_workspace(cfg_override, |cfg| { + match cfg.path().parent() { + None => { + println!("Unable to make new component"); + } + Some(parent) => { + std::env::set_current_dir(parent)?; + + let cluster = cfg.provider.cluster.clone(); + let programs = cfg.programs.entry(cluster).or_default(); + if programs.contains_key(&name) { + return Err(anyhow!("Program already exists")); + } + + programs.insert( + name.clone(), + ProgramDeployment { + address: { + create_component(&name)?; + anchor_cli::rust_template::get_or_create_program_id(&name) + }, + path: None, + idl: None, + }, + ); + + let toml = cfg.to_string(); + fs::write("Anchor.toml", toml)?; + + println!("Created new component: {}", name); + } + }; + Ok(()) + }) +} + +pub fn extract_component_id(line: &str) -> Option<&str> { + let component_id_marker = "#[component_id("; + line.find(component_id_marker).map(|start| { + let start = start + component_id_marker.len(); + let end = line[start..].find(')').unwrap() + start; + line[start..end].trim_matches('"') + }) +} + +pub fn fetch_idl_for_component(component_id: &str, url: &str) -> Result { + let output = std::process::Command::new("bolt") + .arg("idl") + .arg("fetch") + .arg(component_id) + .arg("--provider.cluster") + .arg(url) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output()?; + + if output.status.success() { + let idl_string = String::from_utf8(output.stdout) + .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))? + .to_string(); + Ok(idl_string) + } else { + let error_message = String::from_utf8(output.stderr) + .unwrap_or(format!( + "Error trying to dynamically generate the type \ + for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \ + the appropriate cluster using the --provider.cluster option", + component_id + )) + .to_string(); + Err(anyhow!("Command failed with error: {}", error_message)) + } +} + +pub fn generate_component_type_file( + file_path: &Path, + cfg_override: &ConfigOverride, + component_id: &str, +) -> Result<()> { + let url = discover_cluster_url(cfg_override)?; + let idl_string = fetch_idl_for_component(component_id, &url)?; + let idl: Idl = serde_json::from_str(&idl_string)?; + let mut file = File::create(file_path)?; + file.write_all(component_type(&idl, component_id)?.as_bytes())?; + Ok(()) +} + +pub fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> { + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(lib_rs_path)?; + file.write_all(component_type_import(component_id).as_bytes())?; + Ok(()) +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index fe86ff1..cac50c6 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,18 +1,24 @@ +mod component; mod rust_template; +mod system; +mod templates; +mod workspace; +use crate::component::new_component; use crate::rust_template::{create_component, create_system}; +use crate::system::new_system; use anchor_cli::config; use anchor_cli::config::{ BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment, TestValidator, Validator, WithPath, }; use anchor_client::Cluster; -use anchor_lang_idl::types::Idl; use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; +use component::{append_component_to_lib_rs, extract_component_id, generate_component_type_file}; use heck::{ToKebabCase, ToSnakeCase}; use std::collections::BTreeMap; -use std::fs::{self, create_dir_all, File, OpenOptions}; +use std::fs::{self, create_dir_all, File}; use std::io::Write; use std::io::{self, BufRead}; use std::path::{Path, PathBuf}; @@ -23,7 +29,7 @@ pub const ANCHOR_VERSION: &str = anchor_cli::VERSION; pub const WORLD_PROGRAM: &str = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"; -#[derive(Debug, Subcommand)] +#[derive(Subcommand)] pub enum BoltCommand { #[clap(about = "Create a new component")] Component(ComponentCommand), @@ -50,7 +56,7 @@ pub struct SystemCommand { pub name: String, } -#[derive(Debug, Parser)] +#[derive(Parser)] #[clap(version = VERSION)] pub struct Opts { /// Rebuild the auto-generated types @@ -275,10 +281,10 @@ fn init( fs::write("Anchor.toml", toml)?; // Initialize .gitignore file - fs::write(".gitignore", rust_template::git_ignore())?; + fs::write(".gitignore", templates::workspace::git_ignore())?; // Initialize .prettierignore file - fs::write(".prettierignore", rust_template::prettier_ignore())?; + fs::write(".prettierignore", templates::workspace::prettier_ignore())?; // Remove the default programs if `--force` is passed if force { @@ -349,21 +355,21 @@ fn init( if javascript { // Build javascript config let mut package_json = File::create("package.json")?; - package_json.write_all(rust_template::package_json(jest).as_bytes())?; + package_json.write_all(templates::workspace::package_json(jest).as_bytes())?; if jest { let mut test = File::create(format!("tests/{}.test.js", &project_name))?; if solidity { test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?; } else { - test.write_all(rust_template::jest(&project_name).as_bytes())?; + test.write_all(templates::workspace::jest(&project_name).as_bytes())?; } } else { let mut test = File::create(format!("tests/{}.js", &project_name))?; if solidity { test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?; } else { - test.write_all(rust_template::mocha(&project_name).as_bytes())?; + test.write_all(templates::workspace::mocha(&project_name).as_bytes())?; } } @@ -376,7 +382,7 @@ fn init( ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?; let mut ts_package_json = File::create("package.json")?; - ts_package_json.write_all(rust_template::ts_package_json(jest).as_bytes())?; + ts_package_json.write_all(templates::workspace::ts_package_json(jest).as_bytes())?; let mut deploy = File::create("migrations/deploy.ts")?; deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?; @@ -385,7 +391,7 @@ fn init( if solidity { mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?; } else { - mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?; + mocha.write_all(templates::workspace::ts_mocha(&project_name).as_bytes())?; } } @@ -488,134 +494,6 @@ fn install_node_modules(cmd: &str) -> Result { .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string())) } -// Create a new component from the template -fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> { - with_workspace(cfg_override, |cfg| { - match cfg.path().parent() { - None => { - println!("Unable to make new component"); - } - Some(parent) => { - std::env::set_current_dir(parent)?; - - let cluster = cfg.provider.cluster.clone(); - let programs = cfg.programs.entry(cluster).or_default(); - if programs.contains_key(&name) { - return Err(anyhow!("Program already exists")); - } - - programs.insert( - name.clone(), - ProgramDeployment { - address: { - create_component(&name)?; - anchor_cli::rust_template::get_or_create_program_id(&name) - }, - path: None, - idl: None, - }, - ); - - let toml = cfg.to_string(); - fs::write("Anchor.toml", toml)?; - - println!("Created new component: {}", name); - } - }; - Ok(()) - }) -} - -// Create a new system from the template -fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> { - with_workspace(cfg_override, |cfg| { - match cfg.path().parent() { - None => { - println!("Unable to make new system"); - } - Some(parent) => { - std::env::set_current_dir(parent)?; - - let cluster = cfg.provider.cluster.clone(); - let programs = cfg.programs.entry(cluster).or_default(); - if programs.contains_key(&name) { - return Err(anyhow!("Program already exists")); - } - - programs.insert( - name.clone(), - anchor_cli::config::ProgramDeployment { - address: { - rust_template::create_system(&name)?; - anchor_cli::rust_template::get_or_create_program_id(&name) - }, - path: None, - idl: None, - }, - ); - - let toml = cfg.to_string(); - fs::write("Anchor.toml", toml)?; - - println!("Created new system: {}", name); - } - }; - Ok(()) - }) -} - -// with_workspace ensures the current working directory is always the top level -// workspace directory, i.e., where the `Anchor.toml` file is located, before -// and after the closure invocation. -// -// The closure passed into this function must never change the working directory -// to be outside the workspace. Doing so will have undefined behavior. -fn with_workspace( - cfg_override: &ConfigOverride, - f: impl FnOnce(&mut WithPath) -> R, -) -> R { - set_workspace_dir_or_exit(); - - let mut cfg = Config::discover(cfg_override) - .expect("Previously set the workspace dir") - .expect("Anchor.toml must always exist"); - - let r = f(&mut cfg); - - set_workspace_dir_or_exit(); - - r -} - -fn set_workspace_dir_or_exit() { - let d = match Config::discover(&ConfigOverride::default()) { - Err(err) => { - println!("Workspace configuration error: {err}"); - std::process::exit(1); - } - Ok(d) => d, - }; - match d { - None => { - println!("Not in anchor workspace."); - std::process::exit(1); - } - Some(cfg) => { - match cfg.path().parent() { - None => { - println!("Unable to make new program"); - } - Some(parent) => { - if std::env::set_current_dir(parent).is_err() { - println!("Not in anchor workspace."); - std::process::exit(1); - } - } - }; - } - } -} - fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result { let url = match Config::discover(cfg_override)? { Some(cfg) => cluster_url(&cfg, &cfg.test_validator), @@ -668,7 +546,7 @@ fn build_dynamic_types( .join("Cargo.toml"); if !cargo_path.exists() { let mut file = File::create(cargo_path)?; - file.write_all(rust_template::types_cargo_toml().as_bytes())?; + file.write_all(templates::workspace::types_cargo_toml().as_bytes())?; } std::env::set_current_dir(cur_dir)?; Ok(()) @@ -720,63 +598,3 @@ fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<() })?; Ok(()) } - -fn extract_component_id(line: &str) -> Option<&str> { - let component_id_marker = "#[component_id("; - line.find(component_id_marker).map(|start| { - let start = start + component_id_marker.len(); - let end = line[start..].find(')').unwrap() + start; - line[start..end].trim_matches('"') - }) -} - -fn fetch_idl_for_component(component_id: &str, url: &str) -> Result { - let output = std::process::Command::new("bolt") - .arg("idl") - .arg("fetch") - .arg(component_id) - .arg("--provider.cluster") - .arg(url) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output()?; - - if output.status.success() { - let idl_string = String::from_utf8(output.stdout) - .map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))? - .to_string(); - Ok(idl_string) - } else { - let error_message = String::from_utf8(output.stderr) - .unwrap_or(format!( - "Error trying to dynamically generate the type \ - for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \ - the appropriate cluster using the --provider.cluster option", - component_id - )) - .to_string(); - Err(anyhow!("Command failed with error: {}", error_message)) - } -} - -fn generate_component_type_file( - file_path: &Path, - cfg_override: &ConfigOverride, - component_id: &str, -) -> Result<()> { - let url = discover_cluster_url(cfg_override)?; - let idl_string = fetch_idl_for_component(component_id, &url)?; - let idl: Idl = serde_json::from_str(&idl_string)?; - let mut file = File::create(file_path)?; - file.write_all(rust_template::component_type(&idl, component_id)?.as_bytes())?; - Ok(()) -} - -fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> { - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(lib_rs_path)?; - file.write_all(rust_template::component_type_import(component_id).as_bytes())?; - Ok(()) -} diff --git a/cli/src/rust_template.rs b/cli/src/rust_template.rs index 91062ac..0c40f18 100644 --- a/cli/src/rust_template.rs +++ b/cli/src/rust_template.rs @@ -1,15 +1,15 @@ -use crate::VERSION; -use anchor_cli::rust_template::{get_or_create_program_id, ProgramTemplate}; +use anchor_cli::rust_template::ProgramTemplate; use anchor_cli::{create_files, Files}; -use anchor_lang_idl::types::{ - Idl, IdlArrayLen, IdlDefinedFields, IdlGenericArg, IdlType, IdlTypeDef, IdlTypeDefGeneric, - IdlTypeDefTy, -}; +use anchor_lang_idl::types::{IdlArrayLen, IdlGenericArg, IdlType}; use anyhow::Result; -use heck::{ToSnakeCase, ToUpperCamelCase}; use std::path::{Path, PathBuf}; -pub const ANCHOR_VERSION: &str = anchor_cli::VERSION; +use crate::templates::component::create_component_template_simple; +use crate::templates::program::{create_program_template_multiple, create_program_template_single}; +use crate::templates::system::create_system_template_simple; +use crate::templates::workspace::{ + cargo_toml, cargo_toml_with_serde, workspace_manifest, xargo_toml, +}; /// Create a component from the given name. pub fn create_component(name: &str) -> Result<()> { @@ -60,531 +60,6 @@ pub fn create_program(name: &str, template: ProgramTemplate) -> Result<()> { create_files(&[common_files, template_files].concat()) } -/// Create a component which holds position data. -fn create_component_template_simple(name: &str, program_path: &Path) -> Files { - vec![( - program_path.join("src").join("lib.rs"), - format!( - r#"use bolt_lang::*; - -declare_id!("{}"); - -#[component] -#[derive(Default)] -pub struct {} {{ - pub x: i64, - pub y: i64, - pub z: i64, - #[max_len(20)] - pub description: String, -}} -"#, - anchor_cli::rust_template::get_or_create_program_id(name), - name.to_upper_camel_case(), - ), - )] -} - -/// Create a system which operates on a Position component. -fn create_system_template_simple(name: &str, program_path: &Path) -> Files { - vec![( - program_path.join("src").join("lib.rs"), - format!( - r#"use bolt_lang::*; -use position::Position; - -declare_id!("{}"); - -#[system] -pub mod {} {{ - - pub fn execute(ctx: Context, _args_p: Vec) -> Result {{ - let position = &mut ctx.accounts.position; - position.x += 1; - position.y += 1; - Ok(ctx.accounts) - }} - - #[system_input] - pub struct Components {{ - pub position: Position, - }} - -}} -"#, - anchor_cli::rust_template::get_or_create_program_id(name), - name.to_snake_case(), - ), - )] -} - -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()), - ] -} - -const fn workspace_manifest() -> &'static str { - r#"[workspace] -members = [ - "programs/*", - "programs-ecs/components/*", - "programs-ecs/systems/*" -] -resolver = "2" - -[profile.release] -overflow-checks = true -lto = "fat" -codegen-units = 1 -[profile.release.build-override] -opt-level = 3 -incremental = false -codegen-units = 1 -"# -} - -pub fn package_json(jest: bool) -> String { - if jest { - format!( - r#"{{ - "scripts": {{ - "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", - "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" - }}, - "dependencies": {{ - "@coral-xyz/anchor": "^{ANCHOR_VERSION}" - }}, - "devDependencies": {{ - "jest": "^29.0.3", - "prettier": "^2.6.2" - }} - }} - "# - ) - } else { - format!( - r#"{{ - "scripts": {{ - "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", - "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" - }}, - "dependencies": {{ - "@coral-xyz/anchor": "^{ANCHOR_VERSION}" - }}, - "devDependencies": {{ - "chai": "^4.3.4", - "mocha": "^9.0.3", - "prettier": "^2.6.2", - "@metaplex-foundation/beet": "^0.7.1", - "@metaplex-foundation/beet-solana": "^0.4.0", - "@magicblock-labs/bolt-sdk": "latest" - }} -}} -"# - ) - } -} - -pub fn ts_package_json(jest: bool) -> String { - if jest { - format!( - r#"{{ - "scripts": {{ - "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", - "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" - }}, - "dependencies": {{ - "@coral-xyz/anchor": "^{ANCHOR_VERSION}" - }}, - "devDependencies": {{ - "@types/bn.js": "^5.1.0", - "@types/jest": "^29.0.3", - "jest": "^29.0.3", - "prettier": "^2.6.2", - "ts-jest": "^29.0.2", - "typescript": "^4.3.5", - "@metaplex-foundation/beet": "^0.7.1", - "@metaplex-foundation/beet-solana": "^0.4.0", - "@magicblock-labs/bolt-sdk": "latest" - }} - }} - "# - ) - } else { - format!( - r#"{{ - "scripts": {{ - "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", - "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" - }}, - "dependencies": {{ - "@coral-xyz/anchor": "^{ANCHOR_VERSION}" - }}, - "devDependencies": {{ - "chai": "^4.3.4", - "mocha": "^9.0.3", - "ts-mocha": "^10.0.0", - "@types/bn.js": "^5.1.0", - "@types/chai": "^4.3.0", - "@types/mocha": "^9.0.0", - "typescript": "^4.3.5", - "prettier": "^2.6.2", - "@metaplex-foundation/beet": "^0.7.1", - "@metaplex-foundation/beet-solana": "^0.4.0", - "@magicblock-labs/bolt-sdk": "latest" - }} -}} -"# - ) - } -} - -pub fn mocha(name: &str) -> String { - format!( - r#"const anchor = require("@coral-xyz/anchor"); -const boltSdk = require("@magicblock-labs/bolt-sdk"); -const {{ - InitializeNewWorld, -}} = boltSdk; - -describe("{}", () => {{ - // Configure the client to use the local cluster. - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - - it("InitializeNewWorld", async () => {{ - const initNewWorld = await InitializeNewWorld({{ - payer: provider.wallet.publicKey, - connection: provider.connection, - }}); - const txSign = await provider.sendAndConfirm(initNewWorld.transaction); - console.log(`Initialized a new world (ID=${{initNewWorld.worldPda}}). Initialization signature: ${{txSign}}`); - }}); - }}); -}}); -"#, - name, - ) -} - -pub fn jest(name: &str) -> String { - format!( - r#"const anchor = require("@coral-xyz/anchor"); -const boltSdk = require("@magicblock-labs/bolt-sdk"); -const {{ - InitializeNewWorld, -}} = boltSdk; - -describe("{}", () => {{ - // Configure the client to use the local cluster. - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - - // Constants used to test the program. - let worldPda: PublicKey; - - it("InitializeNewWorld", async () => {{ - const initNewWorld = await InitializeNewWorld({{ - payer: provider.wallet.publicKey, - connection: provider.connection, - }}); - const txSign = await provider.sendAndConfirm(initNewWorld.transaction); - worldPda = initNewWorld.worldPda; - console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`); - }}); - }}); -"#, - name, - ) -} - -pub fn ts_mocha(name: &str) -> String { - format!( - r#"import * as anchor from "@coral-xyz/anchor"; -import {{ Program }} from "@coral-xyz/anchor"; -import {{ PublicKey }} from "@solana/web3.js"; -import {{ Position }} from "../target/types/position"; -import {{ Movement }} from "../target/types/movement"; -import {{ - InitializeNewWorld, - AddEntity, - InitializeComponent, - ApplySystem, -}} from "@magicblock-labs/bolt-sdk" -import {{expect}} from "chai"; - -describe("{}", () => {{ - // Configure the client to use the local cluster. - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - - // Constants used to test the program. - let worldPda: PublicKey; - let entityPda: PublicKey; - let componentPda: PublicKey; - - const positionComponent = anchor.workspace.Position as Program; - const systemMovement = anchor.workspace.Movement as Program; - - it("InitializeNewWorld", async () => {{ - const initNewWorld = await InitializeNewWorld({{ - payer: provider.wallet.publicKey, - connection: provider.connection, - }}); - const txSign = await provider.sendAndConfirm(initNewWorld.transaction); - worldPda = initNewWorld.worldPda; - console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`); - }}); - - it("Add an entity", async () => {{ - const addEntity = await AddEntity({{ - payer: provider.wallet.publicKey, - world: worldPda, - connection: provider.connection, - }}); - const txSign = await provider.sendAndConfirm(addEntity.transaction); - entityPda = addEntity.entityPda; - console.log(`Initialized a new Entity (ID=${{addEntity.entityId}}). Initialization signature: ${{txSign}}`); - }}); - - it("Add a component", async () => {{ - const initializeComponent = await InitializeComponent({{ - payer: provider.wallet.publicKey, - entity: entityPda, - componentId: positionComponent.programId, - }}); - const txSign = await provider.sendAndConfirm(initializeComponent.transaction); - componentPda = initializeComponent.componentPda; - console.log(`Initialized the grid component. Initialization signature: ${{txSign}}`); - }}); - - it("Apply a system", async () => {{ - // Check that the component has been initialized and x is 0 - const positionBefore = await positionComponent.account.position.fetch( - componentPda - ); - expect(positionBefore.x.toNumber()).to.equal(0); - - // Run the movement system - const applySystem = await ApplySystem({{ - authority: provider.wallet.publicKey, - systemId: systemMovement.programId, - entities: [{{ - entity: entityPda, - components: [{{ componentId: positionComponent.programId }}], - }}] - }}); - const txSign = await provider.sendAndConfirm(applySystem.transaction); - console.log(`Applied a system. Signature: ${{txSign}}`); - - // Check that the system has been applied and x is > 0 - const positionAfter = await positionComponent.account.position.fetch( - componentPda - ); - expect(positionAfter.x.toNumber()).to.gt(0); - }}); - -}}); -"#, - name.to_upper_camel_case(), - ) -} - -fn cargo_toml(name: &str) -> String { - format!( - r#"[package] -name = "{0}" -version = "{2}" -description = "Created with Bolt" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "{1}" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build"] - -[dependencies] -bolt-lang = "{2}" -anchor-lang = "{3}" -"#, - name, - name.to_snake_case(), - VERSION, - ANCHOR_VERSION - ) -} - -/// TODO: Remove serde dependency -fn cargo_toml_with_serde(name: &str) -> String { - format!( - r#"[package] -name = "{0}" -version = "{2}" -description = "Created with Bolt" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "{1}" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build"] - -[dependencies] -bolt-lang = "{2}" -anchor-lang = "{3}" -serde = {{ version = "1.0", features = ["derive"] }} -"#, - name, - name.to_snake_case(), - VERSION, - ANCHOR_VERSION - ) -} - -fn xargo_toml() -> &'static str { - r#"[target.bpfel-unknown-unknown.dependencies.std] -features = [] -"# -} -pub fn git_ignore() -> &'static str { - r#" -.anchor -.bolt -.DS_Store -target -**/*.rs.bk -node_modules -test-ledger -.yarn -"# -} - -pub fn prettier_ignore() -> &'static str { - r#" -.anchor -.bolt -.DS_Store -target -node_modules -dist -build -test-ledger -"# -} - pub fn registry_account() -> &'static str { r#" { @@ -604,112 +79,6 @@ pub fn registry_account() -> &'static str { "# } -/// Automatic generation of crates from the components idl - -pub fn component_type(idl: &Idl, component_id: &str) -> Result { - let component_account = idl - .accounts - .iter() - .filter(|a| a.name.to_lowercase() != "Entity") - .last(); - let component_account = - component_account.ok_or_else(|| anyhow::anyhow!("Component account not found in IDL"))?; - - let type_def = &idl - .types - .iter() - .rfind(|ty| ty.name == component_account.name); - let type_def = match type_def { - Some(ty) => ty, - None => return Err(anyhow::anyhow!("Component type not found in IDL")), - }; - let component_code = component_to_rust_code(type_def, component_id); - let types_code = component_types_to_rust_code(&idl.types, &component_account.name); - Ok(format!( - r#"use bolt_lang::*; - -#[component_deserialize] -#[derive(Clone, Copy)] -{} - -{} -"#, - component_code, types_code - )) -} - -/// Convert the component type definition to rust code -fn component_to_rust_code(component: &IdlTypeDef, component_id: &str) -> String { - let mut code = String::new(); - // Add documentation comments, if any - for doc in &component.docs { - code += &format!("/// {}\n", doc); - } - // Handle generics - let generics = { - let generic_names: Vec = component - .generics - .iter() - .map(|gen| match gen { - IdlTypeDefGeneric::Type { name } => name.clone(), - IdlTypeDefGeneric::Const { name, .. } => name.clone(), - }) - .collect(); - if generic_names.is_empty() { - "".to_string() - } else { - format!("<{}>", generic_names.join(", ")) - } - }; - let composite_name = format!("Component{}", component_id); - if let IdlTypeDefTy::Struct { fields } = &component.ty { - code += &format!("pub struct {}{} {{\n", composite_name, generics); - code += &*component_fields_to_rust_code(fields); - code += "}\n\n"; - code += &format!("pub use {} as {};", composite_name, component.name); - } - code -} - -/// Code to expose the generated type, to be added to lib.rs -pub fn component_type_import(component_id: &str) -> String { - format!( - r#"#[allow(non_snake_case)] -mod component_{0}; -pub use component_{0}::*; -"#, - component_id, - ) -} - -/// Convert fields to rust code -fn component_fields_to_rust_code(fields: &Option) -> String { - let mut code = String::new(); - if let Some(fields) = fields { - match fields { - IdlDefinedFields::Named(named_fields) => { - for field in named_fields { - if field.name.to_lowercase() == "bolt_metadata" { - continue; - } - for doc in &field.docs { - code += &format!(" /// {}\n", doc); - } - let field_type = convert_idl_type_to_str(&field.ty); - code += &format!(" pub {}: {},\n", field.name, field_type); - } - } - IdlDefinedFields::Tuple(tuple_types) => { - for (index, ty) in tuple_types.iter().enumerate() { - let field_type = convert_idl_type_to_str(ty); - code += &format!(" pub field_{}: {},\n", index, field_type); - } - } - } - } - code -} - /// Map Idl type to rust type pub fn convert_idl_type_to_str(ty: &IdlType) -> String { match ty { @@ -760,71 +129,3 @@ pub fn convert_idl_type_to_str(ty: &IdlType) -> String { _ => unimplemented!("{ty:?}"), } } - -/// Convert the component types definition to rust code -fn component_types_to_rust_code(types: &[IdlTypeDef], component_name: &str) -> String { - types - .iter() - .filter(|ty| ty.name.to_lowercase() != "boltmetadata" && ty.name != component_name) - .map(component_type_to_rust_code) - .collect::>() - .join("\n") -} - -/// Convert the component type definition to rust code -fn component_type_to_rust_code(component_type: &IdlTypeDef) -> String { - let mut code = String::new(); - // Add documentation comments, if any - for doc in &component_type.docs { - code += &format!("/// {}\n", doc); - } - // Handle generics - let gen = &component_type.generics; - let generics = { - let generic_names: Vec = gen - .iter() - .map(|gen| match gen { - IdlTypeDefGeneric::Type { name } => name.clone(), - IdlTypeDefGeneric::Const { name, .. } => name.clone(), - }) - .collect(); - if generic_names.is_empty() { - "".to_string() - } else { - format!("<{}>", generic_names.join(", ")) - } - }; - if let IdlTypeDefTy::Struct { fields } = &component_type.ty { - code += &format!( - "#[component_deserialize]\n#[derive(Clone, Copy)]\npub struct {}{} {{\n", - component_type.name, generics - ); - code += &*component_fields_to_rust_code(fields); - code += "}\n\n"; - } - code -} - -pub(crate) fn types_cargo_toml() -> String { - let name = "bolt-types"; - format!( - r#"[package] -name = "{0}" -version = "{2}" -description = "Autogenerate types for the bolt language" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "{1}" - -[dependencies] -bolt-lang = "{2}" -anchor-lang = "{3}" -"#, - name, - name.to_snake_case(), - VERSION, - ANCHOR_VERSION - ) -} diff --git a/cli/src/system.rs b/cli/src/system.rs new file mode 100644 index 0000000..be7090e --- /dev/null +++ b/cli/src/system.rs @@ -0,0 +1,42 @@ +use crate::{rust_template::create_system, workspace::with_workspace}; +use anchor_cli::config::ConfigOverride; +use anyhow::{anyhow, Result}; +use std::fs; + +// Create a new system from the template +pub fn new_system(cfg_override: &ConfigOverride, name: String) -> Result<()> { + with_workspace(cfg_override, |cfg| { + match cfg.path().parent() { + None => { + println!("Unable to make new system"); + } + Some(parent) => { + std::env::set_current_dir(parent)?; + + let cluster = cfg.provider.cluster.clone(); + let programs = cfg.programs.entry(cluster).or_default(); + if programs.contains_key(&name) { + return Err(anyhow!("Program already exists")); + } + + programs.insert( + name.clone(), + anchor_cli::config::ProgramDeployment { + address: { + create_system(&name)?; + anchor_cli::rust_template::get_or_create_program_id(&name) + }, + path: None, + idl: None, + }, + ); + + let toml = cfg.to_string(); + fs::write("Anchor.toml", toml)?; + + println!("Created new system: {}", name); + } + }; + Ok(()) + }) +} diff --git a/cli/src/templates/component.rs b/cli/src/templates/component.rs new file mode 100644 index 0000000..ea07f7f --- /dev/null +++ b/cli/src/templates/component.rs @@ -0,0 +1,182 @@ +use anchor_cli::Files; +use anchor_lang_idl::types::{Idl, IdlDefinedFields, IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy}; +use anyhow::Result; +use heck::ToUpperCamelCase; +use std::path::Path; + +use crate::rust_template::convert_idl_type_to_str; // Import the trait + +/// Create a component which holds position data. +pub fn create_component_template_simple(name: &str, program_path: &Path) -> Files { + vec![( + program_path.join("src").join("lib.rs"), + format!( + r#"use bolt_lang::*; + +declare_id!("{}"); + +#[component] +#[derive(Default)] +pub struct {} {{ + pub x: i64, + pub y: i64, + pub z: i64, + #[max_len(20)] + pub description: String, +}} +"#, + anchor_cli::rust_template::get_or_create_program_id(name), + name.to_upper_camel_case(), + ), + )] +} + +/// Automatic generation of crates from the components idl + +pub fn component_type(idl: &Idl, component_id: &str) -> Result { + let component_account = idl + .accounts + .iter() + .filter(|a| a.name.to_lowercase() != "Entity") + .last(); + let component_account = + component_account.ok_or_else(|| anyhow::anyhow!("Component account not found in IDL"))?; + + let type_def = &idl + .types + .iter() + .rfind(|ty| ty.name == component_account.name); + let type_def = match type_def { + Some(ty) => ty, + None => return Err(anyhow::anyhow!("Component type not found in IDL")), + }; + let component_code = component_to_rust_code(type_def, component_id); + let types_code = component_types_to_rust_code(&idl.types, &component_account.name); + Ok(format!( + r#"use bolt_lang::*; + +#[component_deserialize] +#[derive(Clone, Copy)] +{} + +{} +"#, + component_code, types_code + )) +} + +/// Convert the component type definition to rust code +fn component_to_rust_code(component: &IdlTypeDef, component_id: &str) -> String { + let mut code = String::new(); + // Add documentation comments, if any + for doc in &component.docs { + code += &format!("/// {}\n", doc); + } + // Handle generics + let generics = { + let generic_names: Vec = component + .generics + .iter() + .map(|gen| match gen { + IdlTypeDefGeneric::Type { name } => name.clone(), + IdlTypeDefGeneric::Const { name, .. } => name.clone(), + }) + .collect(); + if generic_names.is_empty() { + "".to_string() + } else { + format!("<{}>", generic_names.join(", ")) + } + }; + let composite_name = format!("Component{}", component_id); + if let IdlTypeDefTy::Struct { fields } = &component.ty { + code += &format!("pub struct {}{} {{\n", composite_name, generics); + code += &*component_fields_to_rust_code(fields); + code += "}\n\n"; + code += &format!("pub use {} as {};", composite_name, component.name); + } + code +} + +/// Code to expose the generated type, to be added to lib.rs +pub fn component_type_import(component_id: &str) -> String { + format!( + r#"#[allow(non_snake_case)] +mod component_{0}; +pub use component_{0}::*; +"#, + component_id, + ) +} + +/// Convert fields to rust code +fn component_fields_to_rust_code(fields: &Option) -> String { + let mut code = String::new(); + if let Some(fields) = fields { + match fields { + IdlDefinedFields::Named(named_fields) => { + for field in named_fields { + if field.name.to_lowercase() == "bolt_metadata" { + continue; + } + for doc in &field.docs { + code += &format!(" /// {}\n", doc); + } + let field_type = convert_idl_type_to_str(&field.ty); + code += &format!(" pub {}: {},\n", field.name, field_type); + } + } + IdlDefinedFields::Tuple(tuple_types) => { + for (index, ty) in tuple_types.iter().enumerate() { + let field_type = convert_idl_type_to_str(ty); + code += &format!(" pub field_{}: {},\n", index, field_type); + } + } + } + } + code +} + +/// Convert the component types definition to rust code +fn component_types_to_rust_code(types: &[IdlTypeDef], component_name: &str) -> String { + types + .iter() + .filter(|ty| ty.name.to_lowercase() != "boltmetadata" && ty.name != component_name) + .map(component_type_to_rust_code) + .collect::>() + .join("\n") +} + +/// Convert the component type definition to rust code +fn component_type_to_rust_code(component_type: &IdlTypeDef) -> String { + let mut code = String::new(); + // Add documentation comments, if any + for doc in &component_type.docs { + code += &format!("/// {}\n", doc); + } + // Handle generics + let gen = &component_type.generics; + let generics = { + let generic_names: Vec = gen + .iter() + .map(|gen| match gen { + IdlTypeDefGeneric::Type { name } => name.clone(), + IdlTypeDefGeneric::Const { name, .. } => name.clone(), + }) + .collect(); + if generic_names.is_empty() { + "".to_string() + } else { + format!("<{}>", generic_names.join(", ")) + } + }; + if let IdlTypeDefTy::Struct { fields } = &component_type.ty { + code += &format!( + "#[component_deserialize]\n#[derive(Clone, Copy)]\npub struct {}{} {{\n", + component_type.name, generics + ); + code += &*component_fields_to_rust_code(fields); + code += "}\n\n"; + } + code +} diff --git a/cli/src/templates/mod.rs b/cli/src/templates/mod.rs new file mode 100644 index 0000000..b647cf6 --- /dev/null +++ b/cli/src/templates/mod.rs @@ -0,0 +1,4 @@ +pub mod component; +pub mod program; +pub mod system; +pub mod workspace; diff --git a/cli/src/templates/program.rs b/cli/src/templates/program.rs new file mode 100644 index 0000000..8c3399e --- /dev/null +++ b/cli/src/templates/program.rs @@ -0,0 +1,108 @@ +use anchor_cli::{rust_template::get_or_create_program_id, Files}; +use heck::ToSnakeCase; +use std::path::Path; // Import the trait + +pub 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... +pub 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()), + ] +} diff --git a/cli/src/templates/system.rs b/cli/src/templates/system.rs new file mode 100644 index 0000000..8f7be43 --- /dev/null +++ b/cli/src/templates/system.rs @@ -0,0 +1,36 @@ +use anchor_cli::Files; +use heck::ToSnakeCase; +use std::path::Path; + +/// Create a system which operates on a Position component. +pub fn create_system_template_simple(name: &str, program_path: &Path) -> Files { + vec![( + program_path.join("src").join("lib.rs"), + format!( + r#"use bolt_lang::*; +use position::Position; + +declare_id!("{}"); + +#[system] +pub mod {} {{ + + pub fn execute(ctx: Context, _args_p: Vec) -> Result {{ + let position = &mut ctx.accounts.position; + position.x += 1; + position.y += 1; + Ok(ctx.accounts) + }} + + #[system_input] + pub struct Components {{ + pub position: Position, + }} + +}} +"#, + anchor_cli::rust_template::get_or_create_program_id(name), + name.to_snake_case(), + ), + )] +} diff --git a/cli/src/templates/workspace.rs b/cli/src/templates/workspace.rs new file mode 100644 index 0000000..020c8e7 --- /dev/null +++ b/cli/src/templates/workspace.rs @@ -0,0 +1,389 @@ +use crate::VERSION; +use heck::{ToSnakeCase, ToUpperCamelCase}; +pub const ANCHOR_VERSION: &str = anchor_cli::VERSION; + +pub const fn workspace_manifest() -> &'static str { + r#"[workspace] +members = [ + "programs/*", + "programs-ecs/components/*", + "programs-ecs/systems/*" +] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 +"# +} + +pub fn package_json(jest: bool) -> String { + if jest { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{ANCHOR_VERSION}" + }}, + "devDependencies": {{ + "jest": "^29.0.3", + "prettier": "^2.6.2" + }} + }} + "# + ) + } else { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{ANCHOR_VERSION}" + }}, + "devDependencies": {{ + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "@metaplex-foundation/beet": "^0.7.1", + "@metaplex-foundation/beet-solana": "^0.4.0", + "@magicblock-labs/bolt-sdk": "latest" + }} +}} +"# + ) + } +} + +pub fn ts_package_json(jest: bool) -> String { + if jest { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{ANCHOR_VERSION}" + }}, + "devDependencies": {{ + "@types/bn.js": "^5.1.0", + "@types/jest": "^29.0.3", + "jest": "^29.0.3", + "prettier": "^2.6.2", + "ts-jest": "^29.0.2", + "typescript": "^4.3.5", + "@metaplex-foundation/beet": "^0.7.1", + "@metaplex-foundation/beet-solana": "^0.4.0", + "@magicblock-labs/bolt-sdk": "latest" + }} + }} + "# + ) + } else { + format!( + r#"{{ + "scripts": {{ + "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w", + "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check" + }}, + "dependencies": {{ + "@coral-xyz/anchor": "^{ANCHOR_VERSION}" + }}, + "devDependencies": {{ + "chai": "^4.3.4", + "mocha": "^9.0.3", + "ts-mocha": "^10.0.0", + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "typescript": "^4.3.5", + "prettier": "^2.6.2", + "@metaplex-foundation/beet": "^0.7.1", + "@metaplex-foundation/beet-solana": "^0.4.0", + "@magicblock-labs/bolt-sdk": "latest" + }} +}} +"# + ) + } +} + +pub fn mocha(name: &str) -> String { + format!( + r#"const anchor = require("@coral-xyz/anchor"); +const boltSdk = require("@magicblock-labs/bolt-sdk"); +const {{ + InitializeNewWorld, +}} = boltSdk; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + it("InitializeNewWorld", async () => {{ + const initNewWorld = await InitializeNewWorld({{ + payer: provider.wallet.publicKey, + connection: provider.connection, + }}); + const txSign = await provider.sendAndConfirm(initNewWorld.transaction); + console.log(`Initialized a new world (ID=${{initNewWorld.worldPda}}). Initialization signature: ${{txSign}}`); + }}); + }}); +}}); +"#, + name, + ) +} + +pub fn jest(name: &str) -> String { + format!( + r#"const anchor = require("@coral-xyz/anchor"); +const boltSdk = require("@magicblock-labs/bolt-sdk"); +const {{ + InitializeNewWorld, +}} = boltSdk; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + // Constants used to test the program. + let worldPda: PublicKey; + + it("InitializeNewWorld", async () => {{ + const initNewWorld = await InitializeNewWorld({{ + payer: provider.wallet.publicKey, + connection: provider.connection, + }}); + const txSign = await provider.sendAndConfirm(initNewWorld.transaction); + worldPda = initNewWorld.worldPda; + console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`); + }}); + }}); +"#, + name, + ) +} + +pub fn ts_mocha(name: &str) -> String { + format!( + r#"import * as anchor from "@coral-xyz/anchor"; +import {{ Program }} from "@coral-xyz/anchor"; +import {{ PublicKey }} from "@solana/web3.js"; +import {{ Position }} from "../target/types/position"; +import {{ Movement }} from "../target/types/movement"; +import {{ + InitializeNewWorld, + AddEntity, + InitializeComponent, + ApplySystem, +}} from "@magicblock-labs/bolt-sdk" +import {{expect}} from "chai"; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + // Constants used to test the program. + let worldPda: PublicKey; + let entityPda: PublicKey; + let componentPda: PublicKey; + + const positionComponent = anchor.workspace.Position as Program; + const systemMovement = anchor.workspace.Movement as Program; + + it("InitializeNewWorld", async () => {{ + const initNewWorld = await InitializeNewWorld({{ + payer: provider.wallet.publicKey, + connection: provider.connection, + }}); + const txSign = await provider.sendAndConfirm(initNewWorld.transaction); + worldPda = initNewWorld.worldPda; + console.log(`Initialized a new world (ID=${{worldPda}}). Initialization signature: ${{txSign}}`); + }}); + + it("Add an entity", async () => {{ + const addEntity = await AddEntity({{ + payer: provider.wallet.publicKey, + world: worldPda, + connection: provider.connection, + }}); + const txSign = await provider.sendAndConfirm(addEntity.transaction); + entityPda = addEntity.entityPda; + console.log(`Initialized a new Entity (ID=${{addEntity.entityId}}). Initialization signature: ${{txSign}}`); + }}); + + it("Add a component", async () => {{ + const initializeComponent = await InitializeComponent({{ + payer: provider.wallet.publicKey, + entity: entityPda, + componentId: positionComponent.programId, + }}); + const txSign = await provider.sendAndConfirm(initializeComponent.transaction); + componentPda = initializeComponent.componentPda; + console.log(`Initialized the grid component. Initialization signature: ${{txSign}}`); + }}); + + it("Apply a system", async () => {{ + // Check that the component has been initialized and x is 0 + const positionBefore = await positionComponent.account.position.fetch( + componentPda + ); + expect(positionBefore.x.toNumber()).to.equal(0); + + // Run the movement system + const applySystem = await ApplySystem({{ + authority: provider.wallet.publicKey, + systemId: systemMovement.programId, + entities: [{{ + entity: entityPda, + components: [{{ componentId: positionComponent.programId }}], + }}] + }}); + const txSign = await provider.sendAndConfirm(applySystem.transaction); + console.log(`Applied a system. Signature: ${{txSign}}`); + + // Check that the system has been applied and x is > 0 + const positionAfter = await positionComponent.account.position.fetch( + componentPda + ); + expect(positionAfter.x.toNumber()).to.gt(0); + }}); + +}}); +"#, + name.to_upper_camel_case(), + ) +} + +pub fn cargo_toml(name: &str) -> String { + format!( + r#"[package] +name = "{0}" +version = "{2}" +description = "Created with Bolt" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "{1}" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +bolt-lang = "{2}" +anchor-lang = "{3}" +"#, + name, + name.to_snake_case(), + VERSION, + ANCHOR_VERSION + ) +} + +/// TODO: Remove serde dependency +pub fn cargo_toml_with_serde(name: &str) -> String { + format!( + r#"[package] +name = "{0}" +version = "{2}" +description = "Created with Bolt" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "{1}" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +bolt-lang = "{2}" +anchor-lang = "{3}" +serde = {{ version = "1.0", features = ["derive"] }} +"#, + name, + name.to_snake_case(), + VERSION, + ANCHOR_VERSION + ) +} + +pub fn xargo_toml() -> &'static str { + r#"[target.bpfel-unknown-unknown.dependencies.std] +features = [] +"# +} +pub fn git_ignore() -> &'static str { + r#" +.anchor +.bolt +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn +"# +} + +pub fn prettier_ignore() -> &'static str { + r#" +.anchor +.bolt +.DS_Store +target +node_modules +dist +build +test-ledger +"# +} + +pub(crate) fn types_cargo_toml() -> String { + let name = "bolt-types"; + format!( + r#"[package] +name = "{0}" +version = "{2}" +description = "Autogenerate types for the bolt language" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "{1}" + +[dependencies] +bolt-lang = "{2}" +anchor-lang = "{3}" +"#, + name, + name.to_snake_case(), + VERSION, + ANCHOR_VERSION + ) +} diff --git a/cli/src/workspace.rs b/cli/src/workspace.rs new file mode 100644 index 0000000..ec4cf20 --- /dev/null +++ b/cli/src/workspace.rs @@ -0,0 +1,53 @@ +use anchor_cli::config::{Config, ConfigOverride, WithPath}; + +// with_workspace ensures the current working directory is always the top level +// workspace directory, i.e., where the `Anchor.toml` file is located, before +// and after the closure invocation. +// +// The closure passed into this function must never change the working directory +// to be outside the workspace. Doing so will have undefined behavior. +pub fn with_workspace( + cfg_override: &ConfigOverride, + f: impl FnOnce(&mut WithPath) -> R, +) -> R { + set_workspace_dir_or_exit(); + + let mut cfg = Config::discover(cfg_override) + .expect("Previously set the workspace dir") + .expect("Anchor.toml must always exist"); + + let r = f(&mut cfg); + + set_workspace_dir_or_exit(); + + r +} + +pub fn set_workspace_dir_or_exit() { + let d = match Config::discover(&ConfigOverride::default()) { + Err(err) => { + println!("Workspace configuration error: {err}"); + std::process::exit(1); + } + Ok(d) => d, + }; + match d { + None => { + println!("Not in anchor workspace."); + std::process::exit(1); + } + Some(cfg) => { + match cfg.path().parent() { + None => { + println!("Unable to make new program"); + } + Some(parent) => { + if std::env::set_current_dir(parent).is_err() { + println!("Not in anchor workspace."); + std::process::exit(1); + } + } + }; + } + } +}