bolt_cli/
lib.rs

1mod component;
2mod instructions;
3mod rust_template;
4mod system;
5mod templates;
6mod workspace;
7
8use crate::component::new_component;
9use crate::instructions::{
10    approve_system, authorize, create_registry, create_world, deauthorize, remove_system,
11};
12use crate::rust_template::{create_component, create_system};
13use crate::system::new_system;
14use anchor_cli::config;
15use anchor_cli::config::{
16    BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
17    TestValidator, Validator, WithPath,
18};
19use anchor_client::Cluster;
20use anyhow::{anyhow, Result};
21use clap::{Parser, Subcommand};
22use component::{append_component_to_lib_rs, extract_component_id, generate_component_type_file};
23use heck::{ToKebabCase, ToSnakeCase};
24use std::collections::BTreeMap;
25use std::fs::{self, create_dir_all, File};
26use std::io::Write;
27use std::io::{self, BufRead};
28use std::path::{Path, PathBuf};
29use std::process::Stdio;
30use std::string::ToString;
31pub const VERSION: &str = env!("CARGO_PKG_VERSION");
32pub const ANCHOR_VERSION: &str = anchor_cli::VERSION;
33#[derive(Subcommand)]
34pub enum BoltCommand {
35    #[clap(about = "Create a new component")]
36    Component(ComponentCommand),
37    #[clap(about = "Create a new system")]
38    System(SystemCommand),
39    // Include all existing commands from anchor_cli::Command
40    #[clap(flatten)]
41    Anchor(anchor_cli::Command),
42    #[clap(about = "Add a new registry instance")]
43    Registry(RegistryCommand),
44    #[clap(about = "Add a new world instance")]
45    World(WorldCommand),
46    #[clap(about = "Add a new authority for a world instance")]
47    Authorize(AuthorizeCommand),
48    #[clap(about = "Remove an authority from a world instance")]
49    Deauthorize(DeauthorizeCommand),
50    #[clap(about = "Approve a system for a world instance")]
51    ApproveSystem(ApproveSystemCommand),
52    #[clap(about = "Remove a system from a world instance")]
53    RemoveSystem(RemoveSystemCommand),
54}
55
56#[derive(Debug, Parser)]
57pub struct InitCommand {
58    #[clap(short, long, help = "Workspace name")]
59    pub workspace_name: String,
60}
61
62#[derive(Debug, Parser)]
63pub struct ComponentCommand {
64    pub name: String,
65}
66
67#[derive(Debug, Parser)]
68pub struct SystemCommand {
69    pub name: String,
70}
71
72#[derive(Debug, Parser)]
73pub struct RegistryCommand {}
74
75#[derive(Debug, Parser)]
76pub struct WorldCommand {}
77
78#[derive(Debug, Parser)]
79pub struct AuthorizeCommand {
80    pub world: String,
81    pub new_authority: String,
82}
83
84#[derive(Debug, Parser)]
85pub struct DeauthorizeCommand {
86    pub world: String,
87    pub authority_to_remove: String,
88}
89
90#[derive(Debug, Parser)]
91pub struct ApproveSystemCommand {
92    pub world: String,
93    pub system_to_approve: String,
94}
95
96#[derive(Debug, Parser)]
97pub struct RemoveSystemCommand {
98    pub world: String,
99    pub system_to_remove: String,
100}
101
102#[derive(Parser)]
103#[clap(version = VERSION)]
104pub struct Opts {
105    /// Rebuild the auto-generated types
106    #[clap(global = true, long, action)]
107    pub rebuild_types: bool,
108    #[clap(flatten)]
109    pub cfg_override: ConfigOverride,
110    #[clap(subcommand)]
111    pub command: BoltCommand,
112}
113
114pub fn entry(opts: Opts) -> Result<()> {
115    match opts.command {
116        BoltCommand::Anchor(command) => match command {
117            anchor_cli::Command::Init {
118                name,
119                javascript,
120                solidity,
121                no_install,
122                no_git,
123                template,
124                test_template,
125                force,
126            } => init(
127                &opts.cfg_override,
128                name,
129                javascript,
130                solidity,
131                no_install,
132                no_git,
133                template,
134                test_template,
135                force,
136            ),
137            anchor_cli::Command::Build {
138                idl,
139                no_idl,
140                idl_ts,
141                verifiable,
142                program_name,
143                solana_version,
144                docker_image,
145                bootstrap,
146                cargo_args,
147                env,
148                skip_lint,
149                no_docs,
150                arch,
151            } => build(
152                &opts.cfg_override,
153                no_idl,
154                idl,
155                idl_ts,
156                verifiable,
157                skip_lint,
158                program_name,
159                solana_version,
160                docker_image,
161                bootstrap,
162                None,
163                None,
164                env,
165                cargo_args,
166                no_docs,
167                arch,
168                opts.rebuild_types,
169            ),
170            _ => {
171                let opts = anchor_cli::Opts {
172                    cfg_override: opts.cfg_override,
173                    command,
174                };
175                anchor_cli::entry(opts)
176            }
177        },
178        BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name),
179        BoltCommand::System(command) => new_system(&opts.cfg_override, command.name),
180        BoltCommand::Registry(_command) => create_registry(&opts.cfg_override),
181        BoltCommand::World(_command) => create_world(&opts.cfg_override),
182        BoltCommand::Authorize(command) => {
183            authorize(&opts.cfg_override, command.world, command.new_authority)
184        }
185        BoltCommand::Deauthorize(command) => deauthorize(
186            &opts.cfg_override,
187            command.world,
188            command.authority_to_remove,
189        ),
190        BoltCommand::ApproveSystem(command) => {
191            approve_system(&opts.cfg_override, command.world, command.system_to_approve)
192        }
193        BoltCommand::RemoveSystem(command) => {
194            remove_system(&opts.cfg_override, command.world, command.system_to_remove)
195        }
196    }
197}
198// Bolt Init
199#[allow(clippy::too_many_arguments)]
200fn init(
201    cfg_override: &ConfigOverride,
202    name: String,
203    javascript: bool,
204    solidity: bool,
205    no_install: bool,
206    no_git: bool,
207    template: anchor_cli::rust_template::ProgramTemplate,
208    test_template: anchor_cli::rust_template::TestTemplate,
209    force: bool,
210) -> Result<()> {
211    if !force && Config::discover(cfg_override)?.is_some() {
212        return Err(anyhow!("Workspace already initialized"));
213    }
214
215    // We need to format different cases for the dir and the name
216    let rust_name = name.to_snake_case();
217    let project_name = if name == rust_name {
218        rust_name.clone()
219    } else {
220        name.to_kebab_case()
221    };
222
223    // Additional keywords that have not been added to the `syn` crate as reserved words
224    // https://github.com/dtolnay/syn/pull/1098
225    let extra_keywords = ["async", "await", "try"];
226    let component_name = "position";
227    let system_name = "movement";
228    // Anchor converts to snake case before writing the program name
229    if syn::parse_str::<syn::Ident>(&rust_name).is_err()
230        || extra_keywords.contains(&rust_name.as_str())
231    {
232        return Err(anyhow!(
233            "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.",
234        ));
235    }
236
237    if force {
238        fs::create_dir_all(&project_name)?;
239    } else {
240        fs::create_dir(&project_name)?;
241    }
242    std::env::set_current_dir(&project_name)?;
243    fs::create_dir_all("app")?;
244
245    let mut cfg = Config::default();
246    let jest = test_template == anchor_cli::rust_template::TestTemplate::Jest;
247    if jest {
248        cfg.scripts.insert(
249            "test".to_owned(),
250            if javascript {
251                "yarn run jest"
252            } else {
253                "yarn run jest --preset ts-jest"
254            }
255            .to_owned(),
256        );
257    } else {
258        cfg.scripts.insert(
259            "test".to_owned(),
260            if javascript {
261                "yarn run mocha -t 1000000 tests/"
262            } else {
263                "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
264            }
265            .to_owned(),
266        );
267    }
268
269    let mut localnet = BTreeMap::new();
270    let program_id = anchor_cli::rust_template::get_or_create_program_id(&rust_name);
271    localnet.insert(
272        rust_name,
273        ProgramDeployment {
274            address: program_id,
275            path: None,
276            idl: None,
277        },
278    );
279    if !solidity {
280        let component_id = anchor_cli::rust_template::get_or_create_program_id(component_name);
281        let system_id = anchor_cli::rust_template::get_or_create_program_id(system_name);
282        localnet.insert(
283            component_name.to_owned(),
284            ProgramDeployment {
285                address: component_id,
286                path: None,
287                idl: None,
288            },
289        );
290        localnet.insert(
291            system_name.to_owned(),
292            ProgramDeployment {
293                address: system_id,
294                path: None,
295                idl: None,
296            },
297        );
298        cfg.workspace.members.push("programs/*".to_owned());
299        cfg.workspace
300            .members
301            .push("programs-ecs/components/*".to_owned());
302        cfg.workspace
303            .members
304            .push("programs-ecs/systems/*".to_owned());
305    }
306
307    // Setup the test validator to clone Bolt programs from devnet
308    let validator = Validator {
309        url: Some("https://rpc.magicblock.app/devnet/".to_owned()),
310        rpc_port: 8899,
311        bind_address: "0.0.0.0".to_owned(),
312        ledger: ".bolt/test-ledger".to_owned(),
313        account: Some(vec![
314            // Registry account
315            anchor_cli::config::AccountEntry {
316                address: "EHLkWwAT9oebVv9ht3mtqrvHhRVMKrt54tF3MfHTey2K".to_owned(),
317                filename: "tests/fixtures/registry.json".to_owned(),
318            },
319        ]),
320        ..Default::default()
321    };
322
323    let test_validator = TestValidator {
324        startup_wait: 5000,
325        shutdown_wait: 2000,
326        validator: Some(validator),
327        genesis: Some(vec![GenesisEntry {
328            address: world::id().to_string(),
329            program: "tests/fixtures/world.so".to_owned(),
330            upgradeable: Some(false),
331        }]),
332        ..Default::default()
333    };
334
335    cfg.test_validator = Some(test_validator);
336    cfg.programs.insert(Cluster::Localnet, localnet);
337    let toml = cfg.to_string();
338    fs::write("Anchor.toml", toml)?;
339
340    // Initialize .gitignore file
341    fs::write(".gitignore", templates::workspace::git_ignore())?;
342
343    // Initialize .prettierignore file
344    fs::write(".prettierignore", templates::workspace::prettier_ignore())?;
345
346    // Remove the default programs if `--force` is passed
347    if force {
348        let programs_path = std::env::current_dir()?
349            .join(if solidity { "solidity" } else { "programs" })
350            .join(&project_name);
351        fs::create_dir_all(&programs_path)?;
352        fs::remove_dir_all(&programs_path)?;
353        let programs_ecs_path = std::env::current_dir()?
354            .join("programs-ecs")
355            .join(&project_name);
356        fs::create_dir_all(&programs_ecs_path)?;
357        fs::remove_dir_all(&programs_ecs_path)?;
358    }
359
360    // Build the program.
361    if solidity {
362        anchor_cli::solidity_template::create_program(&project_name)?;
363    } else {
364        create_system(system_name)?;
365        create_component(component_name)?;
366        rust_template::create_program(&project_name, template)?;
367
368        // Add the component as a dependency to the system
369        std::process::Command::new("cargo")
370            .arg("add")
371            .arg("--package")
372            .arg(system_name)
373            .arg("--path")
374            .arg(format!("programs-ecs/components/{}", component_name))
375            .arg("--features")
376            .arg("cpi")
377            .stdout(std::process::Stdio::null())
378            .stderr(std::process::Stdio::null())
379            .spawn()
380            .map_err(|e| {
381                anyhow::format_err!(
382                    "error adding component as dependency to the system: {}",
383                    e.to_string()
384                )
385            })?;
386    }
387
388    // Build the test suite.
389    fs::create_dir_all("tests/fixtures")?;
390    // Build the migrations directory.
391    fs::create_dir_all("migrations")?;
392
393    // Create the registry account
394    fs::write(
395        "tests/fixtures/registry.json",
396        rust_template::registry_account(),
397    )?;
398
399    // Dump the World program into tests/fixtures/world.so
400    std::process::Command::new("solana")
401        .arg("program")
402        .arg("dump")
403        .arg("-u")
404        .arg("d")
405        .arg(world::id().to_string())
406        .arg("tests/fixtures/world.so")
407        .stdout(Stdio::inherit())
408        .stderr(Stdio::inherit())
409        .spawn()
410        .map_err(|e| anyhow::format_err!("solana program dump failed: {}", e.to_string()))?;
411
412    if javascript {
413        // Build javascript config
414        let mut package_json = File::create("package.json")?;
415        package_json.write_all(templates::workspace::package_json(jest).as_bytes())?;
416
417        if jest {
418            let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
419            if solidity {
420                test.write_all(anchor_cli::solidity_template::jest(&project_name).as_bytes())?;
421            } else {
422                test.write_all(templates::workspace::jest(&project_name).as_bytes())?;
423            }
424        } else {
425            let mut test = File::create(format!("tests/{}.js", &project_name))?;
426            if solidity {
427                test.write_all(anchor_cli::solidity_template::mocha(&project_name).as_bytes())?;
428            } else {
429                test.write_all(templates::workspace::mocha(&project_name).as_bytes())?;
430            }
431        }
432
433        let mut deploy = File::create("migrations/deploy.js")?;
434
435        deploy.write_all(anchor_cli::rust_template::deploy_script().as_bytes())?;
436    } else {
437        // Build typescript config
438        let mut ts_config = File::create("tsconfig.json")?;
439        ts_config.write_all(anchor_cli::rust_template::ts_config(jest).as_bytes())?;
440
441        let mut ts_package_json = File::create("package.json")?;
442        ts_package_json.write_all(templates::workspace::ts_package_json(jest).as_bytes())?;
443
444        let mut deploy = File::create("migrations/deploy.ts")?;
445        deploy.write_all(anchor_cli::rust_template::ts_deploy_script().as_bytes())?;
446
447        let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
448        if solidity {
449            mocha.write_all(anchor_cli::solidity_template::ts_mocha(&project_name).as_bytes())?;
450        } else {
451            mocha.write_all(templates::workspace::ts_mocha(&project_name).as_bytes())?;
452        }
453    }
454
455    if !no_install {
456        let yarn_result = install_node_modules("yarn")?;
457        if !yarn_result.status.success() {
458            println!("Failed yarn install will attempt to npm install");
459            install_node_modules("npm")?;
460        }
461    }
462
463    if !no_git {
464        let git_result = std::process::Command::new("git")
465            .arg("init")
466            .stdout(Stdio::inherit())
467            .stderr(Stdio::inherit())
468            .output()
469            .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
470        if !git_result.status.success() {
471            eprintln!("Failed to automatically initialize a new git repository");
472        }
473    }
474
475    println!("{project_name} initialized");
476
477    Ok(())
478}
479
480#[allow(clippy::too_many_arguments)]
481pub fn build(
482    cfg_override: &ConfigOverride,
483    no_idl: bool,
484    idl: Option<String>,
485    idl_ts: Option<String>,
486    verifiable: bool,
487    skip_lint: bool,
488    program_name: Option<String>,
489    solana_version: Option<String>,
490    docker_image: Option<String>,
491    bootstrap: BootstrapMode,
492    stdout: Option<File>,
493    stderr: Option<File>,
494    env_vars: Vec<String>,
495    cargo_args: Vec<String>,
496    no_docs: bool,
497    arch: ProgramArch,
498    rebuild_types: bool,
499) -> Result<()> {
500    let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
501    let types_path = "crates/types/src";
502
503    // If rebuild_types is true and the types directory exists, remove it
504    if rebuild_types && Path::new(types_path).exists() {
505        fs::remove_dir_all(
506            PathBuf::from(types_path)
507                .parent()
508                .ok_or_else(|| anyhow::format_err!("Failed to remove types directory"))?,
509        )?;
510    }
511    create_dir_all(types_path)?;
512    build_dynamic_types(cfg, cfg_override, types_path)?;
513
514    // Build the programs
515    anchor_cli::build(
516        cfg_override,
517        no_idl,
518        idl,
519        idl_ts,
520        verifiable,
521        skip_lint,
522        program_name,
523        solana_version,
524        docker_image,
525        bootstrap,
526        stdout,
527        stderr,
528        env_vars,
529        cargo_args,
530        no_docs,
531        arch,
532    )
533}
534
535// Install node modules
536fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
537    let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
538        "cmd"
539    } else {
540        cmd
541    });
542    if cfg!(target_os = "windows") {
543        command.arg(format!("/C {} install", cmd));
544    } else {
545        command.arg("install");
546    }
547    command
548        .stdout(Stdio::inherit())
549        .stderr(Stdio::inherit())
550        .output()
551        .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
552}
553
554fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result<String> {
555    let url = match Config::discover(cfg_override)? {
556        Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
557        None => {
558            if let Some(cluster) = cfg_override.cluster.clone() {
559                cluster.url().to_string()
560            } else {
561                config::get_solana_cfg_url()?
562            }
563        }
564    };
565    Ok(url)
566}
567
568fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String {
569    let is_localnet = cfg.provider.cluster == Cluster::Localnet;
570    match is_localnet {
571        // Cluster is Localnet, assume the intent is to use the configuration
572        // for solana-test-validator
573        true => test_validator_rpc_url(test_validator),
574        false => cfg.provider.cluster.url().to_string(),
575    }
576}
577
578// Return the URL that solana-test-validator should be running on given the
579// configuration
580fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
581    match test_validator {
582        Some(TestValidator {
583            validator: Some(validator),
584            ..
585        }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
586        _ => "http://127.0.0.1:8899".to_string(),
587    }
588}
589
590fn build_dynamic_types(
591    cfg: WithPath<Config>,
592    cfg_override: &ConfigOverride,
593    types_path: &str,
594) -> Result<()> {
595    let cur_dir = std::env::current_dir()?;
596    for p in cfg.get_rust_program_list()? {
597        process_program_path(&p, cfg_override, types_path)?;
598    }
599    let types_path = PathBuf::from(types_path);
600    let cargo_path = types_path
601        .parent()
602        .unwrap_or(&types_path)
603        .join("Cargo.toml");
604    if !cargo_path.exists() {
605        let mut file = File::create(cargo_path)?;
606        file.write_all(templates::workspace::types_cargo_toml().as_bytes())?;
607    }
608    std::env::set_current_dir(cur_dir)?;
609    Ok(())
610}
611
612fn process_program_path(
613    program_path: &Path,
614    cfg_override: &ConfigOverride,
615    types_path: &str,
616) -> Result<()> {
617    let lib_rs_path = Path::new(types_path).join("lib.rs");
618    let file = File::open(program_path.join("src").join("lib.rs"))?;
619    let lines = io::BufReader::new(file).lines();
620    let mut contains_dynamic_components = false;
621    for line in lines.map_while(Result::ok) {
622        if let Some(component_id) = extract_component_id(&line) {
623            let file_path = PathBuf::from(format!("{}/component_{}.rs", types_path, component_id));
624            if !file_path.exists() {
625                println!("Generating type for Component: {}", component_id);
626                generate_component_type_file(&file_path, cfg_override, component_id)?;
627                append_component_to_lib_rs(&lib_rs_path, component_id)?;
628            }
629            contains_dynamic_components = true;
630        }
631    }
632    if contains_dynamic_components {
633        let program_name = program_path.file_name().unwrap().to_str().unwrap();
634        add_types_crate_dependency(program_name, &types_path.replace("/src", ""))?;
635    }
636
637    Ok(())
638}
639
640fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<()> {
641    std::process::Command::new("cargo")
642        .arg("add")
643        .arg("--package")
644        .arg(program_name)
645        .arg("--path")
646        .arg(types_path)
647        .stdout(Stdio::null())
648        .stderr(Stdio::null())
649        .spawn()
650        .map_err(|e| {
651            anyhow::format_err!(
652                "error adding types as dependency to the program: {}",
653                e.to_string()
654            )
655        })?;
656    Ok(())
657}