lib.rs 21 KB

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