anchor_coverage/
anchor_cli_config.rs

1// smoelius: This file is a near carbon-copy of the following file:
2//
3//     https://github.com/solana-foundation/anchor/blob/v0.32.1/cli/src/config.rs
4
5#![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    /// Cluster override.
46    #[clap(global = true, long = "provider.cluster")]
47    pub cluster: Option<Cluster>,
48    /// Wallet override.
49    #[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    // Climbs each parent directory from the current dir until we find a Cargo.toml
111    pub fn discover() -> Result<Option<WithPath<Manifest>>> {
112        Manifest::discover_from_path(std::env::current_dir()?)
113    }
114
115    // Climbs each parent directory from a given starting directory until we find a Cargo.toml.
116    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            // Not found. Go up a directory level, but don't go up from Anchor.toml
141            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        // Canonicalize the workspace filepaths to compare with relative paths.
163        let (members, exclude) = self.canonicalize_workspace()?;
164
165        // Get all candidate programs.
166        //
167        // If [workspace.members] exists, then use that.
168        // Otherwise, default to `programs/*`.
169        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        // Filter out everything part of the exclude array.
188        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    /// Read and get all the programs from the workspace.
219    ///
220    /// This method will only return the given program if `name` exists.
221    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    /// Get the specified program from the workspace.
238    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    // Separate entry next to test_config because
307    // "anchor localnet" only has access to the Anchor.toml,
308    // not the Test.toml files
309    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/// Package manager to use for the project.
321#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize)]
322#[serde(rename_all = "lowercase")]
323pub enum PackageManager {
324    /// Use npm as the package manager.
325    NPM,
326    /// Use yarn as the package manager.
327    #[default]
328    Yarn,
329    /// Use pnpm as the package manager.
330    PNPM,
331    /// Use bun as the package manager.
332    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    /// Enable account resolution.
351    ///
352    /// Not able to specify default bool value: https://github.com/serde-rs/serde/issues/368
353    #[serde(default = "FeaturesConfig::get_default_resolution")]
354    pub resolution: bool,
355    /// Disable safety comment checks
356    #[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    // Climbs each parent directory until we find an Anchor.toml.
470    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                        // Make sure the program id is correct (only on the initial build)
486                        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                // If `ws` was derived from `http`, serialize `http` as string
541                (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            // Gets keys
579            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                // Checks keys
584                if http_key != "http" || ws_key != "ws" {
585                    return Err(de::Error::custom("Invalid key"));
586                }
587
588                // Checks urls
589                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// This file needs to have the same (sub)structure as Anchor.toml
814// so it can be parsed as a base test file from an Anchor.toml
815#[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
861/// canonicalizes the `file_path` arg.
862/// uses the `path` arg as the current dir
863/// from which to turn the relative path
864/// into a canonical one
865fn 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        // Instantiating a new Self object here ensures that
952        // this function will fail to compile if new fields get added
953        // to Self. This is useful as a reminder if they also require merging
954        *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    // Base58 pubkey string.
979    pub address: String,
980    // Filepath to the compiled program to embed into the genesis.
981    pub program: String,
982    // Whether the genesis program is upgradeable.
983    pub upgradeable: Option<bool>,
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize)]
987pub struct CloneEntry {
988    // Base58 pubkey string.
989    pub address: String,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize)]
993pub struct AccountEntry {
994    // Base58 pubkey string.
995    pub address: String,
996    // Name of JSON file containing the account data.
997    pub filename: String,
998}
999
1000#[derive(Debug, Clone, Serialize, Deserialize)]
1001pub struct AccountDirEntry {
1002    // Directory containing account JSON files
1003    pub directory: String,
1004}
1005
1006#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1007pub struct _Validator {
1008    // Load an account from the provided JSON file
1009    #[serde(skip_serializing_if = "Option::is_none")]
1010    pub account: Option<Vec<AccountEntry>>,
1011    // Load all the accounts from the JSON files found in the specified DIRECTORY
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub account_dir: Option<Vec<AccountDirEntry>>,
1014    // IP address to bind the validator ports. [default: 0.0.0.0]
1015    #[serde(skip_serializing_if = "Option::is_none")]
1016    pub bind_address: Option<String>,
1017    // Copy an account from the cluster referenced by the url argument.
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    pub clone: Option<Vec<CloneEntry>>,
1020    // Range to use for dynamically assigned ports. [default: 1024-65535]
1021    #[serde(skip_serializing_if = "Option::is_none")]
1022    pub dynamic_port_range: Option<String>,
1023    // Enable the faucet on this port [default: 9900].
1024    #[serde(skip_serializing_if = "Option::is_none")]
1025    pub faucet_port: Option<u16>,
1026    // Give the faucet address this much SOL in genesis. [default: 1000000]
1027    #[serde(skip_serializing_if = "Option::is_none")]
1028    pub faucet_sol: Option<String>,
1029    // Geyser plugin config location
1030    #[serde(skip_serializing_if = "Option::is_none")]
1031    pub geyser_plugin_config: Option<String>,
1032    // Gossip DNS name or IP address for the validator to advertise in gossip. [default: 127.0.0.1]
1033    #[serde(skip_serializing_if = "Option::is_none")]
1034    pub gossip_host: Option<String>,
1035    // Gossip port number for the validator
1036    #[serde(skip_serializing_if = "Option::is_none")]
1037    pub gossip_port: Option<u16>,
1038    // URL for Solana's JSON RPC or moniker.
1039    #[serde(skip_serializing_if = "Option::is_none")]
1040    pub url: Option<String>,
1041    // Use DIR as ledger location
1042    #[serde(skip_serializing_if = "Option::is_none")]
1043    pub ledger: Option<String>,
1044    // Keep this amount of shreds in root slots. [default: 10000]
1045    #[serde(skip_serializing_if = "Option::is_none")]
1046    pub limit_ledger_size: Option<String>,
1047    // Enable JSON RPC on this port, and the next port for the RPC websocket. [default: 8899]
1048    #[serde(skip_serializing_if = "Option::is_none")]
1049    pub rpc_port: Option<u16>,
1050    // Override the number of slots in an epoch.
1051    #[serde(skip_serializing_if = "Option::is_none")]
1052    pub slots_per_epoch: Option<String>,
1053    // The number of ticks in a slot
1054    #[serde(skip_serializing_if = "Option::is_none")]
1055    pub ticks_per_slot: Option<u16>,
1056    // Warp the ledger to WARP_SLOT after starting the validator.
1057    #[serde(skip_serializing_if = "Option::is_none")]
1058    pub warp_slot: Option<Slot>,
1059    // Deactivate one or more features.
1060    #[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        // Instantiating a new Self object here ensures that
1164        // this function will fail to compile if new fields get added
1165        // to Self. This is useful as a reminder if they also require merging
1166        *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    // Canonicalized path to the program directory
1254    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    // Lazily initializes the keypair file with a new key if it doesn't exist.
1269    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        // Make sure the layout of `provider.cluster` stays the same after serialization
1434        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}