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