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#[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 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 #[clap(global = true, long = "provider.cluster")]
72 pub cluster: Option<Cluster>,
73 #[clap(global = true, long = "provider.wallet")]
75 pub wallet: Option<WalletPath>,
76 #[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 pub fn discover() -> Result<Option<WithPath<Manifest>>> {
140 Manifest::discover_from_path(std::env::current_dir()?)
141 }
142
143 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 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 let (members, exclude) = self.canonicalize_workspace()?;
192
193 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 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 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 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 pub validator: Option<ValidatorType>,
368 pub test_validator: Option<TestValidator>,
369 pub test_config: Option<TestConfig>,
370 pub surfpool_config: Option<SurfpoolConfig>,
371 pub skip_local_validator: Option<bool>,
375}
376
377#[derive(ValueEnum, Parser, Clone, Copy, PartialEq, Eq, Debug, AbsolutePath)]
378pub enum ValidatorType {
379 Surfpool,
381 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#[derive(Clone, Debug, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize, AbsolutePath)]
397#[serde(rename_all = "lowercase")]
398pub enum PackageManager {
399 NPM,
401 Yarn,
403 PNPM,
405 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 #[serde(default = "FeaturesConfig::get_default_resolution")]
428 pub resolution: bool,
429 #[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#[derive(Debug, Default, Clone, Serialize, Deserialize)]
518pub struct ClientsConfig {
519 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
540#[serde(untagged)]
541pub enum ClientLanguageConfig {
542 Enabled(bool),
544 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
573pub const CLIENT_LANGUAGES: &[&str] = &["js", "js-umi", "rust", "go"];
575
576impl ClientsConfig {
577 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 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 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 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 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 (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 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 if http_key != "http" || ws_key != "ws" {
864 return Err(de::Error::custom("Invalid key"));
865 }
866
867 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, 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#[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
1252fn 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 *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 pub address: String,
1372 pub program: String,
1374 pub upgradeable: Option<bool>,
1376}
1377
1378#[derive(Debug, Clone, Serialize, Deserialize)]
1379pub struct CloneEntry {
1380 pub address: String,
1382}
1383
1384#[derive(Debug, Clone, Serialize, Deserialize)]
1385pub struct AccountEntry {
1386 pub address: String,
1388 pub filename: String,
1390}
1391
1392#[derive(Debug, Clone, Serialize, Deserialize)]
1393pub struct AccountDirEntry {
1394 pub directory: String,
1396}
1397
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1399pub struct FundedAccount {
1400 pub address: String,
1402 #[serde(skip_serializing_if = "Option::is_none")]
1404 pub lamports: Option<u64>,
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize)]
1408pub struct TokenMint {
1409 pub address: String,
1411 pub decimals: u8,
1413 #[serde(skip_serializing_if = "Option::is_none")]
1415 pub supply: Option<u64>,
1416 #[serde(skip_serializing_if = "Option::is_none")]
1418 pub mint_authority: Option<String>,
1419 #[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 pub mint: String,
1428 pub owner: String,
1430 pub amount: u64,
1432 #[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 #[serde(skip_serializing_if = "Option::is_none")]
1441 pub account: Option<Vec<AccountEntry>>,
1442 #[serde(skip_serializing_if = "Option::is_none")]
1444 pub account_dir: Option<Vec<AccountDirEntry>>,
1445 #[serde(skip_serializing_if = "Option::is_none")]
1447 pub fund_accounts: Option<Vec<FundedAccount>>,
1448 #[serde(skip_serializing_if = "Option::is_none")]
1450 pub mints: Option<Vec<TokenMint>>,
1451 #[serde(skip_serializing_if = "Option::is_none")]
1453 pub token_accounts: Option<Vec<TokenAccount>>,
1454 #[serde(skip_serializing_if = "Option::is_none")]
1456 pub bind_address: Option<String>,
1457 #[serde(skip_serializing_if = "Option::is_none")]
1459 pub clone: Option<Vec<CloneEntry>>,
1460 #[serde(skip_serializing_if = "Option::is_none")]
1462 pub dynamic_port_range: Option<String>,
1463 #[serde(skip_serializing_if = "Option::is_none")]
1465 pub faucet_port: Option<u16>,
1466 #[serde(skip_serializing_if = "Option::is_none")]
1468 pub faucet_sol: Option<String>,
1469 #[serde(skip_serializing_if = "Option::is_none")]
1471 pub geyser_plugin_config: Option<String>,
1472 #[serde(skip_serializing_if = "Option::is_none")]
1474 pub gossip_host: Option<String>,
1475 #[serde(skip_serializing_if = "Option::is_none")]
1477 pub gossip_port: Option<u16>,
1478 #[serde(skip_serializing_if = "Option::is_none")]
1480 pub url: Option<String>,
1481 #[serde(skip_serializing_if = "Option::is_none")]
1483 pub ledger: Option<String>,
1484 #[serde(skip_serializing_if = "Option::is_none")]
1486 pub limit_ledger_size: Option<String>,
1487 #[serde(skip_serializing_if = "Option::is_none")]
1489 pub rpc_port: Option<u16>,
1490 #[serde(skip_serializing_if = "Option::is_none")]
1492 pub slots_per_epoch: Option<String>,
1493 #[serde(skip_serializing_if = "Option::is_none")]
1495 pub ticks_per_slot: Option<u16>,
1496 #[serde(skip_serializing_if = "Option::is_none")]
1498 pub warp_slot: Option<Slot>,
1499 #[serde(skip_serializing_if = "Option::is_none")]
1501 pub deactivate_feature: Option<Vec<String>>,
1502 #[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 *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 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 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 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 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); }
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}