Skip to main content

anchor_cli/
config.rs

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