1use crate::{get_keypair, is_hidden, keys_sync, DEFAULT_RPC_PORT};
2use anchor_client::Cluster;
3use anchor_lang_idl::types::Idl;
4use anyhow::{anyhow, Context, Error, Result};
5use clap::{Parser, ValueEnum};
6use dirs::home_dir;
7use heck::ToSnakeCase;
8use reqwest::Url;
9use serde::de::{self, MapAccess, Visitor};
10use serde::ser::SerializeMap;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE};
13use solana_sdk::clock::Slot;
14use solana_sdk::pubkey::Pubkey;
15use solana_sdk::signature::{Keypair, Signer};
16use std::collections::{BTreeMap, HashMap};
17use std::convert::TryFrom;
18use std::fs::{self, File};
19use std::io::prelude::*;
20use std::marker::PhantomData;
21use std::ops::Deref;
22use std::path::Path;
23use std::path::PathBuf;
24use std::str::FromStr;
25use std::{fmt, io};
26use walkdir::WalkDir;
27
28pub trait Merge: Sized {
29 fn merge(&mut self, _other: Self) {}
30}
31
32#[derive(Default, Debug, Parser)]
33pub struct ConfigOverride {
34 #[clap(global = true, long = "provider.cluster")]
36 pub cluster: Option<Cluster>,
37 #[clap(global = true, long = "provider.wallet")]
39 pub wallet: Option<WalletPath>,
40}
41
42#[derive(Debug)]
43pub struct WithPath<T> {
44 inner: T,
45 path: PathBuf,
46}
47
48impl<T> WithPath<T> {
49 pub fn new(inner: T, path: PathBuf) -> Self {
50 Self { inner, path }
51 }
52
53 pub fn path(&self) -> &PathBuf {
54 &self.path
55 }
56
57 pub fn into_inner(self) -> T {
58 self.inner
59 }
60}
61
62impl<T> std::convert::AsRef<T> for WithPath<T> {
63 fn as_ref(&self) -> &T {
64 &self.inner
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
69pub struct Manifest(cargo_toml::Manifest);
70
71impl Manifest {
72 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
73 cargo_toml::Manifest::from_path(&p)
74 .map(Manifest)
75 .map_err(anyhow::Error::from)
76 .with_context(|| format!("Error reading manifest from path: {}", p.as_ref().display()))
77 }
78
79 pub fn lib_name(&self) -> Result<String> {
80 match &self.lib {
81 Some(cargo_toml::Product {
82 name: Some(name), ..
83 }) => Ok(name.to_owned()),
84 _ => self
85 .package
86 .as_ref()
87 .ok_or_else(|| anyhow!("package section not provided"))
88 .map(|pkg| pkg.name.to_snake_case()),
89 }
90 }
91
92 pub fn version(&self) -> String {
93 match &self.package {
94 Some(package) => package.version().to_string(),
95 _ => "0.0.0".to_string(),
96 }
97 }
98
99 pub fn discover() -> Result<Option<WithPath<Manifest>>> {
101 Manifest::discover_from_path(std::env::current_dir()?)
102 }
103
104 pub fn discover_from_path(start_from: PathBuf) -> Result<Option<WithPath<Manifest>>> {
106 let mut cwd_opt = Some(start_from.as_path());
107
108 while let Some(cwd) = cwd_opt {
109 let mut anchor_toml = false;
110
111 for f in fs::read_dir(cwd).with_context(|| {
112 format!("Error reading the directory with path: {}", cwd.display())
113 })? {
114 let p = f
115 .with_context(|| {
116 format!("Error reading the directory with path: {}", cwd.display())
117 })?
118 .path();
119 if let Some(filename) = p.file_name().and_then(|name| name.to_str()) {
120 if filename == "Cargo.toml" {
121 return Ok(Some(WithPath::new(Manifest::from_path(&p)?, p)));
122 }
123 if filename == "Anchor.toml" {
124 anchor_toml = true;
125 }
126 }
127 }
128
129 if anchor_toml {
131 break;
132 }
133
134 cwd_opt = cwd.parent();
135 }
136
137 Ok(None)
138 }
139}
140
141impl Deref for Manifest {
142 type Target = cargo_toml::Manifest;
143
144 fn deref(&self) -> &Self::Target {
145 &self.0
146 }
147}
148
149impl WithPath<Config> {
150 pub fn get_rust_program_list(&self) -> Result<Vec<PathBuf>> {
151 let (members, exclude) = self.canonicalize_workspace()?;
153
154 let program_paths: Vec<PathBuf> = {
159 if members.is_empty() {
160 let path = self.path().parent().unwrap().join("programs");
161 if let Ok(entries) = fs::read_dir(path) {
162 entries
163 .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false))
164 .map(|dir| dir.map(|d| d.path().canonicalize().unwrap()))
165 .collect::<Vec<Result<PathBuf, std::io::Error>>>()
166 .into_iter()
167 .collect::<Result<Vec<PathBuf>, std::io::Error>>()?
168 } else {
169 Vec::new()
170 }
171 } else {
172 members
173 }
174 };
175
176 Ok(program_paths
178 .into_iter()
179 .filter(|m| !exclude.contains(m))
180 .collect())
181 }
182
183 pub fn read_all_programs(&self) -> Result<Vec<Program>> {
184 let mut r = vec![];
185 for path in self.get_rust_program_list()? {
186 let cargo = Manifest::from_path(path.join("Cargo.toml"))?;
187 let lib_name = cargo.lib_name()?;
188
189 let idl_filepath = Path::new("target")
190 .join("idl")
191 .join(&lib_name)
192 .with_extension("json");
193 let idl = fs::read(idl_filepath)
194 .ok()
195 .map(|bytes| serde_json::from_reader(&*bytes))
196 .transpose()?;
197
198 r.push(Program {
199 lib_name,
200 path,
201 idl,
202 });
203 }
204 Ok(r)
205 }
206
207 pub fn get_programs(&self, name: Option<String>) -> Result<Vec<Program>> {
211 let programs = self.read_all_programs()?;
212 let programs = match name {
213 Some(name) => vec![programs
214 .into_iter()
215 .find(|program| {
216 name == program.lib_name
217 || name == program.path.file_name().unwrap().to_str().unwrap()
218 })
219 .ok_or_else(|| anyhow!("Program {name} not found"))?],
220 None => programs,
221 };
222
223 Ok(programs)
224 }
225
226 pub fn get_program(&self, name: &str) -> Result<Program> {
228 self.get_programs(Some(name.to_owned()))?
229 .into_iter()
230 .next()
231 .ok_or_else(|| anyhow!("Expected a program"))
232 }
233
234 pub fn canonicalize_workspace(&self) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
235 let members = self.process_paths(&self.workspace.members)?;
236 let exclude = self.process_paths(&self.workspace.exclude)?;
237 Ok((members, exclude))
238 }
239
240 fn process_paths(&self, paths: &[String]) -> Result<Vec<PathBuf>, Error> {
241 let base_path = self.path().parent().unwrap();
242 paths
243 .iter()
244 .flat_map(|m| {
245 let path = base_path.join(m);
246 if m.ends_with("/*") {
247 let dir = path.parent().unwrap();
248 match fs::read_dir(dir) {
249 Ok(entries) => entries
250 .filter_map(|entry| entry.ok())
251 .map(|entry| self.process_single_path(&entry.path()))
252 .collect(),
253 Err(e) => vec![Err(Error::new(io::Error::other(format!(
254 "Error reading directory {dir:?}: {e}"
255 ))))],
256 }
257 } else {
258 vec![self.process_single_path(&path)]
259 }
260 })
261 .collect()
262 }
263
264 fn process_single_path(&self, path: &PathBuf) -> Result<PathBuf, Error> {
265 path.canonicalize().map_err(|e| {
266 Error::new(io::Error::other(format!(
267 "Error canonicalizing path {path:?}: {e}"
268 )))
269 })
270 }
271}
272
273impl<T> std::ops::Deref for WithPath<T> {
274 type Target = T;
275 fn deref(&self) -> &Self::Target {
276 &self.inner
277 }
278}
279
280impl<T> std::ops::DerefMut for WithPath<T> {
281 fn deref_mut(&mut self) -> &mut Self::Target {
282 &mut self.inner
283 }
284}
285
286#[derive(Debug, Default)]
287pub struct Config {
288 pub toolchain: ToolchainConfig,
289 pub features: FeaturesConfig,
290 pub registry: RegistryConfig,
291 pub provider: ProviderConfig,
292 pub programs: ProgramsConfig,
293 pub scripts: ScriptsConfig,
294 pub workspace: WorkspaceConfig,
295 pub test_validator: Option<TestValidator>,
299 pub test_config: Option<TestConfig>,
300}
301
302#[derive(Default, Clone, Debug, Serialize, Deserialize)]
303pub struct ToolchainConfig {
304 pub anchor_version: Option<String>,
305 pub solana_version: Option<String>,
306 pub package_manager: Option<PackageManager>,
307}
308
309#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize)]
311#[serde(rename_all = "lowercase")]
312pub enum PackageManager {
313 NPM,
315 #[default]
317 Yarn,
318 PNPM,
320 Bun,
322}
323
324impl std::fmt::Display for PackageManager {
325 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
326 let pkg_manager_str = match self {
327 PackageManager::NPM => "npm",
328 PackageManager::Yarn => "yarn",
329 PackageManager::PNPM => "pnpm",
330 PackageManager::Bun => "bun",
331 };
332
333 write!(f, "{pkg_manager_str}")
334 }
335}
336
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub struct FeaturesConfig {
339 #[serde(default = "FeaturesConfig::get_default_resolution")]
343 pub resolution: bool,
344 #[serde(default, rename = "skip-lint")]
346 pub skip_lint: bool,
347}
348
349impl FeaturesConfig {
350 fn get_default_resolution() -> bool {
351 true
352 }
353}
354
355impl Default for FeaturesConfig {
356 fn default() -> Self {
357 Self {
358 resolution: Self::get_default_resolution(),
359 skip_lint: false,
360 }
361 }
362}
363
364#[derive(Clone, Debug, Serialize, Deserialize)]
365pub struct RegistryConfig {
366 pub url: String,
367}
368
369impl Default for RegistryConfig {
370 fn default() -> Self {
371 Self {
372 url: "https://api.apr.dev".to_string(),
373 }
374 }
375}
376
377#[derive(Debug, Default)]
378pub struct ProviderConfig {
379 pub cluster: Cluster,
380 pub wallet: WalletPath,
381}
382
383pub type ScriptsConfig = BTreeMap<String, String>;
384
385pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
386
387#[derive(Debug, Default, Clone, Serialize, Deserialize)]
388pub struct WorkspaceConfig {
389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
390 pub members: Vec<String>,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
392 pub exclude: Vec<String>,
393 #[serde(default, skip_serializing_if = "String::is_empty")]
394 pub types: String,
395}
396
397#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug)]
398pub enum BootstrapMode {
399 None,
400 Debian,
401}
402
403#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug)]
404pub enum ProgramArch {
405 Bpf,
406 Sbf,
407}
408impl ProgramArch {
409 pub fn build_subcommand(&self) -> &str {
410 match self {
411 Self::Bpf => "build-bpf",
412 Self::Sbf => "build-sbf",
413 }
414 }
415}
416
417#[derive(Debug, Clone)]
418pub struct BuildConfig {
419 pub verifiable: bool,
420 pub solana_version: Option<String>,
421 pub docker_image: String,
422 pub bootstrap: BootstrapMode,
423}
424
425impl Config {
426 pub fn add_test_config(
427 &mut self,
428 root: impl AsRef<Path>,
429 test_paths: Vec<PathBuf>,
430 ) -> Result<()> {
431 self.test_config = TestConfig::discover(root, test_paths)?;
432 Ok(())
433 }
434
435 pub fn docker(&self) -> String {
436 let version = self
437 .toolchain
438 .anchor_version
439 .as_deref()
440 .unwrap_or(crate::DOCKER_BUILDER_VERSION);
441 format!("solanafoundation/anchor:v{version}")
442 }
443
444 pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
445 Config::_discover().map(|opt| {
446 opt.map(|mut cfg| {
447 if let Some(cluster) = cfg_override.cluster.clone() {
448 cfg.provider.cluster = cluster;
449 }
450 if let Some(wallet) = cfg_override.wallet.clone() {
451 cfg.provider.wallet = wallet;
452 }
453 cfg
454 })
455 })
456 }
457
458 fn _discover() -> Result<Option<WithPath<Config>>> {
460 let _cwd = std::env::current_dir()?;
461 let mut cwd_opt = Some(_cwd.as_path());
462
463 while let Some(cwd) = cwd_opt {
464 for f in fs::read_dir(cwd).with_context(|| {
465 format!("Error reading the directory with path: {}", cwd.display())
466 })? {
467 let p = f
468 .with_context(|| {
469 format!("Error reading the directory with path: {}", cwd.display())
470 })?
471 .path();
472 if let Some(filename) = p.file_name() {
473 if filename.to_str() == Some("Anchor.toml") {
474 let mut cfg = Config::from_path(&p)?;
476 let deploy_dir = p.parent().unwrap().join("target").join("deploy");
477 if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
478 println!("Updating program ids...");
479 fs::create_dir_all(deploy_dir)?;
480 keys_sync(&ConfigOverride::default(), None)?;
481 cfg = Config::from_path(&p)?;
482 }
483
484 return Ok(Some(WithPath::new(cfg, p)));
485 }
486 }
487 }
488
489 cwd_opt = cwd.parent();
490 }
491
492 Ok(None)
493 }
494
495 fn from_path(p: impl AsRef<Path>) -> Result<Self> {
496 fs::read_to_string(&p)
497 .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))?
498 .parse::<Self>()
499 }
500
501 pub fn wallet_kp(&self) -> Result<Keypair> {
502 get_keypair(&self.provider.wallet.to_string())
503 }
504}
505
506#[derive(Debug, Serialize, Deserialize)]
507struct _Config {
508 toolchain: Option<ToolchainConfig>,
509 features: Option<FeaturesConfig>,
510 programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
511 registry: Option<RegistryConfig>,
512 provider: Provider,
513 workspace: Option<WorkspaceConfig>,
514 scripts: Option<ScriptsConfig>,
515 test: Option<_TestValidator>,
516}
517
518#[derive(Debug, Serialize, Deserialize)]
519struct Provider {
520 #[serde(serialize_with = "ser_cluster", deserialize_with = "des_cluster")]
521 cluster: Cluster,
522 wallet: String,
523}
524
525fn ser_cluster<S: Serializer>(cluster: &Cluster, s: S) -> Result<S::Ok, S::Error> {
526 match cluster {
527 Cluster::Custom(http, ws) => {
528 match (Url::parse(http), Url::parse(ws)) {
529 (Ok(h), Ok(w)) if h.domain() == w.domain() => s.serialize_str(http),
531 _ => {
532 let mut map = s.serialize_map(Some(2))?;
533 map.serialize_entry("http", http)?;
534 map.serialize_entry("ws", ws)?;
535 map.end()
536 }
537 }
538 }
539 _ => s.serialize_str(&cluster.to_string()),
540 }
541}
542
543fn des_cluster<'de, D>(deserializer: D) -> Result<Cluster, D::Error>
544where
545 D: Deserializer<'de>,
546{
547 struct StringOrCustomCluster(PhantomData<fn() -> Cluster>);
548
549 impl<'de> Visitor<'de> for StringOrCustomCluster {
550 type Value = Cluster;
551
552 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
553 formatter.write_str("string or map")
554 }
555
556 fn visit_str<E>(self, value: &str) -> Result<Cluster, E>
557 where
558 E: de::Error,
559 {
560 value.parse().map_err(de::Error::custom)
561 }
562
563 fn visit_map<M>(self, mut map: M) -> Result<Cluster, M::Error>
564 where
565 M: MapAccess<'de>,
566 {
567 if let (Some((http_key, http_value)), Some((ws_key, ws_value))) = (
569 map.next_entry::<String, String>()?,
570 map.next_entry::<String, String>()?,
571 ) {
572 if http_key != "http" || ws_key != "ws" {
574 return Err(de::Error::custom("Invalid key"));
575 }
576
577 Url::parse(&http_value).map_err(de::Error::custom)?;
579 Url::parse(&ws_value).map_err(de::Error::custom)?;
580
581 Ok(Cluster::Custom(http_value, ws_value))
582 } else {
583 Err(de::Error::custom("Invalid entry"))
584 }
585 }
586 }
587 deserializer.deserialize_any(StringOrCustomCluster(PhantomData))
588}
589
590impl fmt::Display for Config {
591 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592 let programs = {
593 let c = ser_programs(&self.programs);
594 if c.is_empty() {
595 None
596 } else {
597 Some(c)
598 }
599 };
600 let cfg = _Config {
601 toolchain: Some(self.toolchain.clone()),
602 features: Some(self.features.clone()),
603 registry: Some(self.registry.clone()),
604 provider: Provider {
605 cluster: self.provider.cluster.clone(),
606 wallet: self.provider.wallet.stringify_with_tilde(),
607 },
608 test: self.test_validator.clone().map(Into::into),
609 scripts: match self.scripts.is_empty() {
610 true => None,
611 false => Some(self.scripts.clone()),
612 },
613 programs,
614 workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
615 .then(|| self.workspace.clone()),
616 };
617
618 let cfg = toml::to_string(&cfg).expect("Must be well formed");
619 write!(f, "{cfg}")
620 }
621}
622
623impl FromStr for Config {
624 type Err = Error;
625
626 fn from_str(s: &str) -> Result<Self, Self::Err> {
627 let cfg: _Config =
628 toml::from_str(s).map_err(|e| anyhow!("Unable to deserialize config: {e}"))?;
629 Ok(Config {
630 toolchain: cfg.toolchain.unwrap_or_default(),
631 features: cfg.features.unwrap_or_default(),
632 registry: cfg.registry.unwrap_or_default(),
633 provider: ProviderConfig {
634 cluster: cfg.provider.cluster,
635 wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
636 },
637 scripts: cfg.scripts.unwrap_or_default(),
638 test_validator: cfg.test.map(Into::into),
639 test_config: None,
640 programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
641 workspace: cfg.workspace.unwrap_or_default(),
642 })
643 }
644}
645
646pub fn get_solana_cfg_url() -> Result<String, io::Error> {
647 let config_file = CONFIG_FILE.as_ref().ok_or_else(|| {
648 io::Error::new(
649 io::ErrorKind::NotFound,
650 "Default Solana config was not found",
651 )
652 })?;
653 SolanaConfig::load(config_file).map(|config| config.json_rpc_url)
654}
655
656fn ser_programs(
657 programs: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
658) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
659 programs
660 .iter()
661 .map(|(cluster, programs)| {
662 let cluster = cluster.to_string();
663 let programs = programs
664 .iter()
665 .map(|(name, deployment)| {
666 (
667 name.clone(),
668 to_value(&_ProgramDeployment::from(deployment)),
669 )
670 })
671 .collect::<BTreeMap<String, serde_json::Value>>();
672 (cluster, programs)
673 })
674 .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
675}
676
677fn to_value(dep: &_ProgramDeployment) -> serde_json::Value {
678 if dep.path.is_none() && dep.idl.is_none() {
679 return serde_json::Value::String(dep.address.to_string());
680 }
681 serde_json::to_value(dep).unwrap()
682}
683
684fn deser_programs(
685 programs: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
686) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
687 programs
688 .iter()
689 .map(|(cluster, programs)| {
690 let cluster: Cluster = cluster.parse()?;
691 let programs = programs
692 .iter()
693 .map(|(name, program_id)| {
694 Ok((
695 name.clone(),
696 ProgramDeployment::try_from(match &program_id {
697 serde_json::Value::String(address) => _ProgramDeployment {
698 address: address.parse()?,
699 path: None,
700 idl: None,
701 },
702
703 serde_json::Value::Object(_) => {
704 serde_json::from_value(program_id.clone())
705 .map_err(|_| anyhow!("Unable to read toml"))?
706 }
707 _ => return Err(anyhow!("Invalid toml type")),
708 })?,
709 ))
710 })
711 .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
712 Ok((cluster, programs))
713 })
714 .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
715}
716
717#[derive(Default, Debug, Clone, Serialize, Deserialize)]
718pub struct TestValidator {
719 pub genesis: Option<Vec<GenesisEntry>>,
720 pub validator: Option<Validator>,
721 pub startup_wait: i32,
722 pub shutdown_wait: i32,
723 pub upgradeable: bool,
724}
725
726#[derive(Default, Debug, Clone, Serialize, Deserialize)]
727pub struct _TestValidator {
728 #[serde(skip_serializing_if = "Option::is_none")]
729 pub genesis: Option<Vec<GenesisEntry>>,
730 #[serde(skip_serializing_if = "Option::is_none")]
731 pub validator: Option<_Validator>,
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub startup_wait: Option<i32>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub shutdown_wait: Option<i32>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub upgradeable: Option<bool>,
738}
739
740pub const STARTUP_WAIT: i32 = 5000;
741pub const SHUTDOWN_WAIT: i32 = 2000;
742
743impl From<_TestValidator> for TestValidator {
744 fn from(_test_validator: _TestValidator) -> Self {
745 Self {
746 shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
747 startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
748 genesis: _test_validator.genesis,
749 validator: _test_validator.validator.map(Into::into),
750 upgradeable: _test_validator.upgradeable.unwrap_or(false),
751 }
752 }
753}
754
755impl From<TestValidator> for _TestValidator {
756 fn from(test_validator: TestValidator) -> Self {
757 Self {
758 shutdown_wait: Some(test_validator.shutdown_wait),
759 startup_wait: Some(test_validator.startup_wait),
760 genesis: test_validator.genesis,
761 validator: test_validator.validator.map(Into::into),
762 upgradeable: Some(test_validator.upgradeable),
763 }
764 }
765}
766
767#[derive(Debug, Clone)]
768pub struct TestConfig {
769 pub test_suite_configs: HashMap<PathBuf, TestToml>,
770}
771
772impl Deref for TestConfig {
773 type Target = HashMap<PathBuf, TestToml>;
774
775 fn deref(&self) -> &Self::Target {
776 &self.test_suite_configs
777 }
778}
779
780impl TestConfig {
781 pub fn discover(root: impl AsRef<Path>, test_paths: Vec<PathBuf>) -> Result<Option<Self>> {
782 let walker = WalkDir::new(root).into_iter();
783 let mut test_suite_configs = HashMap::new();
784 for entry in walker.filter_entry(|e| !is_hidden(e)) {
785 let entry = entry?;
786 if entry.file_name() == "Test.toml" {
787 let entry_path = entry.path();
788 let test_toml = TestToml::from_path(entry_path)?;
789 if test_paths.is_empty() || test_paths.iter().any(|p| entry_path.starts_with(p)) {
790 test_suite_configs.insert(entry.path().into(), test_toml);
791 }
792 }
793 }
794
795 Ok(match test_suite_configs.is_empty() {
796 true => None,
797 false => Some(Self { test_suite_configs }),
798 })
799 }
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct _TestToml {
806 pub extends: Option<Vec<String>>,
807 pub test: Option<_TestValidator>,
808 pub scripts: Option<ScriptsConfig>,
809}
810
811impl _TestToml {
812 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
813 let s = fs::read_to_string(&path)?;
814 let parsed_toml: Self = toml::from_str(&s)?;
815 let mut current_toml = _TestToml {
816 extends: None,
817 test: None,
818 scripts: None,
819 };
820 if let Some(bases) = &parsed_toml.extends {
821 for base in bases {
822 let mut canonical_base = base.clone();
823 canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
824 current_toml.merge(_TestToml::from_path(&canonical_base)?);
825 }
826 }
827 current_toml.merge(parsed_toml);
828
829 if let Some(test) = &mut current_toml.test {
830 if let Some(genesis_programs) = &mut test.genesis {
831 for entry in genesis_programs {
832 entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
833 }
834 }
835 if let Some(validator) = &mut test.validator {
836 if let Some(ledger_dir) = &mut validator.ledger {
837 *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?;
838 }
839 if let Some(accounts) = &mut validator.account {
840 for entry in accounts {
841 entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
842 }
843 }
844 }
845 }
846 Ok(current_toml)
847 }
848}
849
850fn canonicalize_filepath_from_origin(
855 file_path: impl AsRef<Path>,
856 origin: impl AsRef<Path>,
857) -> Result<String> {
858 let previous_dir = std::env::current_dir()?;
859 std::env::set_current_dir(origin.as_ref().parent().unwrap())?;
860 let result = fs::canonicalize(&file_path)
861 .with_context(|| {
862 format!(
863 "Error reading (possibly relative) path: {}. If relative, this is the path that was used as the current path: {}",
864 &file_path.as_ref().display(),
865 &origin.as_ref().display()
866 )
867 })?
868 .display()
869 .to_string();
870 std::env::set_current_dir(previous_dir)?;
871 Ok(result)
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize)]
875pub struct TestToml {
876 #[serde(skip_serializing_if = "Option::is_none")]
877 pub test: Option<TestValidator>,
878 pub scripts: ScriptsConfig,
879}
880
881impl TestToml {
882 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
883 WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
884 }
885}
886
887impl Merge for _TestToml {
888 fn merge(&mut self, other: Self) {
889 let mut my_scripts = self.scripts.take();
890 match &mut my_scripts {
891 None => my_scripts = other.scripts,
892 Some(my_scripts) => {
893 if let Some(other_scripts) = other.scripts {
894 for (name, script) in other_scripts {
895 my_scripts.insert(name, script);
896 }
897 }
898 }
899 }
900
901 let mut my_test = self.test.take();
902 match &mut my_test {
903 Some(my_test) => {
904 if let Some(other_test) = other.test {
905 if let Some(startup_wait) = other_test.startup_wait {
906 my_test.startup_wait = Some(startup_wait);
907 }
908 if let Some(other_genesis) = other_test.genesis {
909 match &mut my_test.genesis {
910 Some(my_genesis) => {
911 for other_entry in other_genesis {
912 match my_genesis
913 .iter()
914 .position(|g| *g.address == other_entry.address)
915 {
916 None => my_genesis.push(other_entry),
917 Some(i) => my_genesis[i] = other_entry,
918 }
919 }
920 }
921 None => my_test.genesis = Some(other_genesis),
922 }
923 }
924 let mut my_validator = my_test.validator.take();
925 match &mut my_validator {
926 None => my_validator = other_test.validator,
927 Some(my_validator) => {
928 if let Some(other_validator) = other_test.validator {
929 my_validator.merge(other_validator)
930 }
931 }
932 }
933
934 my_test.validator = my_validator;
935 }
936 }
937 None => my_test = other.test,
938 };
939
940 *self = Self {
944 test: my_test,
945 scripts: my_scripts,
946 extends: self.extends.take(),
947 };
948 }
949}
950
951impl TryFrom<WithPath<_TestToml>> for TestToml {
952 type Error = Error;
953
954 fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
955 Ok(Self {
956 test: value.test.take().map(Into::into),
957 scripts: value
958 .scripts
959 .take()
960 .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
961 })
962 }
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize)]
966pub struct GenesisEntry {
967 pub address: String,
969 pub program: String,
971 pub upgradeable: Option<bool>,
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct CloneEntry {
977 pub address: String,
979}
980
981#[derive(Debug, Clone, Serialize, Deserialize)]
982pub struct AccountEntry {
983 pub address: String,
985 pub filename: String,
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
990pub struct AccountDirEntry {
991 pub directory: String,
993}
994
995#[derive(Debug, Default, Clone, Serialize, Deserialize)]
996pub struct _Validator {
997 #[serde(skip_serializing_if = "Option::is_none")]
999 pub account: Option<Vec<AccountEntry>>,
1000 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub account_dir: Option<Vec<AccountDirEntry>>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub bind_address: Option<String>,
1006 #[serde(skip_serializing_if = "Option::is_none")]
1008 pub clone: Option<Vec<CloneEntry>>,
1009 #[serde(skip_serializing_if = "Option::is_none")]
1011 pub dynamic_port_range: Option<String>,
1012 #[serde(skip_serializing_if = "Option::is_none")]
1014 pub faucet_port: Option<u16>,
1015 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub faucet_sol: Option<String>,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub geyser_plugin_config: Option<String>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub gossip_host: Option<String>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub gossip_port: Option<u16>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub url: Option<String>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub ledger: Option<String>,
1033 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub limit_ledger_size: Option<String>,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub rpc_port: Option<u16>,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub slots_per_epoch: Option<String>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub ticks_per_slot: Option<u16>,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub warp_slot: Option<Slot>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub deactivate_feature: Option<Vec<String>>,
1051}
1052
1053#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1054pub struct Validator {
1055 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub account: Option<Vec<AccountEntry>>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 pub account_dir: Option<Vec<AccountDirEntry>>,
1059 pub bind_address: String,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1061 pub clone: Option<Vec<CloneEntry>>,
1062 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub dynamic_port_range: Option<String>,
1064 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub faucet_port: Option<u16>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1067 pub faucet_sol: Option<String>,
1068 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub geyser_plugin_config: Option<String>,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub gossip_host: Option<String>,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1073 pub gossip_port: Option<u16>,
1074 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub url: Option<String>,
1076 pub ledger: String,
1077 #[serde(skip_serializing_if = "Option::is_none")]
1078 pub limit_ledger_size: Option<String>,
1079 pub rpc_port: u16,
1080 #[serde(skip_serializing_if = "Option::is_none")]
1081 pub slots_per_epoch: Option<String>,
1082 #[serde(skip_serializing_if = "Option::is_none")]
1083 pub ticks_per_slot: Option<u16>,
1084 #[serde(skip_serializing_if = "Option::is_none")]
1085 pub warp_slot: Option<Slot>,
1086 #[serde(skip_serializing_if = "Option::is_none")]
1087 pub deactivate_feature: Option<Vec<String>>,
1088}
1089
1090impl From<_Validator> for Validator {
1091 fn from(_validator: _Validator) -> Self {
1092 Self {
1093 account: _validator.account,
1094 account_dir: _validator.account_dir,
1095 bind_address: _validator
1096 .bind_address
1097 .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
1098 clone: _validator.clone,
1099 dynamic_port_range: _validator.dynamic_port_range,
1100 faucet_port: _validator.faucet_port,
1101 faucet_sol: _validator.faucet_sol,
1102 geyser_plugin_config: _validator.geyser_plugin_config,
1103 gossip_host: _validator.gossip_host,
1104 gossip_port: _validator.gossip_port,
1105 url: _validator.url,
1106 ledger: _validator
1107 .ledger
1108 .unwrap_or_else(|| get_default_ledger_path().display().to_string()),
1109 limit_ledger_size: _validator.limit_ledger_size,
1110 rpc_port: _validator.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1111 slots_per_epoch: _validator.slots_per_epoch,
1112 ticks_per_slot: _validator.ticks_per_slot,
1113 warp_slot: _validator.warp_slot,
1114 deactivate_feature: _validator.deactivate_feature,
1115 }
1116 }
1117}
1118
1119impl From<Validator> for _Validator {
1120 fn from(validator: Validator) -> Self {
1121 Self {
1122 account: validator.account,
1123 account_dir: validator.account_dir,
1124 bind_address: Some(validator.bind_address),
1125 clone: validator.clone,
1126 dynamic_port_range: validator.dynamic_port_range,
1127 faucet_port: validator.faucet_port,
1128 faucet_sol: validator.faucet_sol,
1129 geyser_plugin_config: validator.geyser_plugin_config,
1130 gossip_host: validator.gossip_host,
1131 gossip_port: validator.gossip_port,
1132 url: validator.url,
1133 ledger: Some(validator.ledger),
1134 limit_ledger_size: validator.limit_ledger_size,
1135 rpc_port: Some(validator.rpc_port),
1136 slots_per_epoch: validator.slots_per_epoch,
1137 ticks_per_slot: validator.ticks_per_slot,
1138 warp_slot: validator.warp_slot,
1139 deactivate_feature: validator.deactivate_feature,
1140 }
1141 }
1142}
1143
1144pub fn get_default_ledger_path() -> PathBuf {
1145 Path::new(".anchor").join("test-ledger")
1146}
1147
1148const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0";
1149
1150impl Merge for _Validator {
1151 fn merge(&mut self, other: Self) {
1152 *self = Self {
1156 account: match self.account.take() {
1157 None => other.account,
1158 Some(mut entries) => match other.account {
1159 None => Some(entries),
1160 Some(other_entries) => {
1161 for other_entry in other_entries {
1162 match entries
1163 .iter()
1164 .position(|my_entry| *my_entry.address == other_entry.address)
1165 {
1166 None => entries.push(other_entry),
1167 Some(i) => entries[i] = other_entry,
1168 };
1169 }
1170 Some(entries)
1171 }
1172 },
1173 },
1174 account_dir: match self.account_dir.take() {
1175 None => other.account_dir,
1176 Some(mut entries) => match other.account_dir {
1177 None => Some(entries),
1178 Some(other_entries) => {
1179 for other_entry in other_entries {
1180 match entries
1181 .iter()
1182 .position(|my_entry| *my_entry.directory == other_entry.directory)
1183 {
1184 None => entries.push(other_entry),
1185 Some(i) => entries[i] = other_entry,
1186 };
1187 }
1188 Some(entries)
1189 }
1190 },
1191 },
1192 bind_address: other.bind_address.or_else(|| self.bind_address.take()),
1193 clone: match self.clone.take() {
1194 None => other.clone,
1195 Some(mut entries) => match other.clone {
1196 None => Some(entries),
1197 Some(other_entries) => {
1198 for other_entry in other_entries {
1199 match entries
1200 .iter()
1201 .position(|my_entry| *my_entry.address == other_entry.address)
1202 {
1203 None => entries.push(other_entry),
1204 Some(i) => entries[i] = other_entry,
1205 };
1206 }
1207 Some(entries)
1208 }
1209 },
1210 },
1211 dynamic_port_range: other
1212 .dynamic_port_range
1213 .or_else(|| self.dynamic_port_range.take()),
1214 faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
1215 faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
1216 geyser_plugin_config: other
1217 .geyser_plugin_config
1218 .or_else(|| self.geyser_plugin_config.take()),
1219 gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
1220 gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
1221 url: other.url.or_else(|| self.url.take()),
1222 ledger: other.ledger.or_else(|| self.ledger.take()),
1223 limit_ledger_size: other
1224 .limit_ledger_size
1225 .or_else(|| self.limit_ledger_size.take()),
1226 rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
1227 slots_per_epoch: other
1228 .slots_per_epoch
1229 .or_else(|| self.slots_per_epoch.take()),
1230 ticks_per_slot: other.ticks_per_slot.or_else(|| self.ticks_per_slot.take()),
1231 warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
1232 deactivate_feature: other
1233 .deactivate_feature
1234 .or_else(|| self.deactivate_feature.take()),
1235 };
1236 }
1237}
1238
1239#[derive(Debug, Clone)]
1240pub struct Program {
1241 pub lib_name: String,
1242 pub path: PathBuf,
1244 pub idl: Option<Idl>,
1245}
1246
1247impl Program {
1248 pub fn pubkey(&self) -> Result<Pubkey> {
1249 self.keypair().map(|kp| kp.pubkey())
1250 }
1251
1252 pub fn keypair(&self) -> Result<Keypair> {
1253 let file = self.keypair_file()?;
1254 get_keypair(file.path().to_str().unwrap())
1255 }
1256
1257 pub fn keypair_file(&self) -> Result<WithPath<File>> {
1259 let deploy_dir_path = Path::new("target").join("deploy");
1260 fs::create_dir_all(&deploy_dir_path)
1261 .with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
1262 let path = std::env::current_dir()
1263 .expect("Must have current dir")
1264 .join(deploy_dir_path.join(format!("{}-keypair.json", self.lib_name)));
1265 if path.exists() {
1266 return Ok(WithPath::new(
1267 File::open(&path)
1268 .with_context(|| format!("Error opening file with path: {}", path.display()))?,
1269 path,
1270 ));
1271 }
1272 let program_kp = Keypair::new();
1273 let mut file = File::create(&path)
1274 .with_context(|| format!("Error creating file with path: {}", path.display()))?;
1275 file.write_all(format!("{:?}", &program_kp.to_bytes()).as_bytes())?;
1276 Ok(WithPath::new(file, path))
1277 }
1278
1279 pub fn binary_path(&self, verifiable: bool) -> PathBuf {
1280 let path = Path::new("target")
1281 .join(if verifiable { "verifiable" } else { "deploy" })
1282 .join(&self.lib_name)
1283 .with_extension("so");
1284
1285 std::env::current_dir()
1286 .expect("Must have current dir")
1287 .join(path)
1288 }
1289}
1290
1291#[derive(Debug, Default)]
1292pub struct ProgramDeployment {
1293 pub address: Pubkey,
1294 pub path: Option<String>,
1295 pub idl: Option<String>,
1296}
1297
1298impl TryFrom<_ProgramDeployment> for ProgramDeployment {
1299 type Error = anyhow::Error;
1300 fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
1301 Ok(ProgramDeployment {
1302 address: pd.address.parse()?,
1303 path: pd.path,
1304 idl: pd.idl,
1305 })
1306 }
1307}
1308
1309#[derive(Debug, Default, Serialize, Deserialize)]
1310pub struct _ProgramDeployment {
1311 pub address: String,
1312 pub path: Option<String>,
1313 pub idl: Option<String>,
1314}
1315
1316impl From<&ProgramDeployment> for _ProgramDeployment {
1317 fn from(pd: &ProgramDeployment) -> Self {
1318 Self {
1319 address: pd.address.to_string(),
1320 path: pd.path.clone(),
1321 idl: pd.idl.clone(),
1322 }
1323 }
1324}
1325
1326pub struct ProgramWorkspace {
1327 pub name: String,
1328 pub program_id: Pubkey,
1329 pub idl: Idl,
1330}
1331
1332#[derive(Debug, Serialize, Deserialize)]
1333pub struct AnchorPackage {
1334 pub name: String,
1335 pub address: String,
1336 pub idl: Option<String>,
1337}
1338
1339impl AnchorPackage {
1340 pub fn from(name: String, cfg: &WithPath<Config>) -> Result<Self> {
1341 let cluster = &cfg.provider.cluster;
1342 if cluster != &Cluster::Mainnet {
1343 return Err(anyhow!("Publishing requires the mainnet cluster"));
1344 }
1345 let program_details = cfg
1346 .programs
1347 .get(cluster)
1348 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
1349 .get(&name)
1350 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
1351 let idl = program_details.idl.clone();
1352 let address = program_details.address.to_string();
1353 Ok(Self { name, address, idl })
1354 }
1355}
1356
1357#[macro_export]
1358macro_rules! home_path {
1359 ($my_struct:ident, $path:literal) => {
1360 #[derive(Clone, Debug)]
1361 pub struct $my_struct(String);
1362
1363 impl Default for $my_struct {
1364 fn default() -> Self {
1365 $my_struct(
1366 home_dir()
1367 .unwrap()
1368 .join($path.replace('/', std::path::MAIN_SEPARATOR_STR))
1369 .display()
1370 .to_string(),
1371 )
1372 }
1373 }
1374
1375 impl $my_struct {
1376 fn stringify_with_tilde(&self) -> String {
1377 self.0
1378 .replacen(home_dir().unwrap().to_str().unwrap(), "~", 1)
1379 }
1380 }
1381
1382 impl FromStr for $my_struct {
1383 type Err = anyhow::Error;
1384
1385 fn from_str(s: &str) -> Result<Self, Self::Err> {
1386 Ok(Self(s.to_owned()))
1387 }
1388 }
1389
1390 impl fmt::Display for $my_struct {
1391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1392 write!(f, "{}", self.0)
1393 }
1394 }
1395 };
1396}
1397
1398home_path!(WalletPath, ".config/solana/id.json");
1399
1400#[cfg(test)]
1401mod tests {
1402 use super::*;
1403
1404 const BASE_CONFIG: &str = "
1405 [provider]
1406 cluster = \"localnet\"
1407 wallet = \"id.json\"
1408 ";
1409
1410 #[test]
1411 fn parse_custom_cluster_str() {
1412 let config = Config::from_str(
1413 "
1414 [provider]
1415 cluster = \"http://my-url.com\"
1416 wallet = \"id.json\"
1417 ",
1418 )
1419 .unwrap();
1420 assert!(!config.features.skip_lint);
1421
1422 assert!(config
1424 .to_string()
1425 .contains(r#"cluster = "http://my-url.com""#));
1426 }
1427
1428 #[test]
1429 fn parse_custom_cluster_map() {
1430 let config = Config::from_str(
1431 "
1432 [provider]
1433 cluster = { http = \"http://my-url.com\", ws = \"ws://my-url.com\" }
1434 wallet = \"id.json\"
1435 ",
1436 )
1437 .unwrap();
1438 assert!(!config.features.skip_lint);
1439 }
1440
1441 #[test]
1442 fn parse_skip_lint_no_section() {
1443 let config = Config::from_str(BASE_CONFIG).unwrap();
1444 assert!(!config.features.skip_lint);
1445 }
1446
1447 #[test]
1448 fn parse_skip_lint_no_value() {
1449 let string = BASE_CONFIG.to_owned() + "[features]";
1450 let config = Config::from_str(&string).unwrap();
1451 assert!(!config.features.skip_lint);
1452 }
1453
1454 #[test]
1455 fn parse_skip_lint_true() {
1456 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true";
1457 let config = Config::from_str(&string).unwrap();
1458 assert!(config.features.skip_lint);
1459 }
1460
1461 #[test]
1462 fn parse_skip_lint_false() {
1463 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false";
1464 let config = Config::from_str(&string).unwrap();
1465 assert!(!config.features.skip_lint);
1466 }
1467}