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 #[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 #[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#[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 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 let extra_keywords = ["async", "await", "try"];
226 let component_name = "position";
227 let system_name = "movement";
228 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 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 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 fs::write(".gitignore", templates::workspace::git_ignore())?;
342
343 fs::write(".prettierignore", templates::workspace::prettier_ignore())?;
345
346 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 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 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 fs::create_dir_all("tests/fixtures")?;
390 fs::create_dir_all("migrations")?;
392
393 fs::write(
395 "tests/fixtures/registry.json",
396 rust_template::registry_account(),
397 )?;
398
399 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 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 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 && 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 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
535fn 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 true => test_validator_rpc_url(test_validator),
574 false => cfg.provider.cluster.url().to_string(),
575 }
576}
577
578fn 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}