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