anchor_cli/
config.rs

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