Skip to content

Commit e802f53

Browse files
✨ Dynamic types resolver (#24)
* ✨ Dynamic types resolver * ♻️ Code Refactoring * ♻️ Code Refactor * ♻️ Use bolt_lang in macro definition
1 parent 7d378c6 commit e802f53

File tree

13 files changed

+492
-76
lines changed

13 files changed

+492
-76
lines changed

Anchor.toml

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
seeds = true
55
skip-lint = false
66

7+
[programs.devnet]
8+
world = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"
9+
710
[programs.localnet]
811
bolt-component = "CmP2djJgABZ4cRokm4ndxuq6LerqpNHLBsaUv2XKEJua"
912
bolt-system = "7X4EFsDJ5aYTcEjKzJ94rD8FRKgQeXC89fkpeTS4KaqP"
1013
position = "Fn1JzzEdyb55fsyduWS94mYHizGhJZuhvjX6DVvrmGbQ"
11-
velocity = "CbHEFbSQdRN4Wnoby9r16umnJ1zWbULBHg4yqzGQonU1"
1214
system-apply-velocity = "6LHhFVwif6N9Po3jHtSmMVtPjF6zRfL3xMosSzcrQAS8"
1315
system-fly = "HT2YawJjkNmqWcLNfPAMvNsLdWwPvvvbKA5bpMw4eUpq"
1416
system-simple-movement = "FSa6qoJXFBR3a7ThQkTAMrC15p6NkchPEjBdd4n6dXxA"
15-
world = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"
16-
17-
[programs.devnet]
17+
velocity = "CbHEFbSQdRN4Wnoby9r16umnJ1zWbULBHg4yqzGQonU1"
1818
world = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"
1919

2020
[registry]
@@ -24,17 +24,8 @@ url = "https://api.apr.dev"
2424
cluster = "localnet"
2525
wallet = "./tests/fixtures/provider.json"
2626

27+
[workspace]
28+
members = ["programs/bolt-component", "programs/bolt-system", "programs/world", "examples/component-position", "examples/component-velocity", "examples/system-apply-velocity", "examples/system-fly", "examples/system-simple-movement"]
29+
2730
[scripts]
2831
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/bolt.ts"
29-
30-
[workspace]
31-
members = [
32-
"programs/bolt-component",
33-
"programs/bolt-system",
34-
"programs/world",
35-
"examples/component-position",
36-
"examples/component-velocity",
37-
"examples/system-apply-velocity",
38-
"examples/system-fly",
39-
"examples/system-simple-movement",
40-
]

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ dev = []
2323
[dependencies]
2424
anchor-cli = { git = "https://github.com/coral-xyz/anchor.git" }
2525
anchor-client = { git = "https://github.com/coral-xyz/anchor.git" }
26+
anchor-syn = { git = "https://github.com/coral-xyz/anchor.git" }
2627
anyhow = { workspace = true }
28+
serde_json = { workspace = true }
2729
heck = { workspace = true }
2830
clap = { workspace = true }
29-
syn = { workspace = true, features = ["full", "extra-traits"] }
31+
syn = { workspace = true, features = ["full", "extra-traits"] }

cli/src/lib.rs

Lines changed: 192 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
mod rust_template;
22

33
use crate::rust_template::{create_component, create_system};
4+
use anchor_cli::config;
45
use anchor_cli::config::{
56
BootstrapMode, Config, ConfigOverride, GenesisEntry, ProgramArch, ProgramDeployment,
67
TestValidator, Validator, WithPath,
78
};
89
use anchor_client::Cluster;
10+
use anchor_syn::idl::types::Idl;
911
use anyhow::{anyhow, Result};
1012
use clap::{Parser, Subcommand};
1113
use heck::{ToKebabCase, ToSnakeCase};
1214
use std::collections::BTreeMap;
13-
use std::fs::{self, File};
15+
use std::fs::{self, create_dir_all, File, OpenOptions};
1416
use std::io::Write;
17+
use std::io::{self, BufRead};
18+
use std::path::{Path, PathBuf};
1519
use std::process::Stdio;
1620

1721
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -49,6 +53,9 @@ pub struct SystemCommand {
4953
#[derive(Debug, Parser)]
5054
#[clap(version = VERSION)]
5155
pub struct Opts {
56+
/// Rebuild the auto-generated types
57+
#[clap(global = true, long, action)]
58+
pub rebuild_types: bool,
5259
#[clap(flatten)]
5360
pub cfg_override: ConfigOverride,
5461
#[clap(subcommand)]
@@ -105,6 +112,7 @@ pub fn entry(opts: Opts) -> Result<()> {
105112
cargo_args,
106113
no_docs,
107114
arch,
115+
opts.rebuild_types,
108116
),
109117
_ => {
110118
let opts = anchor_cli::Opts {
@@ -415,7 +423,23 @@ pub fn build(
415423
cargo_args: Vec<String>,
416424
no_docs: bool,
417425
arch: ProgramArch,
426+
rebuild_types: bool,
418427
) -> Result<()> {
428+
let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
429+
let types_path = "crates/types/src";
430+
431+
// If rebuild_types is true and the types directory exists, remove it
432+
if rebuild_types && Path::new(types_path).exists() {
433+
fs::remove_dir_all(
434+
PathBuf::from(types_path)
435+
.parent()
436+
.ok_or_else(|| anyhow::format_err!("Failed to remove types directory"))?,
437+
)?;
438+
}
439+
create_dir_all(types_path)?;
440+
build_dynamic_types(cfg, cfg_override, types_path)?;
441+
442+
// Build the programs
419443
anchor_cli::build(
420444
cfg_override,
421445
idl,
@@ -472,9 +496,9 @@ fn new_component(cfg_override: &ConfigOverride, name: String) -> Result<()> {
472496

473497
programs.insert(
474498
name.clone(),
475-
anchor_cli::config::ProgramDeployment {
499+
ProgramDeployment {
476500
address: {
477-
rust_template::create_component(&name)?;
501+
create_component(&name)?;
478502
anchor_cli::rust_template::get_or_create_program_id(&name)
479503
},
480504
path: None,
@@ -581,3 +605,168 @@ fn set_workspace_dir_or_exit() {
581605
}
582606
}
583607
}
608+
609+
fn discover_cluster_url(cfg_override: &ConfigOverride) -> Result<String> {
610+
let url = match Config::discover(cfg_override)? {
611+
Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
612+
None => {
613+
if let Some(cluster) = cfg_override.cluster.clone() {
614+
cluster.url().to_string()
615+
} else {
616+
config::get_solana_cfg_url()?
617+
}
618+
}
619+
};
620+
Ok(url)
621+
}
622+
623+
fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String {
624+
let is_localnet = cfg.provider.cluster == Cluster::Localnet;
625+
match is_localnet {
626+
// Cluster is Localnet, assume the intent is to use the configuration
627+
// for solana-test-validator
628+
true => test_validator_rpc_url(test_validator),
629+
false => cfg.provider.cluster.url().to_string(),
630+
}
631+
}
632+
633+
// Return the URL that solana-test-validator should be running on given the
634+
// configuration
635+
fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
636+
match test_validator {
637+
Some(TestValidator {
638+
validator: Some(validator),
639+
..
640+
}) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
641+
_ => "http://127.0.0.1:8899".to_string(),
642+
}
643+
}
644+
645+
fn build_dynamic_types(
646+
cfg: WithPath<Config>,
647+
cfg_override: &ConfigOverride,
648+
types_path: &str,
649+
) -> Result<()> {
650+
let cur_dir = std::env::current_dir()?;
651+
for p in cfg.get_rust_program_list()? {
652+
process_program_path(&p, cfg_override, types_path)?;
653+
}
654+
let types_path = PathBuf::from(types_path);
655+
let cargo_path = types_path
656+
.parent()
657+
.unwrap_or(&types_path)
658+
.join("Cargo.toml");
659+
if !cargo_path.exists() {
660+
let mut file = File::create(cargo_path)?;
661+
file.write_all(rust_template::types_cargo_toml().as_bytes())?;
662+
}
663+
std::env::set_current_dir(cur_dir)?;
664+
Ok(())
665+
}
666+
667+
fn process_program_path(
668+
program_path: &Path,
669+
cfg_override: &ConfigOverride,
670+
types_path: &str,
671+
) -> Result<()> {
672+
let lib_rs_path = Path::new(types_path).join("lib.rs");
673+
let file = File::open(program_path.join("src").join("lib.rs"))?;
674+
let lines = io::BufReader::new(file).lines();
675+
let mut contains_dynamic_components = false;
676+
for line in lines.map_while(Result::ok) {
677+
if let Some(component_id) = extract_component_id(&line) {
678+
let file_path = PathBuf::from(format!("{}/component_{}.rs", types_path, component_id));
679+
if !file_path.exists() {
680+
println!("Generating type for Component: {}", component_id);
681+
generate_component_type_file(&file_path, cfg_override, component_id)?;
682+
append_component_to_lib_rs(&lib_rs_path, component_id)?;
683+
}
684+
contains_dynamic_components = true;
685+
}
686+
}
687+
if contains_dynamic_components {
688+
let program_name = program_path.file_name().unwrap().to_str().unwrap();
689+
add_types_crate_dependency(program_name, &types_path.replace("/src", ""))?;
690+
}
691+
692+
Ok(())
693+
}
694+
695+
fn add_types_crate_dependency(program_name: &str, types_path: &str) -> Result<()> {
696+
std::process::Command::new("cargo")
697+
.arg("add")
698+
.arg("--package")
699+
.arg(program_name)
700+
.arg("--path")
701+
.arg(types_path)
702+
.stdout(Stdio::null())
703+
.stderr(Stdio::null())
704+
.spawn()
705+
.map_err(|e| {
706+
anyhow::format_err!(
707+
"error adding types as dependency to the program: {}",
708+
e.to_string()
709+
)
710+
})?;
711+
Ok(())
712+
}
713+
714+
fn extract_component_id(line: &str) -> Option<&str> {
715+
let component_id_marker = "#[component_id(";
716+
line.find(component_id_marker).map(|start| {
717+
let start = start + component_id_marker.len();
718+
let end = line[start..].find(')').unwrap() + start;
719+
line[start..end].trim_matches('"')
720+
})
721+
}
722+
723+
fn fetch_idl_for_component(component_id: &str, url: &str) -> Result<String> {
724+
let output = std::process::Command::new("bolt")
725+
.arg("idl")
726+
.arg("fetch")
727+
.arg(component_id)
728+
.arg("--provider.cluster")
729+
.arg(url)
730+
.stdout(Stdio::piped())
731+
.stderr(Stdio::piped())
732+
.output()?;
733+
734+
if output.status.success() {
735+
let idl_string = String::from_utf8(output.stdout)
736+
.map_err(|e| anyhow!("Failed to decode IDL output as UTF-8: {}", e))?
737+
.to_string();
738+
Ok(idl_string)
739+
} else {
740+
let error_message = String::from_utf8(output.stderr)
741+
.unwrap_or(format!(
742+
"Error trying to dynamically generate the type \
743+
for component {}, unable to fetch the idl. \nEnsure that the idl is available. Specify \
744+
the appropriate cluster using the --provider.cluster option",
745+
component_id
746+
))
747+
.to_string();
748+
Err(anyhow!("Command failed with error: {}", error_message))
749+
}
750+
}
751+
752+
fn generate_component_type_file(
753+
file_path: &Path,
754+
cfg_override: &ConfigOverride,
755+
component_id: &str,
756+
) -> Result<()> {
757+
let url = discover_cluster_url(cfg_override)?;
758+
let idl_string = fetch_idl_for_component(component_id, &url)?;
759+
let idl: Idl = serde_json::from_str(&idl_string)?;
760+
let mut file = File::create(file_path)?;
761+
file.write_all(rust_template::component_type(&idl, component_id)?.as_bytes())?;
762+
Ok(())
763+
}
764+
765+
fn append_component_to_lib_rs(lib_rs_path: &Path, component_id: &str) -> Result<()> {
766+
let mut file = OpenOptions::new()
767+
.create(true)
768+
.append(true)
769+
.open(lib_rs_path)?;
770+
file.write_all(rust_template::component_type_import(component_id).as_bytes())?;
771+
Ok(())
772+
}

0 commit comments

Comments
 (0)