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, BTreeSet, 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        sync::{LazyLock, Mutex},
33    },
34    walkdir::WalkDir,
35};
36
37pub const SURFPOOL_HOST: &str = "127.0.0.1";
38/// Wrapper around CommitmentLevel to support case-insensitive parsing
39#[derive(Debug, Clone, Copy, PartialEq, Eq, AbsolutePath)]
40pub struct CaseInsensitiveCommitmentLevel(pub CommitmentLevel);
41
42impl FromStr for CaseInsensitiveCommitmentLevel {
43    type Err = String;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        // Convert to lowercase for case-insensitive matching
47        let lowercase = s.to_lowercase();
48        let commitment = CommitmentLevel::from_str(&lowercase).map_err(|_| {
49            format!(
50                "Invalid commitment level '{}'. Valid values are: processed, confirmed, finalized",
51                s
52            )
53        })?;
54        Ok(CaseInsensitiveCommitmentLevel(commitment))
55    }
56}
57
58impl From<CaseInsensitiveCommitmentLevel> for CommitmentLevel {
59    fn from(val: CaseInsensitiveCommitmentLevel) -> Self {
60        val.0
61    }
62}
63
64pub trait Merge: Sized {
65    fn merge(&mut self, _other: Self) {}
66}
67
68#[derive(Default, Debug, Parser, AbsolutePath)]
69pub struct ConfigOverride {
70    /// Cluster override.
71    #[clap(global = true, long = "provider.cluster")]
72    pub cluster: Option<Cluster>,
73    /// Wallet override.
74    #[clap(global = true, long = "provider.wallet")]
75    pub wallet: Option<WalletPath>,
76    /// Commitment override (valid values: processed, confirmed, finalized).
77    #[clap(global = true, long = "commitment")]
78    pub commitment: Option<CaseInsensitiveCommitmentLevel>,
79}
80
81#[derive(Debug)]
82pub struct WithPath<T> {
83    inner: T,
84    path: PathBuf,
85}
86
87impl<T> WithPath<T> {
88    pub fn new(inner: T, path: PathBuf) -> Self {
89        Self { inner, path }
90    }
91
92    pub fn path(&self) -> &PathBuf {
93        &self.path
94    }
95
96    pub fn into_inner(self) -> T {
97        self.inner
98    }
99}
100
101impl<T> std::convert::AsRef<T> for WithPath<T> {
102    fn as_ref(&self) -> &T {
103        &self.inner
104    }
105}
106
107#[derive(Debug, Clone, PartialEq)]
108pub struct Manifest(cargo_toml::Manifest);
109
110impl Manifest {
111    pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
112        cargo_toml::Manifest::from_path(&p)
113            .map(Manifest)
114            .map_err(anyhow::Error::from)
115            .with_context(|| format!("Error reading manifest from path: {}", p.as_ref().display()))
116    }
117
118    pub fn lib_name(&self) -> Result<String> {
119        match &self.lib {
120            Some(cargo_toml::Product {
121                name: Some(name), ..
122            }) => Ok(name.to_owned()),
123            _ => self
124                .package
125                .as_ref()
126                .ok_or_else(|| anyhow!("package section not provided"))
127                .map(|pkg| pkg.name.to_snake_case()),
128        }
129    }
130
131    pub fn version(&self) -> String {
132        match &self.package {
133            Some(package) => package.version().to_string(),
134            _ => "0.0.0".to_string(),
135        }
136    }
137
138    // Climbs each parent directory from the current dir until we find a Cargo.toml
139    pub fn discover() -> Result<Option<WithPath<Manifest>>> {
140        Manifest::discover_from_path(std::env::current_dir()?)
141    }
142
143    // Climbs each parent directory from a given starting directory until we find a Cargo.toml.
144    pub fn discover_from_path(start_from: PathBuf) -> Result<Option<WithPath<Manifest>>> {
145        let mut cwd_opt = Some(start_from.as_path());
146
147        while let Some(cwd) = cwd_opt {
148            let mut anchor_toml = false;
149
150            for f in fs::read_dir(cwd).with_context(|| {
151                format!("Error reading the directory with path: {}", cwd.display())
152            })? {
153                let p = f
154                    .with_context(|| {
155                        format!("Error reading the directory with path: {}", cwd.display())
156                    })?
157                    .path();
158                if let Some(filename) = p.file_name().and_then(|name| name.to_str()) {
159                    if filename == "Cargo.toml" {
160                        return Ok(Some(WithPath::new(Manifest::from_path(&p)?, p)));
161                    }
162                    if filename == "Anchor.toml" {
163                        anchor_toml = true;
164                    }
165                }
166            }
167
168            // Not found. Go up a directory level, but don't go up from Anchor.toml
169            if anchor_toml {
170                break;
171            }
172
173            cwd_opt = cwd.parent();
174        }
175
176        Ok(None)
177    }
178}
179
180impl Deref for Manifest {
181    type Target = cargo_toml::Manifest;
182
183    fn deref(&self) -> &Self::Target {
184        &self.0
185    }
186}
187
188impl WithPath<Config> {
189    pub fn get_program_list(&self) -> Result<Vec<PathBuf>> {
190        // Canonicalize the workspace filepaths to compare with relative paths.
191        let (members, exclude) = self.canonicalize_workspace()?;
192
193        // Get all candidate programs.
194        //
195        // If [workspace.members] exists, then use that.
196        // Otherwise, default to `programs/*`.
197        let program_paths: Vec<PathBuf> = {
198            if members.is_empty() {
199                let path = self.path().parent().unwrap().join("programs");
200                if let Ok(entries) = fs::read_dir(path) {
201                    entries
202                        .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false))
203                        .map(|dir| dir.map(|d| d.path().canonicalize().unwrap()))
204                        .collect::<Vec<Result<PathBuf, std::io::Error>>>()
205                        .into_iter()
206                        .collect::<Result<Vec<PathBuf>, std::io::Error>>()?
207                } else {
208                    Vec::new()
209                }
210            } else {
211                members
212            }
213        };
214
215        // Filter out everything part of the exclude array.
216        Ok(program_paths
217            .into_iter()
218            .filter(|m| !exclude.contains(m))
219            .collect())
220    }
221
222    pub fn read_all_programs(&self) -> Result<Vec<Program>> {
223        let mut r = vec![];
224        for path in self.get_program_list()? {
225            let cargo = Manifest::from_path(path.join("Cargo.toml"))?;
226            let lib_name = cargo.lib_name()?;
227
228            let idl_filepath = target_dir()?
229                .join("idl")
230                .join(&lib_name)
231                .with_extension("json");
232            let idl = fs::read(idl_filepath)
233                .ok()
234                .map(|bytes| serde_json::from_reader(&*bytes))
235                .transpose()?;
236
237            r.push(Program {
238                lib_name,
239                path,
240                idl,
241            });
242        }
243        Ok(r)
244    }
245
246    /// Read and get all the programs from the workspace.
247    ///
248    /// This method will only return the given program if `name` exists.
249    pub fn get_programs(&self, name: Option<String>) -> Result<Vec<Program>> {
250        let programs = self.read_all_programs()?;
251        let programs = match name {
252            Some(name) => vec![programs
253                .iter()
254                .find(|program| {
255                    program.lib_name == name
256                        || program
257                            .path
258                            .file_name()
259                            .and_then(|f| f.to_str())
260                            .map(|f| f == name)
261                            .unwrap_or(false)
262                })
263                .cloned()
264                .ok_or_else(|| {
265                    let mut available_programs: Vec<String> =
266                        programs.iter().map(|p| p.lib_name.clone()).collect();
267                    available_programs.sort();
268
269                    if available_programs.is_empty() {
270                        anyhow!("Program '{name}' not found. No programs available in workspace.")
271                    } else {
272                        anyhow!(
273                            "Program '{name}' not found.\n\nAvailable programs:\n  {}",
274                            available_programs.join("\n  ")
275                        )
276                    }
277                })?],
278            None => programs,
279        };
280
281        Ok(programs)
282    }
283
284    /// Get the specified program from the workspace.
285    pub fn get_program(&self, name: &str) -> Result<Program> {
286        self.get_programs(Some(name.to_owned()))?
287            .into_iter()
288            .next()
289            .ok_or_else(|| anyhow!("Expected a program"))
290    }
291
292    pub fn canonicalize_workspace(&self) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
293        let members = self.process_paths(&self.workspace.members)?;
294        let exclude = self.process_paths(&self.workspace.exclude)?;
295        Ok((members, exclude))
296    }
297
298    fn process_paths(&self, paths: &[String]) -> Result<Vec<PathBuf>, Error> {
299        let base_path = self.path().parent().unwrap();
300        paths
301            .iter()
302            .flat_map(|m| {
303                let path = base_path.join(m);
304                if m.ends_with("/*") {
305                    let dir = path.parent().unwrap();
306                    match fs::read_dir(dir) {
307                        Ok(entries) => entries
308                            .filter_map(|entry| entry.ok())
309                            .map(|entry| self.process_single_path(&entry.path()))
310                            .collect(),
311                        Err(e) => vec![Err(Error::new(io::Error::other(format!(
312                            "Error reading directory {dir:?}: {e}"
313                        ))))],
314                    }
315                } else {
316                    vec![self.process_single_path(&path)]
317                }
318            })
319            .collect()
320    }
321
322    fn process_single_path(&self, path: &PathBuf) -> Result<PathBuf, Error> {
323        path.canonicalize().map_err(|e| {
324            Error::new(io::Error::other(format!(
325                "Error canonicalizing path {path:?}: {e}"
326            )))
327        })
328    }
329}
330
331impl WalletPath {
332    fn resolve_relative_to(self, base: &Path) -> Self {
333        if self.0.is_relative() {
334            Self(base.join(self.0))
335        } else {
336            self
337        }
338    }
339}
340
341impl<T> std::ops::Deref for WithPath<T> {
342    type Target = T;
343    fn deref(&self) -> &Self::Target {
344        &self.inner
345    }
346}
347
348impl<T> std::ops::DerefMut for WithPath<T> {
349    fn deref_mut(&mut self) -> &mut Self::Target {
350        &mut self.inner
351    }
352}
353
354#[derive(Debug, Default)]
355pub struct Config {
356    pub toolchain: ToolchainConfig,
357    pub features: FeaturesConfig,
358    pub provider: ProviderConfig,
359    pub programs: ProgramsConfig,
360    pub scripts: ScriptsConfig,
361    pub hooks: HooksConfig,
362    pub workspace: WorkspaceConfig,
363    pub clients: ClientsConfig,
364    // Separate entry next to test_config because
365    // "anchor localnet" only has access to the Anchor.toml,
366    // not the Test.toml files
367    pub validator: Option<ValidatorType>,
368    pub test_validator: Option<TestValidator>,
369    pub test_config: Option<TestConfig>,
370    pub surfpool_config: Option<SurfpoolConfig>,
371    /// If `Some(true)`, `anchor test` won't auto-start a validator for this
372    /// workspace. Emitted by `anchor init` for in-process test templates
373    /// (litesvm / mollusk) where the test harness never opens an RPC.
374    pub skip_local_validator: Option<bool>,
375}
376
377#[derive(ValueEnum, Parser, Clone, Copy, PartialEq, Eq, Debug, AbsolutePath)]
378pub enum ValidatorType {
379    /// Use Surfpool validator (default)
380    Surfpool,
381    /// Use Solana test validator
382    Legacy,
383}
384#[derive(Default, Clone, Debug, Serialize, Deserialize)]
385pub struct ToolchainConfig {
386    pub anchor_version: Option<String>,
387    pub solana_version: Option<String>,
388    pub package_manager: Option<PackageManager>,
389}
390
391/// Package manager to use for the project.
392///
393/// No `Default` impl; this enum represents an explicit user choice. Call sites
394/// that need a concrete package manager should go through `resolve_package_manager`
395/// so fallback behavior and missing-binary diagnostics stay centralized.
396#[derive(Clone, Debug, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize, AbsolutePath)]
397#[serde(rename_all = "lowercase")]
398pub enum PackageManager {
399    /// Use npm as the package manager.
400    NPM,
401    /// Use yarn as the package manager.
402    Yarn,
403    /// Use pnpm as the package manager.
404    PNPM,
405    /// Use bun as the package manager.
406    Bun,
407}
408
409impl std::fmt::Display for PackageManager {
410    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
411        let pkg_manager_str = match self {
412            PackageManager::NPM => "npm",
413            PackageManager::Yarn => "yarn",
414            PackageManager::PNPM => "pnpm",
415            PackageManager::Bun => "bun",
416        };
417
418        write!(f, "{pkg_manager_str}")
419    }
420}
421
422#[derive(Clone, Debug, Serialize, Deserialize)]
423pub struct FeaturesConfig {
424    /// Enable account resolution.
425    ///
426    /// Not able to specify default bool value: https://github.com/serde-rs/serde/issues/368
427    #[serde(default = "FeaturesConfig::get_default_resolution")]
428    pub resolution: bool,
429    /// Disable safety comment checks
430    #[serde(default, rename = "skip-lint")]
431    pub skip_lint: bool,
432}
433
434impl FeaturesConfig {
435    fn get_default_resolution() -> bool {
436        true
437    }
438}
439
440impl Default for FeaturesConfig {
441    fn default() -> Self {
442        Self {
443            resolution: Self::get_default_resolution(),
444            skip_lint: false,
445        }
446    }
447}
448
449#[derive(Debug, Default)]
450pub struct ProviderConfig {
451    pub cluster: Cluster,
452    pub wallet: WalletPath,
453}
454
455pub type ScriptsConfig = BTreeMap<String, String>;
456
457pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
458
459#[derive(Default, Clone, Debug, Serialize, Deserialize)]
460#[serde(deny_unknown_fields)]
461pub struct HooksConfig {
462    #[serde(alias = "pre-build")]
463    pre_build: Option<Hook>,
464    #[serde(alias = "post-build")]
465    post_build: Option<Hook>,
466    #[serde(alias = "pre-test")]
467    pre_test: Option<Hook>,
468    #[serde(alias = "post-test")]
469    post_test: Option<Hook>,
470    #[serde(alias = "pre-deploy")]
471    pre_deploy: Option<Hook>,
472    #[serde(alias = "post-deploy")]
473    post_deploy: Option<Hook>,
474}
475
476#[derive(Clone, Debug, Serialize, Deserialize)]
477#[serde(untagged)]
478enum Hook {
479    Single(String),
480    List(Vec<String>),
481}
482
483impl Hook {
484    pub fn hooks(&self) -> &[String] {
485        match self {
486            Self::Single(h) => std::slice::from_ref(h),
487            Self::List(l) => l.as_slice(),
488        }
489    }
490}
491
492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub enum HookType {
494    PreBuild,
495    PostBuild,
496    PreTest,
497    PostTest,
498    PreDeploy,
499    PostDeploy,
500}
501
502/// `[clients]` section of `Anchor.toml`.
503///
504/// Declares which Codama-generated client SDKs the workspace ships, where
505/// they live on disk, and whether they should be regenerated automatically.
506///
507/// TOML shape:
508///
509/// ```toml
510/// [clients]
511/// auto = true
512/// rust = true
513/// js = { enable = true }
514/// go = { enable = true, path = "go-client" }
515/// js-umi = false
516/// ```
517#[derive(Debug, Default, Clone, Serialize, Deserialize)]
518pub struct ClientsConfig {
519    /// Regenerate clients automatically on `anchor build`.
520    #[serde(default, skip_serializing_if = "is_false")]
521    pub auto: bool,
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    pub js: Option<ClientLanguageConfig>,
524    #[serde(
525        default,
526        rename = "js-umi",
527        alias = "js_umi",
528        skip_serializing_if = "Option::is_none"
529    )]
530    pub js_umi: Option<ClientLanguageConfig>,
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub rust: Option<ClientLanguageConfig>,
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub go: Option<ClientLanguageConfig>,
535}
536
537/// Per-language client entry. Accepts either a bare `bool` (`rust = true`)
538/// or a table with explicit `enable` and optional `path` keys.
539#[derive(Debug, Clone, Serialize, Deserialize)]
540#[serde(untagged)]
541pub enum ClientLanguageConfig {
542    /// `lang = true` / `lang = false`.
543    Enabled(bool),
544    /// `lang = { enable = bool, path = "..." }`.
545    Detailed {
546        #[serde(default = "ClientLanguageConfig::default_enable")]
547        enable: bool,
548        #[serde(default, skip_serializing_if = "Option::is_none")]
549        path: Option<String>,
550    },
551}
552
553impl ClientLanguageConfig {
554    fn default_enable() -> bool {
555        true
556    }
557
558    pub fn is_enabled(&self) -> bool {
559        match self {
560            Self::Enabled(enabled) => *enabled,
561            Self::Detailed { enable, .. } => *enable,
562        }
563    }
564
565    pub fn path(&self) -> Option<&str> {
566        match self {
567            Self::Enabled(_) => None,
568            Self::Detailed { path, .. } => path.as_deref(),
569        }
570    }
571}
572
573/// Stable identifiers used both as TOML keys and as Codama script names.
574pub const CLIENT_LANGUAGES: &[&str] = &["js", "js-umi", "rust", "go"];
575
576impl ClientsConfig {
577    /// Look up a language entry by its [`CLIENT_LANGUAGES`] id.
578    pub fn get(&self, language: &str) -> Option<&ClientLanguageConfig> {
579        match language {
580            "js" => self.js.as_ref(),
581            "js-umi" => self.js_umi.as_ref(),
582            "rust" => self.rust.as_ref(),
583            "go" => self.go.as_ref(),
584            _ => None,
585        }
586    }
587
588    /// Languages the user has explicitly enabled, paired with the resolved
589    /// output directory (`<workspace>/clients/<lang>` if no `path` was set on
590    /// the entry). Relative custom paths are resolved from the workspace root,
591    /// not the process cwd.
592    pub fn enabled(&self, workspace_dir: &Path) -> Vec<(&'static str, PathBuf)> {
593        let base = workspace_dir.join("clients");
594        CLIENT_LANGUAGES
595            .iter()
596            .filter_map(|&lang| {
597                let entry = self.get(lang)?;
598                if !entry.is_enabled() {
599                    return None;
600                }
601                let path = entry
602                    .path()
603                    .map(|path| resolve_client_path(workspace_dir, path))
604                    .unwrap_or_else(|| base.join(lang));
605                Some((lang, path))
606            })
607            .collect()
608    }
609}
610
611fn resolve_client_path(workspace_dir: &Path, path: &str) -> PathBuf {
612    let path = PathBuf::from(path);
613    if path.is_absolute() {
614        path
615    } else {
616        workspace_dir.join(path)
617    }
618}
619
620fn is_false(b: &bool) -> bool {
621    !*b
622}
623
624#[derive(Debug, Default, Clone, Serialize, Deserialize)]
625pub struct WorkspaceConfig {
626    #[serde(default, skip_serializing_if = "Vec::is_empty")]
627    pub members: Vec<String>,
628    #[serde(default, skip_serializing_if = "Vec::is_empty")]
629    pub exclude: Vec<String>,
630    #[serde(default, skip_serializing_if = "String::is_empty")]
631    pub idls: String,
632    #[serde(default, skip_serializing_if = "String::is_empty")]
633    pub types: String,
634}
635
636#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug, AbsolutePath)]
637pub enum BootstrapMode {
638    None,
639    Debian,
640}
641
642#[derive(Debug, Clone)]
643pub struct BuildConfig {
644    pub verifiable: bool,
645    pub solana_version: Option<String>,
646    pub docker_image: String,
647    pub bootstrap: BootstrapMode,
648}
649
650impl Config {
651    pub fn add_test_config(
652        &mut self,
653        root: impl AsRef<Path>,
654        test_paths: Vec<PathBuf>,
655    ) -> Result<()> {
656        self.test_config = TestConfig::discover(root, test_paths)?;
657        Ok(())
658    }
659
660    pub fn docker(&self) -> String {
661        let version = self
662            .toolchain
663            .anchor_version
664            .as_deref()
665            .unwrap_or(crate::DOCKER_BUILDER_VERSION);
666        format!("quay.io/ottersec/anchor:v{version}")
667    }
668
669    pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
670        Config::_discover().map(|opt| {
671            opt.map(|mut cfg| {
672                if let Some(cluster) = cfg_override.cluster.clone() {
673                    cfg.provider.cluster = cluster;
674                }
675                if let Some(wallet) = cfg_override.wallet.clone() {
676                    cfg.provider.wallet = wallet;
677                }
678                cfg
679            })
680        })
681    }
682
683    // Climbs each parent directory until we find an Anchor.toml.
684    fn _discover() -> Result<Option<WithPath<Config>>> {
685        let _cwd = std::env::current_dir()?;
686        let mut cwd_opt = Some(_cwd.as_path());
687
688        while let Some(cwd) = cwd_opt {
689            for f in fs::read_dir(cwd).with_context(|| {
690                format!("Error reading the directory with path: {}", cwd.display())
691            })? {
692                let p = f
693                    .with_context(|| {
694                        format!("Error reading the directory with path: {}", cwd.display())
695                    })?
696                    .path();
697                if let Some(filename) = p.file_name() {
698                    if filename.to_str() == Some("Anchor.toml") {
699                        let config_dir = p.parent().unwrap();
700                        // Make sure the program id is correct (only on the initial build)
701                        let mut cfg = Config::from_path(&p)?;
702                        let deploy_dir = target_dir()?.join("deploy");
703                        if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
704                            println!("Updating program ids...");
705                            fs::create_dir_all(deploy_dir)?;
706                            keys_sync(&ConfigOverride::default(), None)?;
707                            cfg = Config::from_path(&p)?;
708                        }
709                        cfg.provider.wallet = cfg.provider.wallet.resolve_relative_to(config_dir);
710
711                        return Ok(Some(WithPath::new(cfg, p)));
712                    }
713                }
714            }
715
716            cwd_opt = cwd.parent();
717        }
718
719        Ok(None)
720    }
721
722    fn from_path(path: impl AsRef<Path>) -> Result<Self> {
723        let path = path.as_ref();
724        let cfg = fs::read_to_string(path)
725            .with_context(|| format!("Error reading configuration file: {path:?}"))?;
726
727        let mut unused = BTreeSet::new();
728        let de = toml::Deserializer::new(&cfg);
729        let cfg: _Config = serde_ignored::deserialize(de, |path| {
730            unused.insert(path.to_string());
731        })?;
732
733        // File path -> unused field paths
734        static CACHE: LazyLock<Mutex<HashMap<PathBuf, BTreeSet<String>>>> =
735            LazyLock::new(Default::default);
736        let mut cache = CACHE
737            .lock()
738            .map_err(|e| anyhow!("Failed to acquire the cache lock ({path:?}): {e}"))?;
739        match cache.get(path) {
740            Some(cached_unused) if *cached_unused == unused => return Self::try_from(cfg),
741            _ => cache.insert(path.to_path_buf(), unused.clone()),
742        };
743
744        if let Some(paths) = unused.into_iter().reduce(|mut acc, path| {
745            if !acc.is_empty() {
746                acc.push_str(", ");
747            }
748
749            acc.push('`');
750            acc.push_str(&path);
751            acc.push('`');
752            acc
753        }) {
754            eprintln!("Warning: Unused Anchor.toml field(s): {paths}");
755        }
756
757        Self::try_from(cfg)
758    }
759
760    pub fn wallet_kp(&self) -> Result<Keypair> {
761        get_keypair(Path::new(&self.provider.wallet.0))
762    }
763
764    pub fn run_hooks(&self, hook_type: HookType) -> Result<()> {
765        let hooks = match hook_type {
766            HookType::PreBuild => &self.hooks.pre_build,
767            HookType::PostBuild => &self.hooks.post_build,
768            HookType::PreTest => &self.hooks.pre_test,
769            HookType::PostTest => &self.hooks.post_test,
770            HookType::PreDeploy => &self.hooks.pre_deploy,
771            HookType::PostDeploy => &self.hooks.post_deploy,
772        };
773        let cmds = hooks.as_ref().map(Hook::hooks).unwrap_or_default();
774        for cmd in cmds {
775            let status = Command::new("bash")
776                .arg("-c")
777                .arg(cmd)
778                .status()
779                .with_context(|| format!("failed to execute `{cmd}`"))?;
780            if !status.success() {
781                match status.code() {
782                    Some(code) => bail!("`{cmd}` failed with exit code {code}"),
783                    None => bail!("`{cmd}` killed by signal"),
784                }
785            }
786        }
787        Ok(())
788    }
789}
790
791#[derive(Debug, Serialize, Deserialize)]
792struct _Config {
793    toolchain: Option<ToolchainConfig>,
794    features: Option<FeaturesConfig>,
795    programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
796    provider: Provider,
797    workspace: Option<WorkspaceConfig>,
798    scripts: Option<ScriptsConfig>,
799    hooks: Option<HooksConfig>,
800    test: Option<_TestValidator>,
801    surfpool: Option<_SurfpoolConfig>,
802    #[serde(skip_serializing_if = "Option::is_none")]
803    skip_local_validator: Option<bool>,
804    #[serde(skip_serializing_if = "Option::is_none")]
805    clients: Option<ClientsConfig>,
806}
807
808#[derive(Debug, Serialize, Deserialize)]
809struct Provider {
810    #[serde(serialize_with = "ser_cluster", deserialize_with = "des_cluster")]
811    cluster: Cluster,
812    wallet: String,
813}
814
815fn ser_cluster<S: Serializer>(cluster: &Cluster, s: S) -> Result<S::Ok, S::Error> {
816    match cluster {
817        Cluster::Custom(http, ws) => {
818            match (Url::parse(http), Url::parse(ws)) {
819                // If `ws` was derived from `http`, serialize `http` as string
820                (Ok(h), Ok(w)) if h.domain() == w.domain() => s.serialize_str(http),
821                _ => {
822                    let mut map = s.serialize_map(Some(2))?;
823                    map.serialize_entry("http", http)?;
824                    map.serialize_entry("ws", ws)?;
825                    map.end()
826                }
827            }
828        }
829        _ => s.serialize_str(&cluster.to_string()),
830    }
831}
832
833fn des_cluster<'de, D>(deserializer: D) -> Result<Cluster, D::Error>
834where
835    D: Deserializer<'de>,
836{
837    struct StringOrCustomCluster(PhantomData<fn() -> Cluster>);
838
839    impl<'de> Visitor<'de> for StringOrCustomCluster {
840        type Value = Cluster;
841
842        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
843            formatter.write_str("string or map")
844        }
845
846        fn visit_str<E>(self, value: &str) -> Result<Cluster, E>
847        where
848            E: de::Error,
849        {
850            value.parse().map_err(de::Error::custom)
851        }
852
853        fn visit_map<M>(self, mut map: M) -> Result<Cluster, M::Error>
854        where
855            M: MapAccess<'de>,
856        {
857            // Gets keys
858            if let (Some((http_key, http_value)), Some((ws_key, ws_value))) = (
859                map.next_entry::<String, String>()?,
860                map.next_entry::<String, String>()?,
861            ) {
862                // Checks keys
863                if http_key != "http" || ws_key != "ws" {
864                    return Err(de::Error::custom("Invalid key"));
865                }
866
867                // Checks urls
868                Url::parse(&http_value).map_err(de::Error::custom)?;
869                Url::parse(&ws_value).map_err(de::Error::custom)?;
870
871                Ok(Cluster::Custom(http_value, ws_value))
872            } else {
873                Err(de::Error::custom("Invalid entry"))
874            }
875        }
876    }
877    deserializer.deserialize_any(StringOrCustomCluster(PhantomData))
878}
879
880impl fmt::Display for Config {
881    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
882        let programs = {
883            let c = ser_programs(&self.programs);
884            if c.is_empty() {
885                None
886            } else {
887                Some(c)
888            }
889        };
890        let cfg = _Config {
891            toolchain: Some(self.toolchain.clone()),
892            features: Some(self.features.clone()),
893            provider: Provider {
894                cluster: self.provider.cluster.clone(),
895                wallet: self.provider.wallet.stringify_with_tilde(),
896            },
897            test: self.test_validator.clone().map(Into::into),
898            scripts: match self.scripts.is_empty() {
899                true => None,
900                false => Some(self.scripts.clone()),
901            },
902            hooks: Some(self.hooks.clone()),
903            programs,
904            workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
905                .then(|| self.workspace.clone()),
906            surfpool: self.surfpool_config.clone().map(Into::into),
907            skip_local_validator: self.skip_local_validator,
908            clients: {
909                let clients = &self.clients;
910                let empty = !clients.auto
911                    && clients.js.is_none()
912                    && clients.js_umi.is_none()
913                    && clients.rust.is_none()
914                    && clients.go.is_none();
915                (!empty).then(|| clients.clone())
916            },
917        };
918
919        let cfg = toml::to_string(&cfg).expect("Must be well formed");
920        write!(f, "{cfg}")
921    }
922}
923
924impl TryFrom<_Config> for Config {
925    type Error = Error;
926
927    fn try_from(cfg: _Config) -> std::result::Result<Self, Self::Error> {
928        Ok(Config {
929            toolchain: cfg.toolchain.unwrap_or_default(),
930            features: cfg.features.unwrap_or_default(),
931            provider: ProviderConfig {
932                cluster: cfg.provider.cluster,
933                wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
934            },
935            scripts: cfg.scripts.unwrap_or_default(),
936            hooks: cfg.hooks.unwrap_or_default(),
937            validator: None, // Will be set based on CLI flags
938            test_validator: cfg.test.map(Into::into),
939            test_config: None,
940            programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
941            workspace: cfg.workspace.unwrap_or_default(),
942            surfpool_config: cfg.surfpool.map(Into::into),
943            skip_local_validator: cfg.skip_local_validator,
944            clients: cfg.clients.unwrap_or_default(),
945        })
946    }
947}
948
949impl FromStr for Config {
950    type Err = Error;
951
952    fn from_str(s: &str) -> Result<Self, Self::Err> {
953        toml::from_str::<_Config>(s)
954            .map_err(|e| anyhow!("Unable to deserialize config: {e}"))
955            .map(TryFrom::try_from)?
956    }
957}
958
959pub fn get_solana_cfg_url() -> Result<String, io::Error> {
960    let config_file = CONFIG_FILE.as_ref().ok_or_else(|| {
961        io::Error::new(
962            io::ErrorKind::NotFound,
963            "Default Solana config was not found",
964        )
965    })?;
966    SolanaConfig::load(config_file).map(|config| config.json_rpc_url)
967}
968
969fn ser_programs(
970    programs: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
971) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
972    programs
973        .iter()
974        .map(|(cluster, programs)| {
975            let cluster = cluster.to_string();
976            let programs = programs
977                .iter()
978                .map(|(name, deployment)| {
979                    (
980                        name.clone(),
981                        to_value(&_ProgramDeployment::from(deployment)),
982                    )
983                })
984                .collect::<BTreeMap<String, serde_json::Value>>();
985            (cluster, programs)
986        })
987        .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
988}
989
990fn to_value(dep: &_ProgramDeployment) -> serde_json::Value {
991    if dep.path.is_none() && dep.idl.is_none() {
992        return serde_json::Value::String(dep.address.to_string());
993    }
994    serde_json::to_value(dep).unwrap()
995}
996
997fn deser_programs(
998    programs: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
999) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
1000    programs
1001        .iter()
1002        .map(|(cluster, programs)| {
1003            let cluster: Cluster = cluster.parse()?;
1004            let programs = programs
1005                .iter()
1006                .map(|(name, program_id)| {
1007                    Ok((
1008                        name.clone(),
1009                        ProgramDeployment::try_from(match &program_id {
1010                            serde_json::Value::String(address) => _ProgramDeployment {
1011                                address: address.parse()?,
1012                                path: None,
1013                                idl: None,
1014                            },
1015
1016                            serde_json::Value::Object(_) => {
1017                                serde_json::from_value(program_id.clone())
1018                                    .map_err(|_| anyhow!("Unable to read toml"))?
1019                            }
1020                            _ => return Err(anyhow!("Invalid toml type")),
1021                        })?,
1022                    ))
1023                })
1024                .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
1025            Ok((cluster, programs))
1026        })
1027        .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
1028}
1029
1030#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1031pub struct TestValidator {
1032    pub genesis: Option<Vec<GenesisEntry>>,
1033    pub validator: Option<Validator>,
1034    pub startup_wait: u64,
1035    pub shutdown_wait: i32,
1036    pub upgradeable: bool,
1037}
1038
1039#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1040pub struct SurfpoolConfig {
1041    pub startup_wait: u64,
1042    pub shutdown_wait: i32,
1043    pub rpc_port: u16,
1044    pub ws_port: Option<u16>,
1045    pub host: String,
1046    pub online: Option<bool>,
1047    pub datasource_rpc_url: Option<String>,
1048    pub airdrop_addresses: Option<Vec<String>>,
1049    pub manifest_file_path: Option<String>,
1050    pub runbooks: Option<Vec<String>>,
1051    pub slot_time: Option<u16>,
1052    pub log_level: Option<String>,
1053    pub block_production_mode: Option<String>,
1054}
1055
1056#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1057pub struct _TestValidator {
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub genesis: Option<Vec<GenesisEntry>>,
1060    #[serde(skip_serializing_if = "Option::is_none")]
1061    pub validator: Option<_Validator>,
1062    #[serde(skip_serializing_if = "Option::is_none")]
1063    pub startup_wait: Option<u64>,
1064    #[serde(skip_serializing_if = "Option::is_none")]
1065    pub shutdown_wait: Option<i32>,
1066    #[serde(skip_serializing_if = "Option::is_none")]
1067    pub upgradeable: Option<bool>,
1068}
1069
1070#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1071pub struct _SurfpoolConfig {
1072    #[serde(skip_serializing_if = "Option::is_none")]
1073    pub startup_wait: Option<u64>,
1074    #[serde(skip_serializing_if = "Option::is_none")]
1075    pub shutdown_wait: Option<i32>,
1076    #[serde(skip_serializing_if = "Option::is_none")]
1077    pub rpc_port: Option<u16>,
1078    #[serde(skip_serializing_if = "Option::is_none")]
1079    pub ws_port: Option<u16>,
1080    #[serde(skip_serializing_if = "Option::is_none")]
1081    pub host: Option<String>,
1082    #[serde(skip_serializing_if = "Option::is_none")]
1083    pub online: Option<bool>,
1084    #[serde(skip_serializing_if = "Option::is_none")]
1085    pub datasource_rpc_url: Option<String>,
1086    #[serde(skip_serializing_if = "Option::is_none")]
1087    pub airdrop_addresses: Option<Vec<String>>,
1088    #[serde(skip_serializing_if = "Option::is_none")]
1089    pub manifest_file_path: Option<String>,
1090    #[serde(skip_serializing_if = "Option::is_none")]
1091    pub runbooks: Option<Vec<String>>,
1092    #[serde(skip_serializing_if = "Option::is_none")]
1093    pub slot_time: Option<u16>,
1094    #[serde(skip_serializing_if = "Option::is_none")]
1095    pub log_level: Option<String>,
1096    #[serde(skip_serializing_if = "Option::is_none")]
1097    pub block_production_mode: Option<String>,
1098}
1099
1100impl From<_SurfpoolConfig> for SurfpoolConfig {
1101    fn from(_surfpool_config: _SurfpoolConfig) -> Self {
1102        Self {
1103            startup_wait: _surfpool_config.startup_wait.unwrap_or(STARTUP_WAIT),
1104            shutdown_wait: _surfpool_config.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
1105            rpc_port: _surfpool_config.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1106            host: _surfpool_config.host.unwrap_or(SURFPOOL_HOST.to_string()),
1107            ws_port: _surfpool_config.ws_port,
1108            online: _surfpool_config.online,
1109            datasource_rpc_url: _surfpool_config.datasource_rpc_url,
1110            airdrop_addresses: _surfpool_config.airdrop_addresses,
1111            manifest_file_path: _surfpool_config.manifest_file_path,
1112            runbooks: _surfpool_config.runbooks,
1113            slot_time: _surfpool_config.slot_time,
1114            log_level: _surfpool_config.log_level,
1115            block_production_mode: _surfpool_config.block_production_mode,
1116        }
1117    }
1118}
1119
1120impl From<SurfpoolConfig> for _SurfpoolConfig {
1121    fn from(surfpool_config: SurfpoolConfig) -> Self {
1122        Self {
1123            startup_wait: Some(surfpool_config.startup_wait),
1124            shutdown_wait: Some(surfpool_config.shutdown_wait),
1125            rpc_port: Some(surfpool_config.rpc_port),
1126            ws_port: surfpool_config.ws_port,
1127            host: Some(surfpool_config.host),
1128            online: surfpool_config.online,
1129            datasource_rpc_url: surfpool_config.datasource_rpc_url,
1130            airdrop_addresses: surfpool_config.airdrop_addresses,
1131            manifest_file_path: surfpool_config.manifest_file_path,
1132            runbooks: surfpool_config.runbooks,
1133            slot_time: surfpool_config.slot_time,
1134            log_level: surfpool_config.log_level,
1135            block_production_mode: surfpool_config.block_production_mode,
1136        }
1137    }
1138}
1139pub const STARTUP_WAIT: u64 = 30000;
1140pub const SHUTDOWN_WAIT: i32 = 2000;
1141
1142impl From<_TestValidator> for TestValidator {
1143    fn from(_test_validator: _TestValidator) -> Self {
1144        Self {
1145            shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
1146            startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
1147            genesis: _test_validator.genesis,
1148            validator: _test_validator.validator.map(Into::into),
1149            upgradeable: _test_validator.upgradeable.unwrap_or(false),
1150        }
1151    }
1152}
1153
1154impl From<TestValidator> for _TestValidator {
1155    fn from(test_validator: TestValidator) -> Self {
1156        Self {
1157            shutdown_wait: Some(test_validator.shutdown_wait),
1158            startup_wait: Some(test_validator.startup_wait),
1159            genesis: test_validator.genesis,
1160            validator: test_validator.validator.map(Into::into),
1161            upgradeable: Some(test_validator.upgradeable),
1162        }
1163    }
1164}
1165
1166#[derive(Debug, Clone)]
1167pub struct TestConfig {
1168    pub test_suite_configs: HashMap<PathBuf, TestToml>,
1169}
1170
1171impl Deref for TestConfig {
1172    type Target = HashMap<PathBuf, TestToml>;
1173
1174    fn deref(&self) -> &Self::Target {
1175        &self.test_suite_configs
1176    }
1177}
1178
1179impl TestConfig {
1180    pub fn discover(root: impl AsRef<Path>, test_paths: Vec<PathBuf>) -> Result<Option<Self>> {
1181        let walker = WalkDir::new(root).into_iter();
1182        let mut test_suite_configs = HashMap::new();
1183        for entry in walker.filter_entry(|e| !is_hidden(e)) {
1184            let entry = entry?;
1185            if entry.file_name() == "Test.toml" {
1186                let entry_path = entry.path();
1187                let test_toml = TestToml::from_path(entry_path)?;
1188                if test_paths.is_empty() || test_paths.iter().any(|p| entry_path.starts_with(p)) {
1189                    test_suite_configs.insert(entry.path().into(), test_toml);
1190                }
1191            }
1192        }
1193
1194        Ok(match test_suite_configs.is_empty() {
1195            true => None,
1196            false => Some(Self { test_suite_configs }),
1197        })
1198    }
1199}
1200
1201// This file needs to have the same (sub)structure as Anchor.toml
1202// so it can be parsed as a base test file from an Anchor.toml
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204pub struct _TestToml {
1205    pub extends: Option<Vec<String>>,
1206    pub test: Option<_TestValidator>,
1207    pub scripts: Option<ScriptsConfig>,
1208}
1209
1210impl _TestToml {
1211    fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
1212        let s = fs::read_to_string(&path)?;
1213        let parsed_toml: Self = toml::from_str(&s)?;
1214        let mut current_toml = _TestToml {
1215            extends: None,
1216            test: None,
1217            scripts: None,
1218        };
1219        if let Some(bases) = &parsed_toml.extends {
1220            for base in bases {
1221                let mut canonical_base = base.clone();
1222                canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
1223                current_toml.merge(_TestToml::from_path(&canonical_base)?);
1224            }
1225        }
1226        current_toml.merge(parsed_toml);
1227
1228        if let Some(test) = &mut current_toml.test {
1229            if let Some(genesis_programs) = &mut test.genesis {
1230                for entry in genesis_programs {
1231                    entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
1232                }
1233            }
1234            if let Some(validator) = &mut test.validator {
1235                if let Some(accounts) = &mut validator.account {
1236                    for entry in accounts {
1237                        entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
1238                    }
1239                }
1240                if let Some(account_dirs) = &mut validator.account_dir {
1241                    for entry in account_dirs {
1242                        entry.directory =
1243                            canonicalize_filepath_from_origin(&entry.directory, &path)?;
1244                    }
1245                }
1246            }
1247        }
1248        Ok(current_toml)
1249    }
1250}
1251
1252/// canonicalizes the `file_path` arg.
1253/// uses the `path` arg as the current dir
1254/// from which to turn the relative path
1255/// into a canonical one
1256fn canonicalize_filepath_from_origin(
1257    file_path: impl AsRef<Path>,
1258    origin: impl AsRef<Path>,
1259) -> Result<String> {
1260    let previous_dir = std::env::current_dir()?;
1261    std::env::set_current_dir(origin.as_ref().parent().unwrap())?;
1262    let result = fs::canonicalize(&file_path)
1263        .with_context(|| {
1264            format!(
1265                "Error reading (possibly relative) path: {}. If relative, this is the path that \
1266                 was used as the current path: {}",
1267                file_path.as_ref().display(),
1268                origin.as_ref().display()
1269            )
1270        })?
1271        .display()
1272        .to_string();
1273    std::env::set_current_dir(previous_dir)?;
1274    Ok(result)
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize)]
1278pub struct TestToml {
1279    #[serde(skip_serializing_if = "Option::is_none")]
1280    pub test: Option<TestValidator>,
1281    pub scripts: ScriptsConfig,
1282}
1283
1284impl TestToml {
1285    pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
1286        WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
1287    }
1288}
1289
1290impl Merge for _TestToml {
1291    fn merge(&mut self, other: Self) {
1292        let mut my_scripts = self.scripts.take();
1293        match &mut my_scripts {
1294            None => my_scripts = other.scripts,
1295            Some(my_scripts) => {
1296                if let Some(other_scripts) = other.scripts {
1297                    for (name, script) in other_scripts {
1298                        my_scripts.insert(name, script);
1299                    }
1300                }
1301            }
1302        }
1303
1304        let mut my_test = self.test.take();
1305        match &mut my_test {
1306            Some(my_test) => {
1307                if let Some(other_test) = other.test {
1308                    if let Some(startup_wait) = other_test.startup_wait {
1309                        my_test.startup_wait = Some(startup_wait);
1310                    }
1311                    if let Some(other_genesis) = other_test.genesis {
1312                        match &mut my_test.genesis {
1313                            Some(my_genesis) => {
1314                                for other_entry in other_genesis {
1315                                    match my_genesis
1316                                        .iter()
1317                                        .position(|g| *g.address == other_entry.address)
1318                                    {
1319                                        None => my_genesis.push(other_entry),
1320                                        Some(i) => my_genesis[i] = other_entry,
1321                                    }
1322                                }
1323                            }
1324                            None => my_test.genesis = Some(other_genesis),
1325                        }
1326                    }
1327                    let mut my_validator = my_test.validator.take();
1328                    match &mut my_validator {
1329                        None => my_validator = other_test.validator,
1330                        Some(my_validator) => {
1331                            if let Some(other_validator) = other_test.validator {
1332                                my_validator.merge(other_validator)
1333                            }
1334                        }
1335                    }
1336
1337                    my_test.validator = my_validator;
1338                }
1339            }
1340            None => my_test = other.test,
1341        };
1342
1343        // Instantiating a new Self object here ensures that
1344        // this function will fail to compile if new fields get added
1345        // to Self. This is useful as a reminder if they also require merging
1346        *self = Self {
1347            test: my_test,
1348            scripts: my_scripts,
1349            extends: self.extends.take(),
1350        };
1351    }
1352}
1353
1354impl TryFrom<WithPath<_TestToml>> for TestToml {
1355    type Error = Error;
1356
1357    fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
1358        Ok(Self {
1359            test: value.test.take().map(Into::into),
1360            scripts: value
1361                .scripts
1362                .take()
1363                .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
1364        })
1365    }
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize)]
1369pub struct GenesisEntry {
1370    // Base58 pubkey string.
1371    pub address: String,
1372    // Filepath to the compiled program to embed into the genesis.
1373    pub program: String,
1374    // Whether the genesis program is upgradeable.
1375    pub upgradeable: Option<bool>,
1376}
1377
1378#[derive(Debug, Clone, Serialize, Deserialize)]
1379pub struct CloneEntry {
1380    // Base58 pubkey string.
1381    pub address: String,
1382}
1383
1384#[derive(Debug, Clone, Serialize, Deserialize)]
1385pub struct AccountEntry {
1386    // Base58 pubkey string.
1387    pub address: String,
1388    // Name of JSON file containing the account data.
1389    pub filename: String,
1390}
1391
1392#[derive(Debug, Clone, Serialize, Deserialize)]
1393pub struct AccountDirEntry {
1394    // Directory containing account JSON files
1395    pub directory: String,
1396}
1397
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1399pub struct FundedAccount {
1400    // Base58 pubkey string of the account to fund, or "new" to generate a random keypair
1401    pub address: String,
1402    // Amount of lamports to fund the account with (default: 1 SOL = 1_000_000_000 lamports)
1403    #[serde(skip_serializing_if = "Option::is_none")]
1404    pub lamports: Option<u64>,
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize)]
1408pub struct TokenMint {
1409    // Base58 pubkey string of the mint account, or "new" to generate a random keypair
1410    pub address: String,
1411    // Number of base 10 digits to the right of the decimal place (required)
1412    pub decimals: u8,
1413    // Initial supply of tokens (default: 0)
1414    #[serde(skip_serializing_if = "Option::is_none")]
1415    pub supply: Option<u64>,
1416    // Optional mint authority (default: None = fixed supply)
1417    #[serde(skip_serializing_if = "Option::is_none")]
1418    pub mint_authority: Option<String>,
1419    // Optional freeze authority (default: None = no freeze authority)
1420    #[serde(skip_serializing_if = "Option::is_none")]
1421    pub freeze_authority: Option<String>,
1422}
1423
1424#[derive(Debug, Clone, Serialize, Deserialize)]
1425pub struct TokenAccount {
1426    // Reference to mint (pubkey string or "new" to use the most recently created mint)
1427    pub mint: String,
1428    // Owner of the token account ("new" to generate random keypair, or specific pubkey)
1429    pub owner: String,
1430    // Amount of tokens to fund the account with
1431    pub amount: u64,
1432    // Optional: specific token account address (default: generate new)
1433    #[serde(skip_serializing_if = "Option::is_none")]
1434    pub address: Option<String>,
1435}
1436
1437#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1438pub struct _Validator {
1439    // Load an account from the provided JSON file
1440    #[serde(skip_serializing_if = "Option::is_none")]
1441    pub account: Option<Vec<AccountEntry>>,
1442    // Load all the accounts from the JSON files found in the specified DIRECTORY
1443    #[serde(skip_serializing_if = "Option::is_none")]
1444    pub account_dir: Option<Vec<AccountDirEntry>>,
1445    // Generate and fund accounts with lamports
1446    #[serde(skip_serializing_if = "Option::is_none")]
1447    pub fund_accounts: Option<Vec<FundedAccount>>,
1448    // Create SPL token mints
1449    #[serde(skip_serializing_if = "Option::is_none")]
1450    pub mints: Option<Vec<TokenMint>>,
1451    // Create and fund SPL token accounts
1452    #[serde(skip_serializing_if = "Option::is_none")]
1453    pub token_accounts: Option<Vec<TokenAccount>>,
1454    // IP address to bind the validator ports. [default: 127.0.0.1]
1455    #[serde(skip_serializing_if = "Option::is_none")]
1456    pub bind_address: Option<String>,
1457    // Copy an account from the cluster referenced by the url argument.
1458    #[serde(skip_serializing_if = "Option::is_none")]
1459    pub clone: Option<Vec<CloneEntry>>,
1460    // Range to use for dynamically assigned ports. [default: 1024-65535]
1461    #[serde(skip_serializing_if = "Option::is_none")]
1462    pub dynamic_port_range: Option<String>,
1463    // Enable the faucet on this port [default: 9900].
1464    #[serde(skip_serializing_if = "Option::is_none")]
1465    pub faucet_port: Option<u16>,
1466    // Give the faucet address this much SOL in genesis. [default: 1000000]
1467    #[serde(skip_serializing_if = "Option::is_none")]
1468    pub faucet_sol: Option<String>,
1469    // Geyser plugin config location
1470    #[serde(skip_serializing_if = "Option::is_none")]
1471    pub geyser_plugin_config: Option<String>,
1472    // Gossip DNS name or IP address for the validator to advertise in gossip. [default: 127.0.0.1]
1473    #[serde(skip_serializing_if = "Option::is_none")]
1474    pub gossip_host: Option<String>,
1475    // Gossip port number for the validator
1476    #[serde(skip_serializing_if = "Option::is_none")]
1477    pub gossip_port: Option<u16>,
1478    // URL for Solana's JSON RPC or moniker.
1479    #[serde(skip_serializing_if = "Option::is_none")]
1480    pub url: Option<String>,
1481    // Use DIR as ledger location
1482    #[serde(skip_serializing_if = "Option::is_none")]
1483    pub ledger: Option<String>,
1484    // Keep this amount of shreds in root slots. [default: 10000]
1485    #[serde(skip_serializing_if = "Option::is_none")]
1486    pub limit_ledger_size: Option<String>,
1487    // Enable JSON RPC on this port, and the next port for the RPC websocket. [default: 8899]
1488    #[serde(skip_serializing_if = "Option::is_none")]
1489    pub rpc_port: Option<u16>,
1490    // Override the number of slots in an epoch.
1491    #[serde(skip_serializing_if = "Option::is_none")]
1492    pub slots_per_epoch: Option<String>,
1493    // The number of ticks in a slot
1494    #[serde(skip_serializing_if = "Option::is_none")]
1495    pub ticks_per_slot: Option<u16>,
1496    // Warp the ledger to WARP_SLOT after starting the validator.
1497    #[serde(skip_serializing_if = "Option::is_none")]
1498    pub warp_slot: Option<Slot>,
1499    // Deactivate one or more features.
1500    #[serde(skip_serializing_if = "Option::is_none")]
1501    pub deactivate_feature: Option<Vec<String>>,
1502    // Extra arguments to pass through to solana-test-validator.
1503    #[serde(skip_serializing_if = "Option::is_none")]
1504    pub extra_args: Option<Vec<String>>,
1505}
1506
1507#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1508pub struct Validator {
1509    #[serde(skip_serializing_if = "Option::is_none")]
1510    pub account: Option<Vec<AccountEntry>>,
1511    #[serde(skip_serializing_if = "Option::is_none")]
1512    pub account_dir: Option<Vec<AccountDirEntry>>,
1513    #[serde(skip_serializing_if = "Option::is_none")]
1514    pub fund_accounts: Option<Vec<FundedAccount>>,
1515    #[serde(skip_serializing_if = "Option::is_none")]
1516    pub mints: Option<Vec<TokenMint>>,
1517    #[serde(skip_serializing_if = "Option::is_none")]
1518    pub token_accounts: Option<Vec<TokenAccount>>,
1519    pub bind_address: String,
1520    #[serde(skip_serializing_if = "Option::is_none")]
1521    pub clone: Option<Vec<CloneEntry>>,
1522    #[serde(skip_serializing_if = "Option::is_none")]
1523    pub dynamic_port_range: Option<String>,
1524    #[serde(skip_serializing_if = "Option::is_none")]
1525    pub faucet_port: Option<u16>,
1526    #[serde(skip_serializing_if = "Option::is_none")]
1527    pub faucet_sol: Option<String>,
1528    #[serde(skip_serializing_if = "Option::is_none")]
1529    pub geyser_plugin_config: Option<String>,
1530    #[serde(skip_serializing_if = "Option::is_none")]
1531    pub gossip_host: Option<String>,
1532    #[serde(skip_serializing_if = "Option::is_none")]
1533    pub gossip_port: Option<u16>,
1534    #[serde(skip_serializing_if = "Option::is_none")]
1535    pub url: Option<String>,
1536    pub ledger: String,
1537    #[serde(skip_serializing_if = "Option::is_none")]
1538    pub limit_ledger_size: Option<String>,
1539    pub rpc_port: u16,
1540    #[serde(skip_serializing_if = "Option::is_none")]
1541    pub slots_per_epoch: Option<String>,
1542    #[serde(skip_serializing_if = "Option::is_none")]
1543    pub ticks_per_slot: Option<u16>,
1544    #[serde(skip_serializing_if = "Option::is_none")]
1545    pub warp_slot: Option<Slot>,
1546    #[serde(skip_serializing_if = "Option::is_none")]
1547    pub deactivate_feature: Option<Vec<String>>,
1548    #[serde(skip_serializing_if = "Option::is_none")]
1549    pub extra_args: Option<Vec<String>>,
1550}
1551
1552impl From<_Validator> for Validator {
1553    fn from(_validator: _Validator) -> Self {
1554        Self {
1555            account: _validator.account,
1556            account_dir: _validator.account_dir,
1557            fund_accounts: _validator.fund_accounts,
1558            mints: _validator.mints,
1559            token_accounts: _validator.token_accounts,
1560            bind_address: _validator
1561                .bind_address
1562                .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
1563            clone: _validator.clone,
1564            dynamic_port_range: _validator.dynamic_port_range,
1565            faucet_port: _validator.faucet_port,
1566            faucet_sol: _validator.faucet_sol,
1567            geyser_plugin_config: _validator.geyser_plugin_config,
1568            gossip_host: _validator.gossip_host,
1569            gossip_port: _validator.gossip_port,
1570            url: _validator.url,
1571            ledger: _validator
1572                .ledger
1573                .unwrap_or_else(|| get_default_ledger_path().display().to_string()),
1574            limit_ledger_size: _validator.limit_ledger_size,
1575            rpc_port: _validator.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1576            slots_per_epoch: _validator.slots_per_epoch,
1577            ticks_per_slot: _validator.ticks_per_slot,
1578            warp_slot: _validator.warp_slot,
1579            deactivate_feature: _validator.deactivate_feature,
1580            extra_args: _validator.extra_args,
1581        }
1582    }
1583}
1584
1585impl From<Validator> for _Validator {
1586    fn from(validator: Validator) -> Self {
1587        Self {
1588            account: validator.account,
1589            account_dir: validator.account_dir,
1590            fund_accounts: validator.fund_accounts,
1591            mints: validator.mints,
1592            token_accounts: validator.token_accounts,
1593            bind_address: Some(validator.bind_address),
1594            clone: validator.clone,
1595            dynamic_port_range: validator.dynamic_port_range,
1596            faucet_port: validator.faucet_port,
1597            faucet_sol: validator.faucet_sol,
1598            geyser_plugin_config: validator.geyser_plugin_config,
1599            gossip_host: validator.gossip_host,
1600            gossip_port: validator.gossip_port,
1601            url: validator.url,
1602            ledger: Some(validator.ledger),
1603            limit_ledger_size: validator.limit_ledger_size,
1604            rpc_port: Some(validator.rpc_port),
1605            slots_per_epoch: validator.slots_per_epoch,
1606            ticks_per_slot: validator.ticks_per_slot,
1607            warp_slot: validator.warp_slot,
1608            deactivate_feature: validator.deactivate_feature,
1609            extra_args: validator.extra_args,
1610        }
1611    }
1612}
1613
1614pub fn get_default_ledger_path() -> PathBuf {
1615    Path::new(".anchor").join("test-ledger")
1616}
1617
1618const DEFAULT_BIND_ADDRESS: &str = "127.0.0.1";
1619
1620fn is_generated_address(address: &str) -> bool {
1621    address.eq_ignore_ascii_case("new")
1622}
1623
1624fn explicit_token_account_address(address: Option<&str>) -> Option<&str> {
1625    match address {
1626        Some(address) if !is_generated_address(address) => Some(address),
1627        _ => None,
1628    }
1629}
1630
1631impl Merge for _Validator {
1632    fn merge(&mut self, other: Self) {
1633        // Instantiating a new Self object here ensures that
1634        // this function will fail to compile if new fields get added
1635        // to Self. This is useful as a reminder if they also require merging
1636        *self = Self {
1637            account: match self.account.take() {
1638                None => other.account,
1639                Some(mut entries) => match other.account {
1640                    None => Some(entries),
1641                    Some(other_entries) => {
1642                        for other_entry in other_entries {
1643                            match entries
1644                                .iter()
1645                                .position(|my_entry| *my_entry.address == other_entry.address)
1646                            {
1647                                None => entries.push(other_entry),
1648                                Some(i) => entries[i] = other_entry,
1649                            };
1650                        }
1651                        Some(entries)
1652                    }
1653                },
1654            },
1655            account_dir: match self.account_dir.take() {
1656                None => other.account_dir,
1657                Some(mut entries) => match other.account_dir {
1658                    None => Some(entries),
1659                    Some(other_entries) => {
1660                        for other_entry in other_entries {
1661                            match entries
1662                                .iter()
1663                                .position(|my_entry| *my_entry.directory == other_entry.directory)
1664                            {
1665                                None => entries.push(other_entry),
1666                                Some(i) => entries[i] = other_entry,
1667                            };
1668                        }
1669                        Some(entries)
1670                    }
1671                },
1672            },
1673            fund_accounts: match self.fund_accounts.take() {
1674                None => other.fund_accounts,
1675                Some(mut entries) => match other.fund_accounts {
1676                    None => Some(entries),
1677                    Some(other_entries) => {
1678                        for other_entry in other_entries {
1679                            match entries.iter().position(|my_entry| {
1680                                !is_generated_address(&my_entry.address)
1681                                    && !is_generated_address(&other_entry.address)
1682                                    && *my_entry.address == other_entry.address
1683                            }) {
1684                                None => entries.push(other_entry),
1685                                Some(i) => entries[i] = other_entry,
1686                            };
1687                        }
1688                        Some(entries)
1689                    }
1690                },
1691            },
1692            mints: match self.mints.take() {
1693                None => other.mints,
1694                Some(mut entries) => match other.mints {
1695                    None => Some(entries),
1696                    Some(other_entries) => {
1697                        for other_entry in other_entries {
1698                            match entries.iter().position(|my_entry| {
1699                                !is_generated_address(&my_entry.address)
1700                                    && !is_generated_address(&other_entry.address)
1701                                    && *my_entry.address == other_entry.address
1702                            }) {
1703                                None => entries.push(other_entry),
1704                                Some(i) => entries[i] = other_entry,
1705                            };
1706                        }
1707                        Some(entries)
1708                    }
1709                },
1710            },
1711            token_accounts: match self.token_accounts.take() {
1712                None => other.token_accounts,
1713                Some(mut entries) => match other.token_accounts {
1714                    None => Some(entries),
1715                    Some(other_entries) => {
1716                        // Generated token accounts do not have a stable merge key.
1717                        // Only explicitly addressed accounts override inherited entries.
1718                        for other_entry in other_entries {
1719                            match entries.iter().position(|my_entry| {
1720                                explicit_token_account_address(my_entry.address.as_deref())
1721                                    .zip(explicit_token_account_address(
1722                                        other_entry.address.as_deref(),
1723                                    ))
1724                                    .is_some_and(|(my_address, other_address)| {
1725                                        my_address == other_address
1726                                    })
1727                            }) {
1728                                None => entries.push(other_entry),
1729                                Some(i) => entries[i] = other_entry,
1730                            };
1731                        }
1732                        Some(entries)
1733                    }
1734                },
1735            },
1736            bind_address: other.bind_address.or_else(|| self.bind_address.take()),
1737            clone: match self.clone.take() {
1738                None => other.clone,
1739                Some(mut entries) => match other.clone {
1740                    None => Some(entries),
1741                    Some(other_entries) => {
1742                        for other_entry in other_entries {
1743                            match entries
1744                                .iter()
1745                                .position(|my_entry| *my_entry.address == other_entry.address)
1746                            {
1747                                None => entries.push(other_entry),
1748                                Some(i) => entries[i] = other_entry,
1749                            };
1750                        }
1751                        Some(entries)
1752                    }
1753                },
1754            },
1755            dynamic_port_range: other
1756                .dynamic_port_range
1757                .or_else(|| self.dynamic_port_range.take()),
1758            faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
1759            faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
1760            geyser_plugin_config: other
1761                .geyser_plugin_config
1762                .or_else(|| self.geyser_plugin_config.take()),
1763            gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
1764            gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
1765            url: other.url.or_else(|| self.url.take()),
1766            ledger: other.ledger.or_else(|| self.ledger.take()),
1767            limit_ledger_size: other
1768                .limit_ledger_size
1769                .or_else(|| self.limit_ledger_size.take()),
1770            rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
1771            slots_per_epoch: other
1772                .slots_per_epoch
1773                .or_else(|| self.slots_per_epoch.take()),
1774            ticks_per_slot: other.ticks_per_slot.or_else(|| self.ticks_per_slot.take()),
1775            warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
1776            deactivate_feature: other
1777                .deactivate_feature
1778                .or_else(|| self.deactivate_feature.take()),
1779            extra_args: match self.extra_args.take() {
1780                None => other.extra_args,
1781                Some(mut args) => {
1782                    if let Some(other_args) = other.extra_args {
1783                        args.extend(other_args);
1784                    }
1785                    Some(args)
1786                }
1787            },
1788        };
1789    }
1790}
1791
1792#[derive(Debug, Clone)]
1793pub struct Program {
1794    pub lib_name: String,
1795    // Canonicalized path to the program directory
1796    pub path: PathBuf,
1797    pub idl: Option<Idl>,
1798}
1799
1800impl Program {
1801    pub fn pubkey(&self) -> Result<Pubkey> {
1802        self.keypair().map(|kp| kp.pubkey())
1803    }
1804
1805    pub fn keypair(&self) -> Result<Keypair> {
1806        let file = self.keypair_file()?;
1807        get_keypair(file.path())
1808    }
1809
1810    // Lazily initializes the keypair file with a new key if it doesn't exist.
1811    pub fn keypair_file(&self) -> Result<WithPath<File>> {
1812        let deploy_dir_path = target_dir()?.join("deploy");
1813        fs::create_dir_all(&deploy_dir_path)
1814            .with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
1815        let path = std::env::current_dir()
1816            .expect("Must have current dir")
1817            .join(deploy_dir_path.join(format!("{}-keypair.json", self.lib_name)));
1818        if path.exists() {
1819            return Ok(WithPath::new(
1820                File::open(&path)
1821                    .with_context(|| format!("Error opening file with path: {}", path.display()))?,
1822                path,
1823            ));
1824        }
1825        let program_kp = Keypair::new();
1826        let mut file = File::create(&path)
1827            .with_context(|| format!("Error creating file with path: {}", path.display()))?;
1828        file.write_all(format!("{:?}", program_kp.to_bytes()).as_bytes())?;
1829        Ok(WithPath::new(file, path))
1830    }
1831
1832    pub fn binary_path(&self, verifiable: bool) -> Result<PathBuf> {
1833        let path = target_dir()?
1834            .join(if verifiable { "verifiable" } else { "deploy" })
1835            .join(&self.lib_name)
1836            .with_extension("so");
1837
1838        Ok(std::env::current_dir()
1839            .expect("Must have current dir")
1840            .join(path))
1841    }
1842}
1843
1844#[derive(Debug, Default)]
1845pub struct ProgramDeployment {
1846    pub address: Pubkey,
1847    pub path: Option<String>,
1848    pub idl: Option<String>,
1849}
1850
1851impl TryFrom<_ProgramDeployment> for ProgramDeployment {
1852    type Error = anyhow::Error;
1853    fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
1854        Ok(ProgramDeployment {
1855            address: pd.address.parse()?,
1856            path: pd.path,
1857            idl: pd.idl,
1858        })
1859    }
1860}
1861
1862#[derive(Debug, Default, Serialize, Deserialize)]
1863pub struct _ProgramDeployment {
1864    pub address: String,
1865    pub path: Option<String>,
1866    pub idl: Option<String>,
1867}
1868
1869impl From<&ProgramDeployment> for _ProgramDeployment {
1870    fn from(pd: &ProgramDeployment) -> Self {
1871        Self {
1872            address: pd.address.to_string(),
1873            path: pd.path.clone(),
1874            idl: pd.idl.clone(),
1875        }
1876    }
1877}
1878
1879pub struct ProgramWorkspace {
1880    pub name: String,
1881    pub program_id: Pubkey,
1882    pub idl: Idl,
1883}
1884
1885#[derive(Debug, Serialize, Deserialize)]
1886pub struct AnchorPackage {
1887    pub name: String,
1888    pub address: String,
1889    pub idl: Option<String>,
1890}
1891
1892impl AnchorPackage {
1893    pub fn from(name: String, cfg: &WithPath<Config>) -> Result<Self> {
1894        let cluster = &cfg.provider.cluster;
1895        if cluster != &Cluster::Mainnet {
1896            return Err(anyhow!("Publishing requires the mainnet cluster"));
1897        }
1898        let program_details = cfg
1899            .programs
1900            .get(cluster)
1901            .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
1902            .get(&name)
1903            .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
1904        let idl = program_details.idl.clone();
1905        let address = program_details.address.to_string();
1906        Ok(Self { name, address, idl })
1907    }
1908}
1909
1910#[derive(Debug, Serialize, Deserialize)]
1911#[serde(rename_all = "camelCase")]
1912pub struct SurfnetInfoResponse {
1913    pub runbook_executions: Vec<RunbookExecution>,
1914}
1915#[derive(Debug, Serialize, Deserialize)]
1916#[serde(rename_all = "camelCase")]
1917pub struct RunbookExecution {
1918    #[serde(rename = "startedAt")]
1919    pub started_at: u32,
1920    #[serde(rename = "completedAt")]
1921    pub completed_at: Option<u32>,
1922    #[serde(rename = "runbookId")]
1923    pub runbook_id: String,
1924    pub errors: Option<Vec<String>>,
1925}
1926
1927#[macro_export]
1928macro_rules! home_path {
1929    ($my_struct:ident, $path:literal) => {
1930        #[derive(Clone, Debug, AbsolutePath)]
1931        pub struct $my_struct(::std::path::PathBuf);
1932
1933        impl Default for $my_struct {
1934            fn default() -> Self {
1935                $my_struct(home_dir().unwrap().join($path))
1936            }
1937        }
1938
1939        impl $my_struct {
1940            fn stringify_with_tilde(&self) -> String {
1941                self.0
1942                    .display()
1943                    .to_string()
1944                    .replacen(home_dir().unwrap().to_str().unwrap(), "~", 1)
1945            }
1946        }
1947
1948        impl FromStr for $my_struct {
1949            type Err = anyhow::Error;
1950
1951            fn from_str(s: &str) -> Result<Self, Self::Err> {
1952                Ok(Self(::std::path::PathBuf::from(s)))
1953            }
1954        }
1955
1956        impl fmt::Display for $my_struct {
1957            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1958                write!(f, "{}", self.0.display())
1959            }
1960        }
1961    };
1962}
1963
1964home_path!(WalletPath, ".config/solana/id.json");
1965
1966#[cfg(test)]
1967mod tests {
1968    use super::*;
1969
1970    const BASE_CONFIG: &str = "
1971        [provider]
1972        cluster = \"localnet\"
1973        wallet = \"id.json\"
1974    ";
1975
1976    #[test]
1977    fn parse_custom_cluster_str() {
1978        let config = Config::from_str(
1979            "
1980        [provider]
1981        cluster = \"http://my-url.com\"
1982        wallet = \"id.json\"
1983    ",
1984        )
1985        .unwrap();
1986        assert!(!config.features.skip_lint);
1987
1988        // Make sure the layout of `provider.cluster` stays the same after serialization
1989        assert!(config
1990            .to_string()
1991            .contains(r#"cluster = "http://my-url.com""#));
1992    }
1993
1994    #[test]
1995    fn parse_custom_cluster_map() {
1996        let config = Config::from_str(
1997            "
1998        [provider]
1999        cluster = { http = \"http://my-url.com\", ws = \"ws://my-url.com\" }
2000        wallet = \"id.json\"
2001    ",
2002        )
2003        .unwrap();
2004        assert!(!config.features.skip_lint);
2005    }
2006
2007    #[test]
2008    fn parse_skip_lint_no_section() {
2009        let config = Config::from_str(BASE_CONFIG).unwrap();
2010        assert!(!config.features.skip_lint);
2011    }
2012
2013    #[test]
2014    fn parse_fund_accounts_config() {
2015        let config_str = r#"
2016        [provider]
2017        cluster = "localnet"
2018        wallet = "id.json"
2019
2020        [test.validator]
2021        [[test.validator.fund_accounts]]
2022        address = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
2023        lamports = 2000000000
2024
2025        [[test.validator.fund_accounts]]
2026        address = "GjJyeC1rB1hL8ZkLqKqJzJzJzJzJzJzJzJzJzJzJzJzJz"
2027        "#;
2028
2029        let config = Config::from_str(config_str).unwrap();
2030        assert!(config.test_validator.is_some());
2031        let test_validator = config.test_validator.as_ref().unwrap();
2032        assert!(test_validator.validator.is_some());
2033        let validator = test_validator.validator.as_ref().unwrap();
2034        assert!(validator.fund_accounts.is_some());
2035
2036        let fund_accounts = validator.fund_accounts.as_ref().unwrap();
2037        assert_eq!(fund_accounts.len(), 2);
2038        assert_eq!(
2039            fund_accounts[0].address,
2040            "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
2041        );
2042        assert_eq!(fund_accounts[0].lamports, Some(2000000000));
2043        assert_eq!(
2044            fund_accounts[1].address,
2045            "GjJyeC1rB1hL8ZkLqKqJzJzJzJzJzJzJzJzJzJzJzJzJz"
2046        );
2047        assert_eq!(fund_accounts[1].lamports, None); // Should default to 1 SOL
2048    }
2049
2050    #[test]
2051    fn parse_fund_accounts_without_lamports() {
2052        let config_str = r#"
2053        [provider]
2054        cluster = "localnet"
2055        wallet = "id.json"
2056
2057        [test.validator]
2058        [[test.validator.fund_accounts]]
2059        address = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
2060        "#;
2061
2062        let config = Config::from_str(config_str).unwrap();
2063        let fund_accounts = config
2064            .test_validator
2065            .as_ref()
2066            .unwrap()
2067            .validator
2068            .as_ref()
2069            .unwrap()
2070            .fund_accounts
2071            .as_ref()
2072            .unwrap();
2073        assert_eq!(fund_accounts.len(), 1);
2074        assert_eq!(fund_accounts[0].lamports, None);
2075    }
2076
2077    #[test]
2078    fn test_toml_extends_preserves_generated_validator_accounts() {
2079        let dir = tempfile::tempdir().unwrap();
2080        let suite_dir = dir.path().join("tests").join("suite");
2081        fs::create_dir_all(&suite_dir).unwrap();
2082
2083        let base_toml = suite_dir.join("Base.toml");
2084        fs::write(
2085            &base_toml,
2086            r#"
2087[scripts]
2088test = "true"
2089
2090[[test.validator.fund_accounts]]
2091address = "new"
2092lamports = 1
2093
2094[[test.validator.mints]]
2095address = "new"
2096decimals = 6
2097
2098[[test.validator.token_accounts]]
2099mint = "new"
2100owner = "new"
2101amount = 1
2102"#,
2103        )
2104        .unwrap();
2105
2106        let test_toml = suite_dir.join("Test.toml");
2107        fs::write(
2108            &test_toml,
2109            r#"
2110extends = ["Base.toml"]
2111
2112[scripts]
2113test = "true"
2114
2115[[test.validator.fund_accounts]]
2116address = "new"
2117lamports = 2
2118
2119[[test.validator.mints]]
2120address = "new"
2121decimals = 9
2122
2123[[test.validator.token_accounts]]
2124mint = "new"
2125owner = "new"
2126amount = 2
2127"#,
2128        )
2129        .unwrap();
2130
2131        let parsed = TestToml::from_path(test_toml).unwrap();
2132        let validator = parsed.test.unwrap().validator.unwrap();
2133
2134        let fund_accounts = validator.fund_accounts.unwrap();
2135        assert_eq!(fund_accounts.len(), 2);
2136        assert_eq!(fund_accounts[0].lamports, Some(1));
2137        assert_eq!(fund_accounts[1].lamports, Some(2));
2138
2139        let mints = validator.mints.unwrap();
2140        assert_eq!(mints.len(), 2);
2141        assert_eq!(mints[0].decimals, 6);
2142        assert_eq!(mints[1].decimals, 9);
2143
2144        let token_accounts = validator.token_accounts.unwrap();
2145        assert_eq!(token_accounts.len(), 2);
2146        assert_eq!(token_accounts[0].amount, 1);
2147        assert_eq!(token_accounts[1].amount, 2);
2148    }
2149
2150    #[test]
2151    fn parse_skip_lint_no_value() {
2152        let string = BASE_CONFIG.to_owned() + "[features]";
2153        let config = Config::from_str(&string).unwrap();
2154        assert!(!config.features.skip_lint);
2155    }
2156
2157    #[test]
2158    fn parse_skip_lint_true() {
2159        let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true";
2160        let config = Config::from_str(&string).unwrap();
2161        assert!(config.features.skip_lint);
2162    }
2163
2164    #[test]
2165    fn parse_clients_section() {
2166        let toml = BASE_CONFIG.to_owned()
2167            + r#"
2168[clients]
2169auto = true
2170rust = true
2171js = false
2172js-umi = { enable = true }
2173go = { enable = true, path = "go-client" }
2174"#;
2175        let config = Config::from_str(&toml).unwrap();
2176        let clients = &config.clients;
2177        assert!(clients.auto);
2178        assert!(clients.rust.as_ref().unwrap().is_enabled());
2179        assert!(!clients.js.as_ref().unwrap().is_enabled());
2180        assert!(clients.js_umi.as_ref().unwrap().is_enabled());
2181        let go = clients.go.as_ref().unwrap();
2182        assert!(go.is_enabled());
2183        assert_eq!(go.path(), Some("go-client"));
2184
2185        let workspace_dir = Path::new("workspace");
2186        let resolved = clients.enabled(workspace_dir);
2187        assert_eq!(
2188            resolved,
2189            vec![
2190                ("js-umi", workspace_dir.join("clients/js-umi")),
2191                ("rust", workspace_dir.join("clients/rust")),
2192                ("go", workspace_dir.join("go-client")),
2193            ]
2194        );
2195    }
2196
2197    #[test]
2198    fn clients_custom_paths_resolve_from_workspace_root() {
2199        let workspace_dir = Path::new("workspace-root");
2200        let clients = ClientsConfig {
2201            rust: Some(ClientLanguageConfig::Detailed {
2202                enable: true,
2203                path: Some("sdk/rust".to_owned()),
2204            }),
2205            go: Some(ClientLanguageConfig::Detailed {
2206                enable: true,
2207                path: Some("/tmp/go-client".to_owned()),
2208            }),
2209            ..Default::default()
2210        };
2211
2212        let resolved = clients.enabled(workspace_dir);
2213        assert_eq!(
2214            resolved,
2215            vec![
2216                ("rust", workspace_dir.join("sdk/rust")),
2217                ("go", PathBuf::from("/tmp/go-client")),
2218            ]
2219        );
2220    }
2221
2222    #[test]
2223    fn clients_section_round_trips() {
2224        let toml = BASE_CONFIG.to_owned()
2225            + r#"
2226[clients]
2227auto = true
2228rust = true
2229go = { enable = true, path = "go-client" }
2230"#;
2231        let config = Config::from_str(&toml).unwrap();
2232        let serialized = config.to_string();
2233        let reparsed = Config::from_str(&serialized).unwrap();
2234        assert!(reparsed.clients.auto);
2235        assert!(reparsed.clients.rust.as_ref().unwrap().is_enabled());
2236        assert_eq!(
2237            reparsed.clients.go.as_ref().and_then(|go| go.path()),
2238            Some("go-client"),
2239        );
2240    }
2241
2242    #[test]
2243    fn clients_section_omitted_when_default() {
2244        let config = Config::from_str(BASE_CONFIG).unwrap();
2245        assert!(!config.to_string().contains("[clients]"));
2246    }
2247
2248    #[test]
2249    fn unknown_clients_fields_are_ignored_for_compatibility() {
2250        let toml = BASE_CONFIG.to_owned()
2251            + r#"
2252[clients]
2253python = true
2254metadata = { owner = "sdk-team" }
2255rust = true
2256"#;
2257        let config = Config::from_str(&toml).unwrap();
2258
2259        assert!(config.clients.rust.as_ref().unwrap().is_enabled());
2260    }
2261
2262    #[test]
2263    fn skip_local_validator_round_trips() {
2264        let toml = "skip_local_validator = true\n".to_owned() + BASE_CONFIG;
2265        let config = Config::from_str(&toml).unwrap();
2266        assert_eq!(config.skip_local_validator, Some(true));
2267        let serialized = config.to_string();
2268        assert!(serialized.contains("skip_local_validator = true"));
2269    }
2270
2271    #[test]
2272    fn test_validator_extra_args_round_trips() {
2273        let toml = BASE_CONFIG.to_owned()
2274            + r#"
2275[test.validator]
2276extra_args = [
2277    "--rpc-pubsub-enable-block-subscription",
2278    "--geyser-plugin-config",
2279    "geyser.json",
2280]
2281"#;
2282        let config = Config::from_str(&toml).unwrap();
2283        let extra_args = config
2284            .test_validator
2285            .as_ref()
2286            .and_then(|test| test.validator.as_ref())
2287            .and_then(|validator| validator.extra_args.as_ref())
2288            .unwrap();
2289
2290        assert_eq!(
2291            extra_args,
2292            &vec![
2293                "--rpc-pubsub-enable-block-subscription".to_string(),
2294                "--geyser-plugin-config".to_string(),
2295                "geyser.json".to_string(),
2296            ]
2297        );
2298
2299        let serialized = config.to_string();
2300        let reparsed = Config::from_str(&serialized).unwrap();
2301        let reparsed_extra_args = reparsed
2302            .test_validator
2303            .as_ref()
2304            .and_then(|test| test.validator.as_ref())
2305            .and_then(|validator| validator.extra_args.as_ref())
2306            .unwrap();
2307        assert_eq!(reparsed_extra_args, extra_args);
2308    }
2309
2310    #[test]
2311    fn parse_skip_lint_false() {
2312        let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false";
2313        let config = Config::from_str(&string).unwrap();
2314        assert!(!config.features.skip_lint);
2315    }
2316
2317    #[test]
2318    fn test_toml_resolves_account_dir_relative_to_file() {
2319        let dir = tempfile::tempdir().unwrap();
2320        let suite_dir = dir.path().join("tests").join("suite");
2321        let accounts_dir = suite_dir.join("accounts");
2322        fs::create_dir_all(&accounts_dir).unwrap();
2323
2324        let account_file = accounts_dir.join("account.json");
2325        fs::write(&account_file, "{}").unwrap();
2326
2327        let test_toml = suite_dir.join("Test.toml");
2328        fs::write(
2329            &test_toml,
2330            r#"
2331[scripts]
2332test = "true"
2333
2334[[test.validator.account]]
2335address = "3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj"
2336filename = "accounts/account.json"
2337
2338[[test.validator.account_dir]]
2339directory = "accounts"
2340"#,
2341        )
2342        .unwrap();
2343
2344        let parsed = TestToml::from_path(test_toml).unwrap();
2345        let validator = parsed.test.unwrap().validator.unwrap();
2346
2347        assert_eq!(
2348            validator.account.unwrap()[0].filename,
2349            account_file.canonicalize().unwrap().display().to_string()
2350        );
2351        assert_eq!(
2352            validator.account_dir.unwrap()[0].directory,
2353            accounts_dir.canonicalize().unwrap().display().to_string()
2354        );
2355    }
2356
2357    #[test]
2358    fn test_toml_keeps_ledger_path_relative() {
2359        let dir = tempfile::tempdir().unwrap();
2360        let suite_dir = dir.path().join("tests").join("suite");
2361        fs::create_dir_all(&suite_dir).unwrap();
2362
2363        let test_toml = suite_dir.join("Test.toml");
2364        fs::write(
2365            &test_toml,
2366            r#"
2367[scripts]
2368test = "true"
2369
2370[test.validator]
2371ledger = "ledgers/local"
2372"#,
2373        )
2374        .unwrap();
2375
2376        let parsed = TestToml::from_path(test_toml).unwrap();
2377        let validator = parsed.test.unwrap().validator.unwrap();
2378
2379        assert_eq!(validator.ledger, "ledgers/local");
2380
2381        fs::create_dir_all(suite_dir.join("ledgers").join("local")).unwrap();
2382
2383        let test_toml = suite_dir.join("Test.toml");
2384        let parsed = TestToml::from_path(test_toml).unwrap();
2385        let validator = parsed.test.unwrap().validator.unwrap();
2386
2387        assert_eq!(validator.ledger, "ledgers/local");
2388    }
2389}