anchor_cli/
config.rs

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