Skip to main content

anchor_cli/
config.rs

1use {
2    crate::{get_keypair, is_hidden, keys_sync, target_dir, AbsolutePath, 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, AbsolutePath)]
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, AbsolutePath)]
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 = target_dir()?
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 WalletPath {
312    fn resolve_relative_to(self, base: &Path) -> Self {
313        if self.0.is_relative() {
314            Self(base.join(self.0))
315        } else {
316            self
317        }
318    }
319}
320
321impl<T> std::ops::Deref for WithPath<T> {
322    type Target = T;
323    fn deref(&self) -> &Self::Target {
324        &self.inner
325    }
326}
327
328impl<T> std::ops::DerefMut for WithPath<T> {
329    fn deref_mut(&mut self) -> &mut Self::Target {
330        &mut self.inner
331    }
332}
333
334#[derive(Debug, Default)]
335pub struct Config {
336    pub toolchain: ToolchainConfig,
337    pub features: FeaturesConfig,
338    pub provider: ProviderConfig,
339    pub programs: ProgramsConfig,
340    pub scripts: ScriptsConfig,
341    pub hooks: HooksConfig,
342    pub workspace: WorkspaceConfig,
343    pub clients: ClientsConfig,
344    // Separate entry next to test_config because
345    // "anchor localnet" only has access to the Anchor.toml,
346    // not the Test.toml files
347    pub validator: Option<ValidatorType>,
348    pub test_validator: Option<TestValidator>,
349    pub test_config: Option<TestConfig>,
350    pub surfpool_config: Option<SurfpoolConfig>,
351    /// If `Some(true)`, `anchor test` won't auto-start a validator for this
352    /// workspace. Emitted by `anchor init` for in-process test templates
353    /// (litesvm / rust / mollusk) where the test harness never opens an RPC.
354    pub skip_local_validator: Option<bool>,
355}
356
357#[derive(ValueEnum, Parser, Clone, Copy, PartialEq, Eq, Debug, AbsolutePath)]
358pub enum ValidatorType {
359    /// Use Surfpool validator (default)
360    Surfpool,
361    /// Use Solana test validator
362    Legacy,
363}
364#[derive(Default, Clone, Debug, Serialize, Deserialize)]
365pub struct ToolchainConfig {
366    pub anchor_version: Option<String>,
367    pub solana_version: Option<String>,
368    pub package_manager: Option<PackageManager>,
369}
370
371/// Package manager to use for the project.
372///
373/// No `Default` impl — the enum represents an explicit user choice. Call sites
374/// that need to resolve a concrete package manager (when nothing is configured)
375/// should go through `crate::resolve_package_manager` so the waterfall
376/// (pnpm → yarn → npm) and missing-binary diagnostics are centralized.
377#[derive(Clone, Debug, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize, AbsolutePath)]
378#[serde(rename_all = "lowercase")]
379pub enum PackageManager {
380    /// Use npm as the package manager.
381    NPM,
382    /// Use yarn as the package manager.
383    Yarn,
384    /// Use pnpm as the package manager.
385    PNPM,
386    /// Use bun as the package manager.
387    Bun,
388}
389
390impl std::fmt::Display for PackageManager {
391    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
392        let pkg_manager_str = match self {
393            PackageManager::NPM => "npm",
394            PackageManager::Yarn => "yarn",
395            PackageManager::PNPM => "pnpm",
396            PackageManager::Bun => "bun",
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(Debug, Default)]
431pub struct ProviderConfig {
432    pub cluster: Cluster,
433    pub wallet: WalletPath,
434}
435
436pub type ScriptsConfig = BTreeMap<String, String>;
437
438pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
439
440#[derive(Default, Clone, Debug, Serialize, Deserialize)]
441#[serde(deny_unknown_fields)]
442pub struct HooksConfig {
443    #[serde(alias = "pre-build")]
444    pre_build: Option<Hook>,
445    #[serde(alias = "post-build")]
446    post_build: Option<Hook>,
447    #[serde(alias = "pre-test")]
448    pre_test: Option<Hook>,
449    #[serde(alias = "post-test")]
450    post_test: Option<Hook>,
451    #[serde(alias = "pre-deploy")]
452    pre_deploy: Option<Hook>,
453    #[serde(alias = "post-deploy")]
454    post_deploy: Option<Hook>,
455}
456
457#[derive(Clone, Debug, Serialize, Deserialize)]
458#[serde(untagged)]
459enum Hook {
460    Single(String),
461    List(Vec<String>),
462}
463
464impl Hook {
465    pub fn hooks(&self) -> &[String] {
466        match self {
467            Self::Single(h) => std::slice::from_ref(h),
468            Self::List(l) => l.as_slice(),
469        }
470    }
471}
472
473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
474pub enum HookType {
475    PreBuild,
476    PostBuild,
477    PreTest,
478    PostTest,
479    PreDeploy,
480    PostDeploy,
481}
482
483/// `[clients]` section of `Anchor.toml`.
484///
485/// Declares which Codama-generated client SDKs the workspace ships, where
486/// they live on disk, and whether they should be regenerated automatically
487/// (e.g. as part of `anchor build` once that integration lands).
488///
489/// TOML shape:
490///
491/// ```toml
492/// [clients]
493/// auto = true
494/// rust = true                                    # enables `clients/rust`
495/// js   = { enable = true }                       # equivalent to `js = true`
496/// go   = { enable = true, path = "go-client" }   # custom output dir
497/// js-umi = false                                 # explicitly disabled
498/// ```
499#[derive(Debug, Default, Clone, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct ClientsConfig {
502    /// Regenerate clients automatically on `anchor build` / IDL change.
503    #[serde(default, skip_serializing_if = "is_false")]
504    pub auto: bool,
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    pub js: Option<ClientLanguageConfig>,
507    #[serde(
508        default,
509        rename = "js-umi",
510        alias = "js_umi",
511        skip_serializing_if = "Option::is_none"
512    )]
513    pub js_umi: Option<ClientLanguageConfig>,
514    #[serde(default, skip_serializing_if = "Option::is_none")]
515    pub rust: Option<ClientLanguageConfig>,
516    #[serde(default, skip_serializing_if = "Option::is_none")]
517    pub go: Option<ClientLanguageConfig>,
518}
519
520/// Per-language client entry. Accepts either a bare `bool` (`rust = true`)
521/// or a table with explicit `enable` and optional `path` keys.
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(untagged)]
524pub enum ClientLanguageConfig {
525    /// `lang = true` / `lang = false`.
526    Enabled(bool),
527    /// `lang = { enable = bool, path = "..." }`.
528    Detailed {
529        #[serde(default = "ClientLanguageConfig::default_enable")]
530        enable: bool,
531        #[serde(default, skip_serializing_if = "Option::is_none")]
532        path: Option<String>,
533    },
534}
535
536impl ClientLanguageConfig {
537    fn default_enable() -> bool {
538        true
539    }
540
541    pub fn is_enabled(&self) -> bool {
542        match self {
543            Self::Enabled(b) => *b,
544            Self::Detailed { enable, .. } => *enable,
545        }
546    }
547
548    pub fn path(&self) -> Option<&str> {
549        match self {
550            Self::Enabled(_) => None,
551            Self::Detailed { path, .. } => path.as_deref(),
552        }
553    }
554}
555
556/// Stable identifiers used both as TOML keys and as Codama script names so
557/// `Anchor.toml` and `anchor codama generate -l <lang>` agree on spelling.
558pub const CLIENT_LANGUAGES: &[&str] = &["js", "js-umi", "rust", "go"];
559
560impl ClientsConfig {
561    /// Look up a language entry by its [`CLIENT_LANGUAGES`] id.
562    pub fn get(&self, language: &str) -> Option<&ClientLanguageConfig> {
563        match language {
564            "js" => self.js.as_ref(),
565            "js-umi" => self.js_umi.as_ref(),
566            "rust" => self.rust.as_ref(),
567            "go" => self.go.as_ref(),
568            _ => None,
569        }
570    }
571
572    /// Languages the user has explicitly enabled, paired with the resolved
573    /// output directory (`<base>/<lang>` if no `path` was set on the entry).
574    pub fn enabled(&self, base: &Path) -> Vec<(&'static str, PathBuf)> {
575        CLIENT_LANGUAGES
576            .iter()
577            .filter_map(|&lang| {
578                let entry = self.get(lang)?;
579                if !entry.is_enabled() {
580                    return None;
581                }
582                let path = entry
583                    .path()
584                    .map(PathBuf::from)
585                    .unwrap_or_else(|| base.join(lang));
586                Some((lang, path))
587            })
588            .collect()
589    }
590}
591
592fn is_false(b: &bool) -> bool {
593    !*b
594}
595
596#[derive(Debug, Default, Clone, Serialize, Deserialize)]
597pub struct WorkspaceConfig {
598    #[serde(default, skip_serializing_if = "Vec::is_empty")]
599    pub members: Vec<String>,
600    #[serde(default, skip_serializing_if = "Vec::is_empty")]
601    pub exclude: Vec<String>,
602    #[serde(default, skip_serializing_if = "String::is_empty")]
603    pub types: String,
604}
605
606#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug, AbsolutePath)]
607pub enum BootstrapMode {
608    None,
609    Debian,
610}
611
612#[derive(Debug, Clone)]
613pub struct BuildConfig {
614    pub verifiable: bool,
615    pub solana_version: Option<String>,
616    pub docker_image: String,
617    pub bootstrap: BootstrapMode,
618}
619
620impl Config {
621    pub fn add_test_config(
622        &mut self,
623        root: impl AsRef<Path>,
624        test_paths: Vec<PathBuf>,
625    ) -> Result<()> {
626        self.test_config = TestConfig::discover(root, test_paths)?;
627        Ok(())
628    }
629
630    pub fn docker(&self) -> String {
631        let version = self
632            .toolchain
633            .anchor_version
634            .as_deref()
635            .unwrap_or(crate::DOCKER_BUILDER_VERSION);
636        format!("solanafoundation/anchor:v{version}")
637    }
638
639    pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
640        Config::_discover().map(|opt| {
641            opt.map(|mut cfg| {
642                if let Some(cluster) = cfg_override.cluster.clone() {
643                    cfg.provider.cluster = cluster;
644                }
645                if let Some(wallet) = cfg_override.wallet.clone() {
646                    cfg.provider.wallet = wallet;
647                }
648                cfg
649            })
650        })
651    }
652
653    // Climbs each parent directory until we find an Anchor.toml.
654    fn _discover() -> Result<Option<WithPath<Config>>> {
655        let _cwd = std::env::current_dir()?;
656        let mut cwd_opt = Some(_cwd.as_path());
657
658        while let Some(cwd) = cwd_opt {
659            for f in fs::read_dir(cwd).with_context(|| {
660                format!("Error reading the directory with path: {}", cwd.display())
661            })? {
662                let p = f
663                    .with_context(|| {
664                        format!("Error reading the directory with path: {}", cwd.display())
665                    })?
666                    .path();
667                if let Some(filename) = p.file_name() {
668                    if filename.to_str() == Some("Anchor.toml") {
669                        let config_dir = p.parent().unwrap();
670                        // Make sure the program id is correct (only on the initial build)
671                        let mut cfg = Config::from_path(&p)?;
672                        let deploy_dir = target_dir()?.join("deploy");
673                        if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
674                            println!("Updating program ids...");
675                            fs::create_dir_all(deploy_dir)?;
676                            keys_sync(&ConfigOverride::default(), None)?;
677                            cfg = Config::from_path(&p)?;
678                        }
679                        cfg.provider.wallet = cfg.provider.wallet.resolve_relative_to(config_dir);
680
681                        return Ok(Some(WithPath::new(cfg, p)));
682                    }
683                }
684            }
685
686            cwd_opt = cwd.parent();
687        }
688
689        Ok(None)
690    }
691
692    fn from_path(p: impl AsRef<Path>) -> Result<Self> {
693        fs::read_to_string(&p)
694            .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))?
695            .parse::<Self>()
696    }
697
698    pub fn wallet_kp(&self) -> Result<Keypair> {
699        get_keypair(Path::new(&self.provider.wallet.0))
700    }
701
702    pub fn run_hooks(&self, hook_type: HookType) -> Result<()> {
703        let hooks = match hook_type {
704            HookType::PreBuild => &self.hooks.pre_build,
705            HookType::PostBuild => &self.hooks.post_build,
706            HookType::PreTest => &self.hooks.pre_test,
707            HookType::PostTest => &self.hooks.post_test,
708            HookType::PreDeploy => &self.hooks.pre_deploy,
709            HookType::PostDeploy => &self.hooks.post_deploy,
710        };
711        let cmds = hooks.as_ref().map(Hook::hooks).unwrap_or_default();
712        for cmd in cmds {
713            let status = Command::new("bash")
714                .arg("-c")
715                .arg(cmd)
716                .status()
717                .with_context(|| format!("failed to execute `{cmd}`"))?;
718            if !status.success() {
719                match status.code() {
720                    Some(code) => bail!("`{cmd}` failed with exit code {code}"),
721                    None => bail!("`{cmd}` killed by signal"),
722                }
723            }
724        }
725        Ok(())
726    }
727}
728
729#[derive(Debug, Serialize, Deserialize)]
730struct _Config {
731    toolchain: Option<ToolchainConfig>,
732    features: Option<FeaturesConfig>,
733    programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
734    provider: Provider,
735    workspace: Option<WorkspaceConfig>,
736    scripts: Option<ScriptsConfig>,
737    hooks: Option<HooksConfig>,
738    test: Option<_TestValidator>,
739    surfpool: Option<_SurfpoolConfig>,
740    #[serde(skip_serializing_if = "Option::is_none")]
741    skip_local_validator: Option<bool>,
742    #[serde(skip_serializing_if = "Option::is_none")]
743    clients: Option<ClientsConfig>,
744}
745
746#[derive(Debug, Serialize, Deserialize)]
747struct Provider {
748    #[serde(serialize_with = "ser_cluster", deserialize_with = "des_cluster")]
749    cluster: Cluster,
750    wallet: String,
751}
752
753fn ser_cluster<S: Serializer>(cluster: &Cluster, s: S) -> Result<S::Ok, S::Error> {
754    match cluster {
755        Cluster::Custom(http, ws) => {
756            match (Url::parse(http), Url::parse(ws)) {
757                // If `ws` was derived from `http`, serialize `http` as string
758                (Ok(h), Ok(w)) if h.domain() == w.domain() => s.serialize_str(http),
759                _ => {
760                    let mut map = s.serialize_map(Some(2))?;
761                    map.serialize_entry("http", http)?;
762                    map.serialize_entry("ws", ws)?;
763                    map.end()
764                }
765            }
766        }
767        _ => s.serialize_str(&cluster.to_string()),
768    }
769}
770
771fn des_cluster<'de, D>(deserializer: D) -> Result<Cluster, D::Error>
772where
773    D: Deserializer<'de>,
774{
775    struct StringOrCustomCluster(PhantomData<fn() -> Cluster>);
776
777    impl<'de> Visitor<'de> for StringOrCustomCluster {
778        type Value = Cluster;
779
780        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
781            formatter.write_str("string or map")
782        }
783
784        fn visit_str<E>(self, value: &str) -> Result<Cluster, E>
785        where
786            E: de::Error,
787        {
788            value.parse().map_err(de::Error::custom)
789        }
790
791        fn visit_map<M>(self, mut map: M) -> Result<Cluster, M::Error>
792        where
793            M: MapAccess<'de>,
794        {
795            // Gets keys
796            if let (Some((http_key, http_value)), Some((ws_key, ws_value))) = (
797                map.next_entry::<String, String>()?,
798                map.next_entry::<String, String>()?,
799            ) {
800                // Checks keys
801                if http_key != "http" || ws_key != "ws" {
802                    return Err(de::Error::custom("Invalid key"));
803                }
804
805                // Checks urls
806                Url::parse(&http_value).map_err(de::Error::custom)?;
807                Url::parse(&ws_value).map_err(de::Error::custom)?;
808
809                Ok(Cluster::Custom(http_value, ws_value))
810            } else {
811                Err(de::Error::custom("Invalid entry"))
812            }
813        }
814    }
815    deserializer.deserialize_any(StringOrCustomCluster(PhantomData))
816}
817
818impl fmt::Display for Config {
819    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820        let programs = {
821            let c = ser_programs(&self.programs);
822            if c.is_empty() {
823                None
824            } else {
825                Some(c)
826            }
827        };
828        let cfg = _Config {
829            toolchain: Some(self.toolchain.clone()),
830            features: Some(self.features.clone()),
831            provider: Provider {
832                cluster: self.provider.cluster.clone(),
833                wallet: self.provider.wallet.stringify_with_tilde(),
834            },
835            test: self.test_validator.clone().map(Into::into),
836            scripts: match self.scripts.is_empty() {
837                true => None,
838                false => Some(self.scripts.clone()),
839            },
840            hooks: Some(self.hooks.clone()),
841            programs,
842            workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
843                .then(|| self.workspace.clone()),
844            surfpool: self.surfpool_config.clone().map(Into::into),
845            skip_local_validator: self.skip_local_validator,
846            clients: {
847                let c = &self.clients;
848                let empty = !c.auto
849                    && c.js.is_none()
850                    && c.js_umi.is_none()
851                    && c.rust.is_none()
852                    && c.go.is_none();
853                if empty {
854                    None
855                } else {
856                    Some(c.clone())
857                }
858            },
859        };
860
861        let cfg = toml::to_string(&cfg).expect("Must be well formed");
862        write!(f, "{cfg}")
863    }
864}
865
866impl FromStr for Config {
867    type Err = Error;
868
869    fn from_str(s: &str) -> Result<Self, Self::Err> {
870        let cfg: _Config =
871            toml::from_str(s).map_err(|e| anyhow!("Unable to deserialize config: {e}"))?;
872        Ok(Config {
873            toolchain: cfg.toolchain.unwrap_or_default(),
874            features: cfg.features.unwrap_or_default(),
875            provider: ProviderConfig {
876                cluster: cfg.provider.cluster,
877                wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
878            },
879            scripts: cfg.scripts.unwrap_or_default(),
880            hooks: cfg.hooks.unwrap_or_default(),
881            validator: None, // Will be set based on CLI flags
882            test_validator: cfg.test.map(Into::into),
883            test_config: None,
884            programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
885            workspace: cfg.workspace.unwrap_or_default(),
886            surfpool_config: cfg.surfpool.map(Into::into),
887            skip_local_validator: cfg.skip_local_validator,
888            clients: cfg.clients.unwrap_or_default(),
889        })
890    }
891}
892
893pub fn get_solana_cfg_url() -> Result<String, io::Error> {
894    let config_file = CONFIG_FILE.as_ref().ok_or_else(|| {
895        io::Error::new(
896            io::ErrorKind::NotFound,
897            "Default Solana config was not found",
898        )
899    })?;
900    SolanaConfig::load(config_file).map(|config| config.json_rpc_url)
901}
902
903fn ser_programs(
904    programs: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
905) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
906    programs
907        .iter()
908        .map(|(cluster, programs)| {
909            let cluster = cluster.to_string();
910            let programs = programs
911                .iter()
912                .map(|(name, deployment)| {
913                    (
914                        name.clone(),
915                        to_value(&_ProgramDeployment::from(deployment)),
916                    )
917                })
918                .collect::<BTreeMap<String, serde_json::Value>>();
919            (cluster, programs)
920        })
921        .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
922}
923
924fn to_value(dep: &_ProgramDeployment) -> serde_json::Value {
925    if dep.path.is_none() && dep.idl.is_none() {
926        return serde_json::Value::String(dep.address.to_string());
927    }
928    serde_json::to_value(dep).unwrap()
929}
930
931fn deser_programs(
932    programs: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
933) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
934    programs
935        .iter()
936        .map(|(cluster, programs)| {
937            let cluster: Cluster = cluster.parse()?;
938            let programs = programs
939                .iter()
940                .map(|(name, program_id)| {
941                    Ok((
942                        name.clone(),
943                        ProgramDeployment::try_from(match &program_id {
944                            serde_json::Value::String(address) => _ProgramDeployment {
945                                address: address.parse()?,
946                                path: None,
947                                idl: None,
948                            },
949
950                            serde_json::Value::Object(_) => {
951                                serde_json::from_value(program_id.clone())
952                                    .map_err(|_| anyhow!("Unable to read toml"))?
953                            }
954                            _ => return Err(anyhow!("Invalid toml type")),
955                        })?,
956                    ))
957                })
958                .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
959            Ok((cluster, programs))
960        })
961        .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
962}
963
964#[derive(Default, Debug, Clone, Serialize, Deserialize)]
965pub struct TestValidator {
966    pub genesis: Option<Vec<GenesisEntry>>,
967    pub validator: Option<Validator>,
968    pub startup_wait: i32,
969    pub shutdown_wait: i32,
970    pub upgradeable: bool,
971}
972
973#[derive(Default, Debug, Clone, Serialize, Deserialize)]
974pub struct SurfpoolConfig {
975    pub startup_wait: i32,
976    pub shutdown_wait: i32,
977    pub rpc_port: u16,
978    pub ws_port: Option<u16>,
979    pub host: String,
980    pub online: Option<bool>,
981    pub datasource_rpc_url: Option<String>,
982    pub airdrop_addresses: Option<Vec<String>>,
983    pub manifest_file_path: Option<String>,
984    pub runbooks: Option<Vec<String>>,
985    pub slot_time: Option<u16>,
986    pub log_level: Option<String>,
987    pub block_production_mode: Option<String>,
988}
989
990#[derive(Default, Debug, Clone, Serialize, Deserialize)]
991pub struct _TestValidator {
992    #[serde(skip_serializing_if = "Option::is_none")]
993    pub genesis: Option<Vec<GenesisEntry>>,
994    #[serde(skip_serializing_if = "Option::is_none")]
995    pub validator: Option<_Validator>,
996    #[serde(skip_serializing_if = "Option::is_none")]
997    pub startup_wait: Option<i32>,
998    #[serde(skip_serializing_if = "Option::is_none")]
999    pub shutdown_wait: Option<i32>,
1000    #[serde(skip_serializing_if = "Option::is_none")]
1001    pub upgradeable: Option<bool>,
1002}
1003
1004#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1005pub struct _SurfpoolConfig {
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    pub startup_wait: Option<i32>,
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub shutdown_wait: Option<i32>,
1010    #[serde(skip_serializing_if = "Option::is_none")]
1011    pub rpc_port: Option<u16>,
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub ws_port: Option<u16>,
1014    #[serde(skip_serializing_if = "Option::is_none")]
1015    pub host: Option<String>,
1016    #[serde(skip_serializing_if = "Option::is_none")]
1017    pub online: Option<bool>,
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    pub datasource_rpc_url: Option<String>,
1020    #[serde(skip_serializing_if = "Option::is_none")]
1021    pub airdrop_addresses: Option<Vec<String>>,
1022    #[serde(skip_serializing_if = "Option::is_none")]
1023    pub manifest_file_path: Option<String>,
1024    #[serde(skip_serializing_if = "Option::is_none")]
1025    pub runbooks: Option<Vec<String>>,
1026    #[serde(skip_serializing_if = "Option::is_none")]
1027    pub slot_time: Option<u16>,
1028    #[serde(skip_serializing_if = "Option::is_none")]
1029    pub log_level: Option<String>,
1030    #[serde(skip_serializing_if = "Option::is_none")]
1031    pub block_production_mode: Option<String>,
1032}
1033
1034impl From<_SurfpoolConfig> for SurfpoolConfig {
1035    fn from(_surfpool_config: _SurfpoolConfig) -> Self {
1036        Self {
1037            startup_wait: _surfpool_config.startup_wait.unwrap_or(STARTUP_WAIT),
1038            shutdown_wait: _surfpool_config.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
1039            rpc_port: _surfpool_config.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1040            host: _surfpool_config.host.unwrap_or(SURFPOOL_HOST.to_string()),
1041            ws_port: _surfpool_config.ws_port,
1042            online: _surfpool_config.online,
1043            datasource_rpc_url: _surfpool_config.datasource_rpc_url,
1044            airdrop_addresses: _surfpool_config.airdrop_addresses,
1045            manifest_file_path: _surfpool_config.manifest_file_path,
1046            runbooks: _surfpool_config.runbooks,
1047            slot_time: _surfpool_config.slot_time,
1048            log_level: _surfpool_config.log_level,
1049            block_production_mode: _surfpool_config.block_production_mode,
1050        }
1051    }
1052}
1053
1054impl From<SurfpoolConfig> for _SurfpoolConfig {
1055    fn from(surfpool_config: SurfpoolConfig) -> Self {
1056        Self {
1057            startup_wait: Some(surfpool_config.startup_wait),
1058            shutdown_wait: Some(surfpool_config.shutdown_wait),
1059            rpc_port: Some(surfpool_config.rpc_port),
1060            ws_port: surfpool_config.ws_port,
1061            host: Some(surfpool_config.host),
1062            online: surfpool_config.online,
1063            datasource_rpc_url: surfpool_config.datasource_rpc_url,
1064            airdrop_addresses: surfpool_config.airdrop_addresses,
1065            manifest_file_path: surfpool_config.manifest_file_path,
1066            runbooks: surfpool_config.runbooks,
1067            slot_time: surfpool_config.slot_time,
1068            log_level: surfpool_config.log_level,
1069            block_production_mode: surfpool_config.block_production_mode,
1070        }
1071    }
1072}
1073pub const STARTUP_WAIT: i32 = 5000;
1074pub const SHUTDOWN_WAIT: i32 = 2000;
1075
1076impl From<_TestValidator> for TestValidator {
1077    fn from(_test_validator: _TestValidator) -> Self {
1078        Self {
1079            shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
1080            startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
1081            genesis: _test_validator.genesis,
1082            validator: _test_validator.validator.map(Into::into),
1083            upgradeable: _test_validator.upgradeable.unwrap_or(false),
1084        }
1085    }
1086}
1087
1088impl From<TestValidator> for _TestValidator {
1089    fn from(test_validator: TestValidator) -> Self {
1090        Self {
1091            shutdown_wait: Some(test_validator.shutdown_wait),
1092            startup_wait: Some(test_validator.startup_wait),
1093            genesis: test_validator.genesis,
1094            validator: test_validator.validator.map(Into::into),
1095            upgradeable: Some(test_validator.upgradeable),
1096        }
1097    }
1098}
1099
1100#[derive(Debug, Clone)]
1101pub struct TestConfig {
1102    pub test_suite_configs: HashMap<PathBuf, TestToml>,
1103}
1104
1105impl Deref for TestConfig {
1106    type Target = HashMap<PathBuf, TestToml>;
1107
1108    fn deref(&self) -> &Self::Target {
1109        &self.test_suite_configs
1110    }
1111}
1112
1113impl TestConfig {
1114    pub fn discover(root: impl AsRef<Path>, test_paths: Vec<PathBuf>) -> Result<Option<Self>> {
1115        let walker = WalkDir::new(root).into_iter();
1116        let mut test_suite_configs = HashMap::new();
1117        for entry in walker.filter_entry(|e| !is_hidden(e)) {
1118            let entry = entry?;
1119            if entry.file_name() == "Test.toml" {
1120                let entry_path = entry.path();
1121                let test_toml = TestToml::from_path(entry_path)?;
1122                if test_paths.is_empty() || test_paths.iter().any(|p| entry_path.starts_with(p)) {
1123                    test_suite_configs.insert(entry.path().into(), test_toml);
1124                }
1125            }
1126        }
1127
1128        Ok(match test_suite_configs.is_empty() {
1129            true => None,
1130            false => Some(Self { test_suite_configs }),
1131        })
1132    }
1133}
1134
1135// This file needs to have the same (sub)structure as Anchor.toml
1136// so it can be parsed as a base test file from an Anchor.toml
1137#[derive(Debug, Clone, Serialize, Deserialize)]
1138pub struct _TestToml {
1139    pub extends: Option<Vec<String>>,
1140    pub test: Option<_TestValidator>,
1141    pub scripts: Option<ScriptsConfig>,
1142}
1143
1144impl _TestToml {
1145    fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
1146        let s = fs::read_to_string(&path)?;
1147        let parsed_toml: Self = toml::from_str(&s)?;
1148        let mut current_toml = _TestToml {
1149            extends: None,
1150            test: None,
1151            scripts: None,
1152        };
1153        if let Some(bases) = &parsed_toml.extends {
1154            for base in bases {
1155                let mut canonical_base = base.clone();
1156                canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
1157                current_toml.merge(_TestToml::from_path(&canonical_base)?);
1158            }
1159        }
1160        current_toml.merge(parsed_toml);
1161
1162        if let Some(test) = &mut current_toml.test {
1163            if let Some(genesis_programs) = &mut test.genesis {
1164                for entry in genesis_programs {
1165                    entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
1166                }
1167            }
1168            if let Some(validator) = &mut test.validator {
1169                if let Some(ledger_dir) = &mut validator.ledger {
1170                    *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?;
1171                }
1172                if let Some(accounts) = &mut validator.account {
1173                    for entry in accounts {
1174                        entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
1175                    }
1176                }
1177                if let Some(account_dirs) = &mut validator.account_dir {
1178                    for entry in account_dirs {
1179                        entry.directory =
1180                            canonicalize_filepath_from_origin(&entry.directory, &path)?;
1181                    }
1182                }
1183            }
1184        }
1185        Ok(current_toml)
1186    }
1187}
1188
1189/// canonicalizes the `file_path` arg.
1190/// uses the `path` arg as the current dir
1191/// from which to turn the relative path
1192/// into a canonical one
1193fn canonicalize_filepath_from_origin(
1194    file_path: impl AsRef<Path>,
1195    origin: impl AsRef<Path>,
1196) -> Result<String> {
1197    let previous_dir = std::env::current_dir()?;
1198    std::env::set_current_dir(origin.as_ref().parent().unwrap())?;
1199    let result = fs::canonicalize(&file_path)
1200        .with_context(|| {
1201            format!(
1202                "Error reading (possibly relative) path: {}. If relative, this is the path that \
1203                 was used as the current path: {}",
1204                &file_path.as_ref().display(),
1205                &origin.as_ref().display()
1206            )
1207        })?
1208        .display()
1209        .to_string();
1210    std::env::set_current_dir(previous_dir)?;
1211    Ok(result)
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize)]
1215pub struct TestToml {
1216    #[serde(skip_serializing_if = "Option::is_none")]
1217    pub test: Option<TestValidator>,
1218    pub scripts: ScriptsConfig,
1219}
1220
1221impl TestToml {
1222    pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
1223        WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
1224    }
1225}
1226
1227impl Merge for _TestToml {
1228    fn merge(&mut self, other: Self) {
1229        let mut my_scripts = self.scripts.take();
1230        match &mut my_scripts {
1231            None => my_scripts = other.scripts,
1232            Some(my_scripts) => {
1233                if let Some(other_scripts) = other.scripts {
1234                    for (name, script) in other_scripts {
1235                        my_scripts.insert(name, script);
1236                    }
1237                }
1238            }
1239        }
1240
1241        let mut my_test = self.test.take();
1242        match &mut my_test {
1243            Some(my_test) => {
1244                if let Some(other_test) = other.test {
1245                    if let Some(startup_wait) = other_test.startup_wait {
1246                        my_test.startup_wait = Some(startup_wait);
1247                    }
1248                    if let Some(other_genesis) = other_test.genesis {
1249                        match &mut my_test.genesis {
1250                            Some(my_genesis) => {
1251                                for other_entry in other_genesis {
1252                                    match my_genesis
1253                                        .iter()
1254                                        .position(|g| *g.address == other_entry.address)
1255                                    {
1256                                        None => my_genesis.push(other_entry),
1257                                        Some(i) => my_genesis[i] = other_entry,
1258                                    }
1259                                }
1260                            }
1261                            None => my_test.genesis = Some(other_genesis),
1262                        }
1263                    }
1264                    let mut my_validator = my_test.validator.take();
1265                    match &mut my_validator {
1266                        None => my_validator = other_test.validator,
1267                        Some(my_validator) => {
1268                            if let Some(other_validator) = other_test.validator {
1269                                my_validator.merge(other_validator)
1270                            }
1271                        }
1272                    }
1273
1274                    my_test.validator = my_validator;
1275                }
1276            }
1277            None => my_test = other.test,
1278        };
1279
1280        // Instantiating a new Self object here ensures that
1281        // this function will fail to compile if new fields get added
1282        // to Self. This is useful as a reminder if they also require merging
1283        *self = Self {
1284            test: my_test,
1285            scripts: my_scripts,
1286            extends: self.extends.take(),
1287        };
1288    }
1289}
1290
1291impl TryFrom<WithPath<_TestToml>> for TestToml {
1292    type Error = Error;
1293
1294    fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
1295        Ok(Self {
1296            test: value.test.take().map(Into::into),
1297            scripts: value
1298                .scripts
1299                .take()
1300                .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
1301        })
1302    }
1303}
1304
1305#[derive(Debug, Clone, Serialize, Deserialize)]
1306pub struct GenesisEntry {
1307    // Base58 pubkey string.
1308    pub address: String,
1309    // Filepath to the compiled program to embed into the genesis.
1310    pub program: String,
1311    // Whether the genesis program is upgradeable.
1312    pub upgradeable: Option<bool>,
1313}
1314
1315#[derive(Debug, Clone, Serialize, Deserialize)]
1316pub struct CloneEntry {
1317    // Base58 pubkey string.
1318    pub address: String,
1319}
1320
1321#[derive(Debug, Clone, Serialize, Deserialize)]
1322pub struct AccountEntry {
1323    // Base58 pubkey string.
1324    pub address: String,
1325    // Name of JSON file containing the account data.
1326    pub filename: String,
1327}
1328
1329#[derive(Debug, Clone, Serialize, Deserialize)]
1330pub struct AccountDirEntry {
1331    // Directory containing account JSON files
1332    pub directory: String,
1333}
1334
1335#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1336pub struct _Validator {
1337    // Load an account from the provided JSON file
1338    #[serde(skip_serializing_if = "Option::is_none")]
1339    pub account: Option<Vec<AccountEntry>>,
1340    // Load all the accounts from the JSON files found in the specified DIRECTORY
1341    #[serde(skip_serializing_if = "Option::is_none")]
1342    pub account_dir: Option<Vec<AccountDirEntry>>,
1343    // IP address to bind the validator ports. [default: 127.0.0.1]
1344    #[serde(skip_serializing_if = "Option::is_none")]
1345    pub bind_address: Option<String>,
1346    // Copy an account from the cluster referenced by the url argument.
1347    #[serde(skip_serializing_if = "Option::is_none")]
1348    pub clone: Option<Vec<CloneEntry>>,
1349    // Range to use for dynamically assigned ports. [default: 1024-65535]
1350    #[serde(skip_serializing_if = "Option::is_none")]
1351    pub dynamic_port_range: Option<String>,
1352    // Enable the faucet on this port [default: 9900].
1353    #[serde(skip_serializing_if = "Option::is_none")]
1354    pub faucet_port: Option<u16>,
1355    // Give the faucet address this much SOL in genesis. [default: 1000000]
1356    #[serde(skip_serializing_if = "Option::is_none")]
1357    pub faucet_sol: Option<String>,
1358    // Geyser plugin config location
1359    #[serde(skip_serializing_if = "Option::is_none")]
1360    pub geyser_plugin_config: Option<String>,
1361    // Gossip DNS name or IP address for the validator to advertise in gossip. [default: 127.0.0.1]
1362    #[serde(skip_serializing_if = "Option::is_none")]
1363    pub gossip_host: Option<String>,
1364    // Gossip port number for the validator
1365    #[serde(skip_serializing_if = "Option::is_none")]
1366    pub gossip_port: Option<u16>,
1367    // URL for Solana's JSON RPC or moniker.
1368    #[serde(skip_serializing_if = "Option::is_none")]
1369    pub url: Option<String>,
1370    // Use DIR as ledger location
1371    #[serde(skip_serializing_if = "Option::is_none")]
1372    pub ledger: Option<String>,
1373    // Keep this amount of shreds in root slots. [default: 10000]
1374    #[serde(skip_serializing_if = "Option::is_none")]
1375    pub limit_ledger_size: Option<String>,
1376    // Enable JSON RPC on this port, and the next port for the RPC websocket. [default: 8899]
1377    #[serde(skip_serializing_if = "Option::is_none")]
1378    pub rpc_port: Option<u16>,
1379    // Override the number of slots in an epoch.
1380    #[serde(skip_serializing_if = "Option::is_none")]
1381    pub slots_per_epoch: Option<String>,
1382    // The number of ticks in a slot
1383    #[serde(skip_serializing_if = "Option::is_none")]
1384    pub ticks_per_slot: Option<u16>,
1385    // Warp the ledger to WARP_SLOT after starting the validator.
1386    #[serde(skip_serializing_if = "Option::is_none")]
1387    pub warp_slot: Option<Slot>,
1388    // Deactivate one or more features.
1389    #[serde(skip_serializing_if = "Option::is_none")]
1390    pub deactivate_feature: Option<Vec<String>>,
1391}
1392
1393#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1394pub struct Validator {
1395    #[serde(skip_serializing_if = "Option::is_none")]
1396    pub account: Option<Vec<AccountEntry>>,
1397    #[serde(skip_serializing_if = "Option::is_none")]
1398    pub account_dir: Option<Vec<AccountDirEntry>>,
1399    pub bind_address: String,
1400    #[serde(skip_serializing_if = "Option::is_none")]
1401    pub clone: Option<Vec<CloneEntry>>,
1402    #[serde(skip_serializing_if = "Option::is_none")]
1403    pub dynamic_port_range: Option<String>,
1404    #[serde(skip_serializing_if = "Option::is_none")]
1405    pub faucet_port: Option<u16>,
1406    #[serde(skip_serializing_if = "Option::is_none")]
1407    pub faucet_sol: Option<String>,
1408    #[serde(skip_serializing_if = "Option::is_none")]
1409    pub geyser_plugin_config: Option<String>,
1410    #[serde(skip_serializing_if = "Option::is_none")]
1411    pub gossip_host: Option<String>,
1412    #[serde(skip_serializing_if = "Option::is_none")]
1413    pub gossip_port: Option<u16>,
1414    #[serde(skip_serializing_if = "Option::is_none")]
1415    pub url: Option<String>,
1416    pub ledger: String,
1417    #[serde(skip_serializing_if = "Option::is_none")]
1418    pub limit_ledger_size: Option<String>,
1419    pub rpc_port: u16,
1420    #[serde(skip_serializing_if = "Option::is_none")]
1421    pub slots_per_epoch: Option<String>,
1422    #[serde(skip_serializing_if = "Option::is_none")]
1423    pub ticks_per_slot: Option<u16>,
1424    #[serde(skip_serializing_if = "Option::is_none")]
1425    pub warp_slot: Option<Slot>,
1426    #[serde(skip_serializing_if = "Option::is_none")]
1427    pub deactivate_feature: Option<Vec<String>>,
1428}
1429
1430impl From<_Validator> for Validator {
1431    fn from(_validator: _Validator) -> Self {
1432        Self {
1433            account: _validator.account,
1434            account_dir: _validator.account_dir,
1435            bind_address: _validator
1436                .bind_address
1437                .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
1438            clone: _validator.clone,
1439            dynamic_port_range: _validator.dynamic_port_range,
1440            faucet_port: _validator.faucet_port,
1441            faucet_sol: _validator.faucet_sol,
1442            geyser_plugin_config: _validator.geyser_plugin_config,
1443            gossip_host: _validator.gossip_host,
1444            gossip_port: _validator.gossip_port,
1445            url: _validator.url,
1446            ledger: _validator
1447                .ledger
1448                .unwrap_or_else(|| get_default_ledger_path().display().to_string()),
1449            limit_ledger_size: _validator.limit_ledger_size,
1450            rpc_port: _validator.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1451            slots_per_epoch: _validator.slots_per_epoch,
1452            ticks_per_slot: _validator.ticks_per_slot,
1453            warp_slot: _validator.warp_slot,
1454            deactivate_feature: _validator.deactivate_feature,
1455        }
1456    }
1457}
1458
1459impl From<Validator> for _Validator {
1460    fn from(validator: Validator) -> Self {
1461        Self {
1462            account: validator.account,
1463            account_dir: validator.account_dir,
1464            bind_address: Some(validator.bind_address),
1465            clone: validator.clone,
1466            dynamic_port_range: validator.dynamic_port_range,
1467            faucet_port: validator.faucet_port,
1468            faucet_sol: validator.faucet_sol,
1469            geyser_plugin_config: validator.geyser_plugin_config,
1470            gossip_host: validator.gossip_host,
1471            gossip_port: validator.gossip_port,
1472            url: validator.url,
1473            ledger: Some(validator.ledger),
1474            limit_ledger_size: validator.limit_ledger_size,
1475            rpc_port: Some(validator.rpc_port),
1476            slots_per_epoch: validator.slots_per_epoch,
1477            ticks_per_slot: validator.ticks_per_slot,
1478            warp_slot: validator.warp_slot,
1479            deactivate_feature: validator.deactivate_feature,
1480        }
1481    }
1482}
1483
1484pub fn get_default_ledger_path() -> PathBuf {
1485    Path::new(".anchor").join("test-ledger")
1486}
1487
1488const DEFAULT_BIND_ADDRESS: &str = "127.0.0.1";
1489
1490impl Merge for _Validator {
1491    fn merge(&mut self, other: Self) {
1492        // Instantiating a new Self object here ensures that
1493        // this function will fail to compile if new fields get added
1494        // to Self. This is useful as a reminder if they also require merging
1495        *self = Self {
1496            account: match self.account.take() {
1497                None => other.account,
1498                Some(mut entries) => match other.account {
1499                    None => Some(entries),
1500                    Some(other_entries) => {
1501                        for other_entry in other_entries {
1502                            match entries
1503                                .iter()
1504                                .position(|my_entry| *my_entry.address == other_entry.address)
1505                            {
1506                                None => entries.push(other_entry),
1507                                Some(i) => entries[i] = other_entry,
1508                            };
1509                        }
1510                        Some(entries)
1511                    }
1512                },
1513            },
1514            account_dir: match self.account_dir.take() {
1515                None => other.account_dir,
1516                Some(mut entries) => match other.account_dir {
1517                    None => Some(entries),
1518                    Some(other_entries) => {
1519                        for other_entry in other_entries {
1520                            match entries
1521                                .iter()
1522                                .position(|my_entry| *my_entry.directory == other_entry.directory)
1523                            {
1524                                None => entries.push(other_entry),
1525                                Some(i) => entries[i] = other_entry,
1526                            };
1527                        }
1528                        Some(entries)
1529                    }
1530                },
1531            },
1532            bind_address: other.bind_address.or_else(|| self.bind_address.take()),
1533            clone: match self.clone.take() {
1534                None => other.clone,
1535                Some(mut entries) => match other.clone {
1536                    None => Some(entries),
1537                    Some(other_entries) => {
1538                        for other_entry in other_entries {
1539                            match entries
1540                                .iter()
1541                                .position(|my_entry| *my_entry.address == other_entry.address)
1542                            {
1543                                None => entries.push(other_entry),
1544                                Some(i) => entries[i] = other_entry,
1545                            };
1546                        }
1547                        Some(entries)
1548                    }
1549                },
1550            },
1551            dynamic_port_range: other
1552                .dynamic_port_range
1553                .or_else(|| self.dynamic_port_range.take()),
1554            faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
1555            faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
1556            geyser_plugin_config: other
1557                .geyser_plugin_config
1558                .or_else(|| self.geyser_plugin_config.take()),
1559            gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
1560            gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
1561            url: other.url.or_else(|| self.url.take()),
1562            ledger: other.ledger.or_else(|| self.ledger.take()),
1563            limit_ledger_size: other
1564                .limit_ledger_size
1565                .or_else(|| self.limit_ledger_size.take()),
1566            rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
1567            slots_per_epoch: other
1568                .slots_per_epoch
1569                .or_else(|| self.slots_per_epoch.take()),
1570            ticks_per_slot: other.ticks_per_slot.or_else(|| self.ticks_per_slot.take()),
1571            warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
1572            deactivate_feature: other
1573                .deactivate_feature
1574                .or_else(|| self.deactivate_feature.take()),
1575        };
1576    }
1577}
1578
1579#[derive(Debug, Clone)]
1580pub struct Program {
1581    pub lib_name: String,
1582    // Canonicalized path to the program directory
1583    pub path: PathBuf,
1584    pub idl: Option<Idl>,
1585}
1586
1587impl Program {
1588    pub fn pubkey(&self) -> Result<Pubkey> {
1589        self.keypair().map(|kp| kp.pubkey())
1590    }
1591
1592    pub fn keypair(&self) -> Result<Keypair> {
1593        let file = self.keypair_file()?;
1594        get_keypair(file.path())
1595    }
1596
1597    // Lazily initializes the keypair file with a new key if it doesn't exist.
1598    pub fn keypair_file(&self) -> Result<WithPath<File>> {
1599        let deploy_dir_path = target_dir()?.join("deploy");
1600        fs::create_dir_all(&deploy_dir_path)
1601            .with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
1602        let path = std::env::current_dir()
1603            .expect("Must have current dir")
1604            .join(deploy_dir_path.join(format!("{}-keypair.json", self.lib_name)));
1605        if path.exists() {
1606            return Ok(WithPath::new(
1607                File::open(&path)
1608                    .with_context(|| format!("Error opening file with path: {}", path.display()))?,
1609                path,
1610            ));
1611        }
1612        let program_kp = Keypair::new();
1613        let mut file = File::create(&path)
1614            .with_context(|| format!("Error creating file with path: {}", path.display()))?;
1615        file.write_all(format!("{:?}", &program_kp.to_bytes()).as_bytes())?;
1616        Ok(WithPath::new(file, path))
1617    }
1618
1619    pub fn binary_path(&self, verifiable: bool) -> Result<PathBuf> {
1620        let path = target_dir()?
1621            .join(if verifiable { "verifiable" } else { "deploy" })
1622            .join(&self.lib_name)
1623            .with_extension("so");
1624
1625        Ok(std::env::current_dir()
1626            .expect("Must have current dir")
1627            .join(path))
1628    }
1629}
1630
1631#[derive(Debug, Default)]
1632pub struct ProgramDeployment {
1633    pub address: Pubkey,
1634    pub path: Option<String>,
1635    pub idl: Option<String>,
1636}
1637
1638impl TryFrom<_ProgramDeployment> for ProgramDeployment {
1639    type Error = anyhow::Error;
1640    fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
1641        Ok(ProgramDeployment {
1642            address: pd.address.parse()?,
1643            path: pd.path,
1644            idl: pd.idl,
1645        })
1646    }
1647}
1648
1649#[derive(Debug, Default, Serialize, Deserialize)]
1650pub struct _ProgramDeployment {
1651    pub address: String,
1652    pub path: Option<String>,
1653    pub idl: Option<String>,
1654}
1655
1656impl From<&ProgramDeployment> for _ProgramDeployment {
1657    fn from(pd: &ProgramDeployment) -> Self {
1658        Self {
1659            address: pd.address.to_string(),
1660            path: pd.path.clone(),
1661            idl: pd.idl.clone(),
1662        }
1663    }
1664}
1665
1666pub struct ProgramWorkspace {
1667    pub name: String,
1668    pub program_id: Pubkey,
1669    pub idl: Idl,
1670}
1671
1672#[derive(Debug, Serialize, Deserialize)]
1673pub struct AnchorPackage {
1674    pub name: String,
1675    pub address: String,
1676    pub idl: Option<String>,
1677}
1678
1679impl AnchorPackage {
1680    pub fn from(name: String, cfg: &WithPath<Config>) -> Result<Self> {
1681        let cluster = &cfg.provider.cluster;
1682        if cluster != &Cluster::Mainnet {
1683            return Err(anyhow!("Publishing requires the mainnet cluster"));
1684        }
1685        let program_details = cfg
1686            .programs
1687            .get(cluster)
1688            .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
1689            .get(&name)
1690            .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
1691        let idl = program_details.idl.clone();
1692        let address = program_details.address.to_string();
1693        Ok(Self { name, address, idl })
1694    }
1695}
1696
1697#[derive(Debug, Serialize, Deserialize)]
1698#[serde(rename_all = "camelCase")]
1699pub struct SurfnetInfoResponse {
1700    pub runbook_executions: Vec<RunbookExecution>,
1701}
1702#[derive(Debug, Serialize, Deserialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct RunbookExecution {
1705    #[serde(rename = "startedAt")]
1706    pub started_at: u32,
1707    #[serde(rename = "completedAt")]
1708    pub completed_at: Option<u32>,
1709    #[serde(rename = "runbookId")]
1710    pub runbook_id: String,
1711    pub errors: Option<Vec<String>>,
1712}
1713
1714#[macro_export]
1715macro_rules! home_path {
1716    ($my_struct:ident, $path:literal) => {
1717        #[derive(Clone, Debug, AbsolutePath)]
1718        pub struct $my_struct(::std::path::PathBuf);
1719
1720        impl Default for $my_struct {
1721            fn default() -> Self {
1722                $my_struct(home_dir().unwrap().join($path))
1723            }
1724        }
1725
1726        impl $my_struct {
1727            fn stringify_with_tilde(&self) -> String {
1728                self.0
1729                    .display()
1730                    .to_string()
1731                    .replacen(home_dir().unwrap().to_str().unwrap(), "~", 1)
1732            }
1733        }
1734
1735        impl FromStr for $my_struct {
1736            type Err = anyhow::Error;
1737
1738            fn from_str(s: &str) -> Result<Self, Self::Err> {
1739                Ok(Self(::std::path::PathBuf::from(s)))
1740            }
1741        }
1742
1743        impl fmt::Display for $my_struct {
1744            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1745                write!(f, "{}", self.0.display())
1746            }
1747        }
1748    };
1749}
1750
1751home_path!(WalletPath, ".config/solana/id.json");
1752
1753#[cfg(test)]
1754mod tests {
1755    use super::*;
1756
1757    const BASE_CONFIG: &str = "
1758        [provider]
1759        cluster = \"localnet\"
1760        wallet = \"id.json\"
1761    ";
1762
1763    #[test]
1764    fn parse_custom_cluster_str() {
1765        let config = Config::from_str(
1766            "
1767        [provider]
1768        cluster = \"http://my-url.com\"
1769        wallet = \"id.json\"
1770    ",
1771        )
1772        .unwrap();
1773        assert!(!config.features.skip_lint);
1774
1775        // Make sure the layout of `provider.cluster` stays the same after serialization
1776        assert!(config
1777            .to_string()
1778            .contains(r#"cluster = "http://my-url.com""#));
1779    }
1780
1781    #[test]
1782    fn parse_custom_cluster_map() {
1783        let config = Config::from_str(
1784            "
1785        [provider]
1786        cluster = { http = \"http://my-url.com\", ws = \"ws://my-url.com\" }
1787        wallet = \"id.json\"
1788    ",
1789        )
1790        .unwrap();
1791        assert!(!config.features.skip_lint);
1792    }
1793
1794    #[test]
1795    fn parse_skip_lint_no_section() {
1796        let config = Config::from_str(BASE_CONFIG).unwrap();
1797        assert!(!config.features.skip_lint);
1798    }
1799
1800    #[test]
1801    fn parse_skip_lint_no_value() {
1802        let string = BASE_CONFIG.to_owned() + "[features]";
1803        let config = Config::from_str(&string).unwrap();
1804        assert!(!config.features.skip_lint);
1805    }
1806
1807    #[test]
1808    fn parse_skip_lint_true() {
1809        let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true";
1810        let config = Config::from_str(&string).unwrap();
1811        assert!(config.features.skip_lint);
1812    }
1813
1814    #[test]
1815    fn parse_clients_section() {
1816        let toml = BASE_CONFIG.to_owned()
1817            + r#"
1818[clients]
1819auto = true
1820rust = true
1821js = false
1822js-umi = { enable = true }
1823go = { enable = true, path = "go-client" }
1824"#;
1825        let config = Config::from_str(&toml).unwrap();
1826        let clients = &config.clients;
1827        assert!(clients.auto);
1828        assert!(clients.rust.as_ref().unwrap().is_enabled());
1829        assert!(!clients.js.as_ref().unwrap().is_enabled());
1830        assert!(clients.js_umi.as_ref().unwrap().is_enabled());
1831        let go = clients.go.as_ref().unwrap();
1832        assert!(go.is_enabled());
1833        assert_eq!(go.path(), Some("go-client"));
1834
1835        let resolved = clients.enabled(Path::new("clients"));
1836        // js is explicitly disabled; rust gets the default path; go uses the
1837        // override; js-umi is enabled with no `path` so it falls back to the
1838        // base+id default.
1839        assert_eq!(
1840            resolved,
1841            vec![
1842                ("js-umi", PathBuf::from("clients/js-umi")),
1843                ("rust", PathBuf::from("clients/rust")),
1844                ("go", PathBuf::from("go-client")),
1845            ]
1846        );
1847    }
1848
1849    #[test]
1850    fn clients_section_round_trips() {
1851        // Round-trip the table form through Display+FromStr to make sure
1852        // serde's untagged enum picks the same variant we wrote out.
1853        let toml = BASE_CONFIG.to_owned()
1854            + r#"
1855[clients]
1856auto = true
1857rust = true
1858go = { enable = true, path = "go-client" }
1859"#;
1860        let config = Config::from_str(&toml).unwrap();
1861        let serialized = config.to_string();
1862        let reparsed = Config::from_str(&serialized).unwrap();
1863        assert!(reparsed.clients.auto);
1864        assert!(reparsed.clients.rust.as_ref().unwrap().is_enabled());
1865        assert_eq!(
1866            reparsed.clients.go.as_ref().and_then(|g| g.path()),
1867            Some("go-client"),
1868        );
1869    }
1870
1871    #[test]
1872    fn clients_section_omitted_when_default() {
1873        // An empty [clients] section should not be emitted: a fresh `anchor
1874        // init` workspace has no clients configured, so we don't want a
1875        // confusing empty stanza in `Anchor.toml`.
1876        let config = Config::from_str(BASE_CONFIG).unwrap();
1877        assert!(!config.to_string().contains("[clients]"));
1878    }
1879
1880    #[test]
1881    fn unknown_clients_field_is_rejected() {
1882        // `deny_unknown_fields` on `ClientsConfig` catches typos like
1883        // `[clients] python = true` so users don't silently ship a config
1884        // that no Codama renderer will pick up.
1885        let toml = BASE_CONFIG.to_owned() + "[clients]\npython = true\n";
1886        assert!(Config::from_str(&toml).is_err());
1887    }
1888
1889    #[test]
1890    fn parse_skip_lint_false() {
1891        let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false";
1892        let config = Config::from_str(&string).unwrap();
1893        assert!(!config.features.skip_lint);
1894    }
1895
1896    #[test]
1897    fn test_toml_resolves_account_dir_relative_to_file() {
1898        let dir = tempfile::tempdir().unwrap();
1899        let suite_dir = dir.path().join("tests").join("suite");
1900        let accounts_dir = suite_dir.join("accounts");
1901        fs::create_dir_all(&accounts_dir).unwrap();
1902
1903        let account_file = accounts_dir.join("account.json");
1904        fs::write(&account_file, "{}").unwrap();
1905
1906        let test_toml = suite_dir.join("Test.toml");
1907        fs::write(
1908            &test_toml,
1909            r#"
1910[scripts]
1911test = "true"
1912
1913[[test.validator.account]]
1914address = "3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj"
1915filename = "accounts/account.json"
1916
1917[[test.validator.account_dir]]
1918directory = "accounts"
1919"#,
1920        )
1921        .unwrap();
1922
1923        let parsed = TestToml::from_path(test_toml).unwrap();
1924        let validator = parsed.test.unwrap().validator.unwrap();
1925
1926        assert_eq!(
1927            validator.account.unwrap()[0].filename,
1928            account_file.canonicalize().unwrap().display().to_string()
1929        );
1930        assert_eq!(
1931            validator.account_dir.unwrap()[0].directory,
1932            accounts_dir.canonicalize().unwrap().display().to_string()
1933        );
1934    }
1935}