diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b58c224..8c388a8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,16 +4,16 @@ on: pull_request: env: - solana_version: v1.17.0 + solana_version: v1.18.0 anchor_version: 0.29.0 jobs: install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: cache solana cli id: cache-solana with: @@ -22,7 +22,7 @@ jobs: ~/.local/share/solana/ key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 @@ -42,7 +42,8 @@ jobs: export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" yarn --frozen-lockfile --network-concurrency 2 - - uses: dtolnay/rust-toolchain@stable + - name: install rust + uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -61,7 +62,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache rust uses: Swatinem/rust-cache@v2 - name: Run fmt @@ -73,14 +74,14 @@ jobs: needs: install runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 - name: Cache node dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} @@ -93,10 +94,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: install rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Cache rust + uses: Swatinem/rust-cache@v2 + + - uses: actions/checkout@v4 - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 @@ -110,7 +119,7 @@ jobs: export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" yarn --frozen-lockfile - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: cache solana cli id: cache-solana with: @@ -144,6 +153,13 @@ jobs: npm i -g @coral-xyz/anchor-cli@${{ env.anchor_version }} ts-mocha typescript anchor test + - name: Install the Bolt CLI and create & build a new project + run: | + export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" + cargo install --path cli --force --locked + bolt init test-project --force + cd test-project && bolt build + - uses: actions/upload-artifact@v3 if: always() with: diff --git a/Cargo.lock b/Cargo.lock index 7dfb82e..8380693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "proc-macro2", "quote", "syn 1.0.109", @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "bs58 0.5.0", "proc-macro2", "quote", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "quote", "syn 1.0.109", ] @@ -194,9 +194,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "quote", "syn 1.0.109", ] @@ -216,9 +216,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "proc-macro2", "quote", "syn 1.0.109", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "anchor-attribute-program" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "quote", "syn 1.0.109", ] @@ -248,13 +248,13 @@ dependencies = [ [[package]] name = "anchor-cli" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ "anchor-client", - "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "anyhow", - "base64 0.13.1", + "base64 0.21.5", "bincode", "cargo_toml", "chrono", @@ -285,9 +285,9 @@ dependencies = [ [[package]] name = "anchor-client" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-lang 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "anyhow", "futures", "regex", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "quote", "syn 1.0.109", ] @@ -337,9 +337,9 @@ dependencies = [ [[package]] name = "anchor-derive-serde" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ - "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", + "anchor-syn 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "borsh-derive-internal 0.10.3", "proc-macro2", "quote", @@ -360,7 +360,7 @@ dependencies = [ [[package]] name = "anchor-derive-space" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ "proc-macro2", "quote", @@ -395,19 +395,20 @@ dependencies = [ [[package]] name = "anchor-lang" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" -dependencies = [ - "anchor-attribute-access-control 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-attribute-account 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-attribute-constant 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-attribute-error 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-attribute-event 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-attribute-program 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-derive-accounts 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-derive-serde 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", - "anchor-derive-space 0.29.0 (git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0)", +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" +dependencies = [ + "ahash 0.8.6", + "anchor-attribute-access-control 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-attribute-account 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-attribute-constant 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-attribute-error 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-attribute-event 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-attribute-program 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-derive-accounts 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-derive-serde 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", + "anchor-derive-space 0.29.0 (git+https://github.com/coral-xyz/anchor.git)", "arrayref", - "base64 0.13.1", + "base64 0.21.5", "bincode", "borsh 0.10.3", "bytemuck", @@ -437,7 +438,7 @@ dependencies = [ [[package]] name = "anchor-syn" version = "0.29.0" -source = "git+https://github.com/coral-xyz/anchor.git?rev=v0.29.0#fc9fd6d24b9be84abb2f40e47ed3faf7b11864ae" +source = "git+https://github.com/coral-xyz/anchor.git#169264d730ffeae16c12a33c0c353b7d3a461532" dependencies = [ "anyhow", "bs58 0.5.0", @@ -947,9 +948,11 @@ name = "bolt-cli" version = "0.0.1" dependencies = [ "anchor-cli", + "anchor-client", "anyhow", "clap 4.4.11", "heck 0.4.1", + "syn 1.0.109", ] [[package]] @@ -968,6 +971,7 @@ version = "0.1.0" name = "bolt-lang" version = "0.0.1" dependencies = [ + "ahash 0.8.6", "anchor-lang 0.29.0 (registry+https://github.com/rust-lang/crates.io-index)", "bolt-attribute-bolt-account", "bolt-attribute-bolt-component", @@ -4684,9 +4688,9 @@ dependencies = [ [[package]] name = "solang-parser" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb9fa2fa2fa6837be8a2495486ff92e3ffe68a99b6eeba288e139efdd842457" +checksum = "c425ce1c59f4b154717592f0bdf4715c3a1d55058883622d3157e1f0908a5b26" dependencies = [ "itertools 0.11.0", "lalrpop", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a3307cc..96d8b71 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,7 +16,9 @@ path = "src/bin/main.rs" dev = [] [dependencies] -anchor-cli = { git = "https://github.com/coral-xyz/anchor.git", rev = "v0.29.0" } +anchor-cli = { git = "https://github.com/coral-xyz/anchor.git" } +anchor-client = { git = "https://github.com/coral-xyz/anchor.git" } anyhow = "1.0.32" heck = "0.4.0" clap = { version = "4.2.4", features = ["derive"] } +syn = { version = "1.0.60", features = ["full", "extra-traits"] } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e360124..8c3c4fc 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,36 +1,50 @@ mod rust_template; -use anchor_cli::config::{Config, ConfigOverride, WithPath}; +use crate::rust_template::{create_component, create_system}; +use anchor_cli::config::{ + Config, ConfigOverride, ProgramDeployment, TestValidator, Validator, WithPath, +}; +use anchor_client::Cluster; use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; -use std::fs; +use heck::{ToKebabCase, ToSnakeCase}; +use std::collections::BTreeMap; +use std::fs::{self, File}; +use std::io::Write; +use std::process::Stdio; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const ANCHOR_VERSION: &str = anchor_cli::VERSION; #[derive(Debug, Subcommand)] pub enum BoltCommand { - // Include all existing commands from anchor_cli::Command - #[clap(flatten)] - Anchor(anchor_cli::Command), #[clap(about = "Create a new component")] Component(ComponentCommand), #[clap(about = "Create a new system")] System(SystemCommand), + // Include all existing commands from anchor_cli::Command + #[clap(flatten)] + Anchor(anchor_cli::Command), +} + +#[derive(Debug, Parser)] +pub struct InitCommand { + #[clap(short, long, help = "Workspace name")] + pub workspace_name: String, } #[derive(Debug, Parser)] pub struct ComponentCommand { - #[clap(short, long, help = "Name of the component")] pub name: String, } #[derive(Debug, Parser)] pub struct SystemCommand { - #[clap(short, long, help = "Name of the system")] pub name: String, } #[derive(Debug, Parser)] +#[clap(version = VERSION)] pub struct Opts { #[clap(flatten)] pub cfg_override: ConfigOverride, @@ -41,18 +55,302 @@ pub struct Opts { pub fn entry(opts: Opts) -> Result<()> { match opts.command { BoltCommand::Anchor(command) => { - // Delegate to the existing anchor_cli handler - let ops = anchor_cli::Opts { - cfg_override: opts.cfg_override, - command, - }; - anchor_cli::entry(ops) + if let anchor_cli::Command::Init { + name, + javascript, + solidity, + no_git, + jest, + template, + force, + } = command + { + init( + &opts.cfg_override, + name, + javascript, + solidity, + no_git, + jest, + template, + force, + ) + } else { + // Delegate to the existing anchor_cli handler + let opts = anchor_cli::Opts { + cfg_override: opts.cfg_override, + command, + }; + anchor_cli::entry(opts) + } } BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name), BoltCommand::System(command) => new_system(&opts.cfg_override, command.name), } } +// Bolt Init + +#[allow(clippy::too_many_arguments)] +fn init( + cfg_override: &ConfigOverride, + name: String, + javascript: bool, + solidity: bool, + no_git: bool, + jest: bool, + template: anchor_cli::rust_template::ProgramTemplate, + force: bool, +) -> Result<()> { + if !force && Config::discover(cfg_override)?.is_some() { + return Err(anyhow!("Workspace already initialized")); + } + + // We need to format different cases for the dir and the name + let rust_name = name.to_snake_case(); + let project_name = if name == rust_name { + rust_name.clone() + } else { + name.to_kebab_case() + }; + + // Additional keywords that have not been added to the `syn` crate as reserved words + // https://github.com/dtolnay/syn/pull/1098 + let extra_keywords = ["async", "await", "try"]; + let component_name = "position"; + let system_name = "movement"; + // Anchor converts to snake case before writing the program name + if syn::parse_str::(&rust_name).is_err() + || extra_keywords.contains(&rust_name.as_str()) + { + return Err(anyhow!( + "Anchor workspace name must be a valid Rust identifier. It may not be a Rust reserved word, start with a digit, or include certain disallowed characters. See https://doc.rust-lang.org/reference/identifiers.html for more detail.", + )); + } + + if force { + fs::create_dir_all(&project_name)?; + } else { + fs::create_dir(&project_name)?; + } + std::env::set_current_dir(&project_name)?; + fs::create_dir_all("app")?; + + let mut cfg = Config::default(); + if jest { + cfg.scripts.insert( + "test".to_owned(), + if javascript { + "yarn run jest" + } else { + "yarn run jest --preset ts-jest" + } + .to_owned(), + ); + } else { + cfg.scripts.insert( + "test".to_owned(), + if javascript { + "yarn run mocha -t 1000000 tests/" + } else { + "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + } + .to_owned(), + ); + } + + let mut localnet = BTreeMap::new(); + let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name); + localnet.insert( + rust_name, + ProgramDeployment { + address: program_id, + path: None, + idl: None, + }, + ); + if !solidity { + let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name); + let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name); + localnet.insert( + component_name.to_owned(), + ProgramDeployment { + address: component_id, + path: None, + idl: None, + }, + ); + localnet.insert( + system_name.to_owned(), + ProgramDeployment { + address: system_id, + path: None, + idl: None, + }, + ); + cfg.workspace.members.push("programs/*".to_owned()); + cfg.workspace + .members + .push("programs-ecs/components/*".to_owned()); + cfg.workspace + .members + .push("programs-ecs/systems/*".to_owned()); + } + + // Setup the test validator to clone Bolt programs from devnet + let validator = Validator { + url: Some("https://rpc.magicblock.app/devnet/".to_owned()), + rpc_port: 8899, + bind_address: "0.0.0.0".to_owned(), + ledger: ".bolt/test-ledger".to_owned(), + clone: Some(vec![ + // World program + anchor_cli::config::CloneEntry { + address: "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n".to_owned(), + }, + // World executable data + anchor_cli::config::CloneEntry { + address: "CrsqUXPpJYpVAAx5qMKU6K8RT1TzT81T8BL6JndWSeo3".to_owned(), + }, + // Registry + anchor_cli::config::CloneEntry { + address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(), + }, + ]), + ..Default::default() + }; + + let test_validator = TestValidator { + startup_wait: 5000, + shutdown_wait: 2000, + validator: Some(validator), + ..Default::default() + }; + + cfg.test_validator = Some(test_validator); + cfg.programs.insert(Cluster::Localnet, localnet); + let toml = cfg.to_string(); + fs::write("Anchor.toml", toml)?; + + // Initialize .gitignore file + fs::write(".gitignore", rust_template::git_ignore())?; + + // Initialize .prettierignore file + fs::write(".prettierignore", rust_template::prettier_ignore())?; + + // Remove the default programs if `--force` is passed + if force { + let programs_path = std::env::current_dir()? + .join(if solidity { "solidity" } else { "programs" }) + .join(&project_name); + fs::create_dir_all(&programs_path)?; + fs::remove_dir_all(&programs_path)?; + let programs_ecs_path = std::env::current_dir()? + .join("programs-ecs") + .join(&project_name); + fs::create_dir_all(&programs_ecs_path)?; + fs::remove_dir_all(&programs_ecs_path)?; + } + + // Build the program. + if solidity { + anchor_cli::solidity_template::create_program(&project_name)?; + } else { + create_component(component_name)?; + create_system(system_name)?; + anchor_cli::rust_template::create_program(&project_name, template)?; + } + + // Build the test suite. + fs::create_dir_all("tests")?; + // Build the migrations directory. + fs::create_dir_all("migrations")?; + + if javascript { + // Build javascript config + let mut package_json = File::create("package.json")?; + package_json.write_all(rust_template::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())?; + } + } 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())?; + } + } + + let mut deploy = File::create("migrations/deploy.js")?; + + deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?; + } else { + // Build typescript config + let mut ts_config = File::create("tsconfig.json")?; + 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())?; + + let mut deploy = File::create("migrations/deploy.ts")?; + deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?; + + let mut mocha = File::create(format!("tests/{}.ts", &project_name))?; + 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())?; + } + } + + let yarn_result = install_node_modules("yarn")?; + if !yarn_result.status.success() { + println!("Failed yarn install will attempt to npm install"); + install_node_modules("npm")?; + } + + if !no_git { + let git_result = std::process::Command::new("git") + .arg("init") + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?; + if !git_result.status.success() { + eprintln!("Failed to automatically initialize a new git repository"); + } + } + + println!("{project_name} initialized"); + + Ok(()) +} + +// Install node modules +fn install_node_modules(cmd: &str) -> Result { + let mut command = std::process::Command::new(if cfg!(target_os = "windows") { + "cmd" + } else { + cmd + }); + if cfg!(target_os = "windows") { + command.arg(format!("/C {} install", cmd)); + } else { + command.arg("install"); + } + command + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .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| { diff --git a/cli/src/rust_template.rs b/cli/src/rust_template.rs index 9e00812..4ab80cc 100644 --- a/cli/src/rust_template.rs +++ b/cli/src/rust_template.rs @@ -1,12 +1,13 @@ +use crate::ANCHOR_VERSION; use crate::VERSION; use anchor_cli::Files; use anyhow::Result; -use heck::{ToKebabCase, ToSnakeCase, ToUpperCamelCase}; +use heck::{ToSnakeCase, ToUpperCamelCase}; use std::path::{Path, PathBuf}; /// Create a component from the given name. pub fn create_component(name: &str) -> Result<()> { - let program_path = Path::new("programs").join(name); + let program_path = Path::new("programs-ecs/components").join(name); let common_files = vec![ ( PathBuf::from("Cargo.toml".to_string()), @@ -22,7 +23,7 @@ pub fn create_component(name: &str) -> Result<()> { /// Create a system from the given name. pub(crate) fn create_system(name: &str) -> Result<()> { - let program_path = Path::new("programs").join(name); + let program_path = Path::new("programs-ecs/systems").join(name); let common_files = vec![ ( PathBuf::from("Cargo.toml".to_string()), @@ -36,7 +37,7 @@ pub(crate) fn create_system(name: &str) -> Result<()> { anchor_cli::create_files(&[common_files, template_files].concat()) } -/// Create a program with a single `lib.rs` file. +/// 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"), @@ -52,7 +53,7 @@ pub mod {} {{ }} #[account] -#[bolt_account(component_id = "{}")] +#[bolt_account(component_id = "")] pub struct {} {{ pub x: i64, pub y: i64, @@ -64,13 +65,12 @@ pub struct {} {{ anchor_cli::rust_template::get_or_create_program_id(name), name.to_upper_camel_case(), name.to_snake_case(), - name.to_kebab_case(), name.to_upper_camel_case(), ), )] } -/// Create a program with a single `lib.rs` file. +/// 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"), @@ -85,10 +85,8 @@ pub mod {} {{ use super::*; pub fn execute(ctx: Context, args: Vec) -> Result {{ - let mut position = Position::from_account_info(&ctx.accounts.position)?; position.x += 1; - Ok(position) }} }} @@ -105,7 +103,6 @@ pub struct Position {{ pub x: i64, pub y: i64, pub z: i64, - #[max_len(20)] pub description: String, }} "#, @@ -118,7 +115,9 @@ pub struct Position {{ const fn workspace_manifest() -> &'static str { r#"[workspace] members = [ - "programs/*" + "programs/*", + "programs-ecs/components/*", + "programs-ecs/systems/*" ] resolver = "2" @@ -133,6 +132,292 @@ 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": "^{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": "^{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", + "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", + "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", + "bolt-sdk": "latest" + }} +}} +"# + ) + } +} + +pub fn mocha(name: &str) -> String { + format!( + r#"const anchor = require("@coral-xyz/anchor"); +const boltSdk = require("bolt-sdk"); +const {{ + createInitializeNewWorldInstruction, + FindWorldPda, + FindWorldRegistryPda, + Registry, + World +}} = boltSdk; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + it("InitializeNewWorld", async () => {{ + const registry = await Registry.fromAccountAddress(provider.connection, registryPda); + worldId = new anchor.BN(registry.worlds); + worldPda = FindWorldPda(new anchor.BN(worldId)) + const initializeWorldIx = createInitializeNewWorldInstruction( + {{ + world: worldPda, + registry: registryPda, + payer: provider.wallet.publicKey, + }}); + + const tx = new anchor.web3.Transaction().add(initializeWorldIx); + const txSign = await provider.sendAndConfirm(tx); + console.log(`Initialized a new world (ID=${{worldId}}). Initialization signature: ${{txSign}}`); + }}); + }}); +}}); +"#, + name, + ) +} + +pub fn jest(name: &str) -> String { + format!( + r#"const anchor = require("@coral-xyz/anchor"); +const boltSdk = require("bolt-sdk"); +const {{ + createInitializeNewWorldInstruction, + FindWorldPda, + FindWorldRegistryPda, + Registry, + World +}} = boltSdk; + +describe("{}", () => {{ + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + // Constants used to test the program. + const registryPda = FindWorldRegistryPda(); + let worldId: anchor.BN; + let worldPda: PublicKey; + + it("InitializeNewWorld", async () => {{ + const registry = await Registry.fromAccountAddress(provider.connection, registryPda); + worldId = new anchor.BN(registry.worlds); + worldPda = FindWorldPda(new anchor.BN(worldId)) + const initializeWorldIx = createInitializeNewWorldInstruction( + {{ + world: worldPda, + registry: registryPda, + payer: provider.wallet.publicKey, + }}); + + const tx = new anchor.web3.Transaction().add(initializeWorldIx); + const txSign = await provider.sendAndConfirm(tx); + console.log(`Initialized a new world (ID=${{worldId}}). 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 {{ + createInitializeNewWorldInstruction, + FindWorldPda, + FindWorldRegistryPda, + FindEntityPda, + Registry, + World, + createAddEntityInstruction, + createInitializeComponentInstruction, + FindComponentPda, createApplyInstruction +}} from "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. + const registryPda = FindWorldRegistryPda(); + let worldId: anchor.BN; + let worldPda: PublicKey; + let entityPda: PublicKey; + + const positionComponent = anchor.workspace.Position as Program; + const systemMovement = anchor.workspace.Movement as Program; + + it("InitializeNewWorld", async () => {{ + const registry = await Registry.fromAccountAddress(provider.connection, registryPda); + worldId = new anchor.BN(registry.worlds); + worldPda = FindWorldPda(new anchor.BN(worldId)) + const initializeWorldIx = createInitializeNewWorldInstruction( + {{ + world: worldPda, + registry: registryPda, + payer: provider.wallet.publicKey, + }}); + + const tx = new anchor.web3.Transaction().add(initializeWorldIx); + const txSign = await provider.sendAndConfirm(tx); + console.log(`Initialized a new world (ID=${{worldId}}). Initialization signature: ${{txSign}}`); + }}); + + it("Add an entity", async () => {{ + const world = await World.fromAccountAddress(provider.connection, worldPda); + const entityId = new anchor.BN(world.entities); + entityPda = FindEntityPda(worldId, entityId); + + let createEntityIx = createAddEntityInstruction({{ + world: worldPda, + payer: provider.wallet.publicKey, + entity: entityPda, + }}); + const tx = new anchor.web3.Transaction().add(createEntityIx); + const txSign = await provider.sendAndConfirm(tx); + console.log(`Initialized a new Entity (ID=${{worldId}}). Initialization signature: ${{txSign}}`); + }}); + + it("Add a component", async () => {{ + const positionComponentPda = FindComponentPda(positionComponent.programId, entityPda, ""); + let initComponentIx = createInitializeComponentInstruction({{ + payer: provider.wallet.publicKey, + entity: entityPda, + data: positionComponentPda, + componentProgram: positionComponent.programId, + }}); + + const tx = new anchor.web3.Transaction().add(initComponentIx); + const txSign = await provider.sendAndConfirm(tx); + console.log(`Initialized a new component. Initialization signature: ${{txSign}}`); + }}); + + it("Apply a system", async () => {{ + const positionComponentPda = FindComponentPda(positionComponent.programId, entityPda, ""); + // Check that the component has been initialized and x is 0 + let positionData = await positionComponent.account.position.fetch( + positionComponentPda + ); + expect(positionData.x.toNumber()).to.eq(0); + let applySystemIx = createApplyInstruction({{ + componentProgram: positionComponent.programId, + boltSystem: systemMovement.programId, + boltComponent: positionComponentPda, + }}, {{args: new Uint8Array()}}); + + const tx = new anchor.web3.Transaction().add(applySystemIx); + await provider.sendAndConfirm(tx); + + // Check that the system has been applied and x is > 0 + positionData = await positionComponent.account.position.fetch( + positionComponentPda + ); + expect(positionData.x.toNumber()).to.gt(0); + }}); + +}}); +"#, + name.to_upper_camel_case(), + ) +} + fn cargo_toml(name: &str) -> String { format!( r#"[package] @@ -168,3 +453,28 @@ fn xargo_toml() -> &'static str { 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 +"# +} diff --git a/crates/bolt-lang/Cargo.toml b/crates/bolt-lang/Cargo.toml index 92c8dfb..b531dd3 100644 --- a/crates/bolt-lang/Cargo.toml +++ b/crates/bolt-lang/Cargo.toml @@ -20,4 +20,7 @@ bolt-system = { path = "../../programs/bolt-system", features = ["cpi"], version # Other dependencies serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" + +# TODO: Remove once https://github.com/solana-labs/solana/issues/33504 is resolved. +ahash = "=0.8.6" \ No newline at end of file