1use {
2 crate::{get_keypair, is_hidden, keys_sync, target_dir, AbsolutePath, DEFAULT_RPC_PORT},
3 anchor_client::Cluster,
4 anchor_lang_idl::types::Idl,
5 anyhow::{anyhow, bail, Context, Error, Result},
6 clap::{Parser, ValueEnum},
7 dirs::home_dir,
8 heck::ToSnakeCase,
9 reqwest::Url,
10 serde::{
11 de::{self, MapAccess, Visitor},
12 ser::SerializeMap,
13 Deserialize, Deserializer, Serialize, Serializer,
14 },
15 solana_cli_config::{Config as SolanaConfig, CONFIG_FILE},
16 solana_clock::Slot,
17 solana_commitment_config::CommitmentLevel,
18 solana_keypair::Keypair,
19 solana_pubkey::Pubkey,
20 solana_signer::Signer,
21 std::{
22 collections::{BTreeMap, HashMap},
23 convert::TryFrom,
24 fmt,
25 fs::{self, File},
26 io::{self, prelude::*},
27 marker::PhantomData,
28 ops::Deref,
29 path::{Path, PathBuf},
30 process::Command,
31 str::FromStr,
32 },
33 walkdir::WalkDir,
34};
35
36pub const SURFPOOL_HOST: &str = "127.0.0.1";
37#[derive(Debug, Clone, Copy, PartialEq, Eq, AbsolutePath)]
39pub struct CaseInsensitiveCommitmentLevel(pub CommitmentLevel);
40
41impl FromStr for CaseInsensitiveCommitmentLevel {
42 type Err = String;
43
44 fn from_str(s: &str) -> Result<Self, Self::Err> {
45 let lowercase = s.to_lowercase();
47 let commitment = CommitmentLevel::from_str(&lowercase).map_err(|_| {
48 format!(
49 "Invalid commitment level '{}'. Valid values are: processed, confirmed, finalized",
50 s
51 )
52 })?;
53 Ok(CaseInsensitiveCommitmentLevel(commitment))
54 }
55}
56
57impl From<CaseInsensitiveCommitmentLevel> for CommitmentLevel {
58 fn from(val: CaseInsensitiveCommitmentLevel) -> Self {
59 val.0
60 }
61}
62
63pub trait Merge: Sized {
64 fn merge(&mut self, _other: Self) {}
65}
66
67#[derive(Default, Debug, Parser, AbsolutePath)]
68pub struct ConfigOverride {
69 #[clap(global = true, long = "provider.cluster")]
71 pub cluster: Option<Cluster>,
72 #[clap(global = true, long = "provider.wallet")]
74 pub wallet: Option<WalletPath>,
75 #[clap(global = true, long = "commitment")]
77 pub commitment: Option<CaseInsensitiveCommitmentLevel>,
78}
79
80#[derive(Debug)]
81pub struct WithPath<T> {
82 inner: T,
83 path: PathBuf,
84}
85
86impl<T> WithPath<T> {
87 pub fn new(inner: T, path: PathBuf) -> Self {
88 Self { inner, path }
89 }
90
91 pub fn path(&self) -> &PathBuf {
92 &self.path
93 }
94
95 pub fn into_inner(self) -> T {
96 self.inner
97 }
98}
99
100impl<T> std::convert::AsRef<T> for WithPath<T> {
101 fn as_ref(&self) -> &T {
102 &self.inner
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub struct Manifest(cargo_toml::Manifest);
108
109impl Manifest {
110 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
111 cargo_toml::Manifest::from_path(&p)
112 .map(Manifest)
113 .map_err(anyhow::Error::from)
114 .with_context(|| format!("Error reading manifest from path: {}", p.as_ref().display()))
115 }
116
117 pub fn lib_name(&self) -> Result<String> {
118 match &self.lib {
119 Some(cargo_toml::Product {
120 name: Some(name), ..
121 }) => Ok(name.to_owned()),
122 _ => self
123 .package
124 .as_ref()
125 .ok_or_else(|| anyhow!("package section not provided"))
126 .map(|pkg| pkg.name.to_snake_case()),
127 }
128 }
129
130 pub fn version(&self) -> String {
131 match &self.package {
132 Some(package) => package.version().to_string(),
133 _ => "0.0.0".to_string(),
134 }
135 }
136
137 pub fn discover() -> Result<Option<WithPath<Manifest>>> {
139 Manifest::discover_from_path(std::env::current_dir()?)
140 }
141
142 pub fn discover_from_path(start_from: PathBuf) -> Result<Option<WithPath<Manifest>>> {
144 let mut cwd_opt = Some(start_from.as_path());
145
146 while let Some(cwd) = cwd_opt {
147 let mut anchor_toml = false;
148
149 for f in fs::read_dir(cwd).with_context(|| {
150 format!("Error reading the directory with path: {}", cwd.display())
151 })? {
152 let p = f
153 .with_context(|| {
154 format!("Error reading the directory with path: {}", cwd.display())
155 })?
156 .path();
157 if let Some(filename) = p.file_name().and_then(|name| name.to_str()) {
158 if filename == "Cargo.toml" {
159 return Ok(Some(WithPath::new(Manifest::from_path(&p)?, p)));
160 }
161 if filename == "Anchor.toml" {
162 anchor_toml = true;
163 }
164 }
165 }
166
167 if anchor_toml {
169 break;
170 }
171
172 cwd_opt = cwd.parent();
173 }
174
175 Ok(None)
176 }
177}
178
179impl Deref for Manifest {
180 type Target = cargo_toml::Manifest;
181
182 fn deref(&self) -> &Self::Target {
183 &self.0
184 }
185}
186
187impl WithPath<Config> {
188 pub fn get_rust_program_list(&self) -> Result<Vec<PathBuf>> {
189 let (members, exclude) = self.canonicalize_workspace()?;
191
192 let program_paths: Vec<PathBuf> = {
197 if members.is_empty() {
198 let path = self.path().parent().unwrap().join("programs");
199 if let Ok(entries) = fs::read_dir(path) {
200 entries
201 .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false))
202 .map(|dir| dir.map(|d| d.path().canonicalize().unwrap()))
203 .collect::<Vec<Result<PathBuf, std::io::Error>>>()
204 .into_iter()
205 .collect::<Result<Vec<PathBuf>, std::io::Error>>()?
206 } else {
207 Vec::new()
208 }
209 } else {
210 members
211 }
212 };
213
214 Ok(program_paths
216 .into_iter()
217 .filter(|m| !exclude.contains(m))
218 .collect())
219 }
220
221 pub fn read_all_programs(&self) -> Result<Vec<Program>> {
222 let mut r = vec![];
223 for path in self.get_rust_program_list()? {
224 let cargo = Manifest::from_path(path.join("Cargo.toml"))?;
225 let lib_name = cargo.lib_name()?;
226
227 let idl_filepath = target_dir()?
228 .join("idl")
229 .join(&lib_name)
230 .with_extension("json");
231 let idl = fs::read(idl_filepath)
232 .ok()
233 .map(|bytes| serde_json::from_reader(&*bytes))
234 .transpose()?;
235
236 r.push(Program {
237 lib_name,
238 path,
239 idl,
240 });
241 }
242 Ok(r)
243 }
244
245 pub fn get_programs(&self, name: Option<String>) -> Result<Vec<Program>> {
249 let programs = self.read_all_programs()?;
250 let programs = match name {
251 Some(name) => vec![programs
252 .into_iter()
253 .find(|program| {
254 name == program.lib_name
255 || name == program.path.file_name().unwrap().to_str().unwrap()
256 })
257 .ok_or_else(|| anyhow!("Program {name} not found"))?],
258 None => programs,
259 };
260
261 Ok(programs)
262 }
263
264 pub fn get_program(&self, name: &str) -> Result<Program> {
266 self.get_programs(Some(name.to_owned()))?
267 .into_iter()
268 .next()
269 .ok_or_else(|| anyhow!("Expected a program"))
270 }
271
272 pub fn canonicalize_workspace(&self) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
273 let members = self.process_paths(&self.workspace.members)?;
274 let exclude = self.process_paths(&self.workspace.exclude)?;
275 Ok((members, exclude))
276 }
277
278 fn process_paths(&self, paths: &[String]) -> Result<Vec<PathBuf>, Error> {
279 let base_path = self.path().parent().unwrap();
280 paths
281 .iter()
282 .flat_map(|m| {
283 let path = base_path.join(m);
284 if m.ends_with("/*") {
285 let dir = path.parent().unwrap();
286 match fs::read_dir(dir) {
287 Ok(entries) => entries
288 .filter_map(|entry| entry.ok())
289 .map(|entry| self.process_single_path(&entry.path()))
290 .collect(),
291 Err(e) => vec![Err(Error::new(io::Error::other(format!(
292 "Error reading directory {dir:?}: {e}"
293 ))))],
294 }
295 } else {
296 vec![self.process_single_path(&path)]
297 }
298 })
299 .collect()
300 }
301
302 fn process_single_path(&self, path: &PathBuf) -> Result<PathBuf, Error> {
303 path.canonicalize().map_err(|e| {
304 Error::new(io::Error::other(format!(
305 "Error canonicalizing path {path:?}: {e}"
306 )))
307 })
308 }
309}
310
311impl WalletPath {
312 fn resolve_relative_to(self, base: &Path) -> Self {
313 if self.0.is_relative() {
314 Self(base.join(self.0))
315 } else {
316 self
317 }
318 }
319}
320
321impl<T> std::ops::Deref for WithPath<T> {
322 type Target = T;
323 fn deref(&self) -> &Self::Target {
324 &self.inner
325 }
326}
327
328impl<T> std::ops::DerefMut for WithPath<T> {
329 fn deref_mut(&mut self) -> &mut Self::Target {
330 &mut self.inner
331 }
332}
333
334#[derive(Debug, Default)]
335pub struct Config {
336 pub toolchain: ToolchainConfig,
337 pub features: FeaturesConfig,
338 pub provider: ProviderConfig,
339 pub programs: ProgramsConfig,
340 pub scripts: ScriptsConfig,
341 pub hooks: HooksConfig,
342 pub workspace: WorkspaceConfig,
343 pub clients: ClientsConfig,
344 pub validator: Option<ValidatorType>,
348 pub test_validator: Option<TestValidator>,
349 pub test_config: Option<TestConfig>,
350 pub surfpool_config: Option<SurfpoolConfig>,
351 pub skip_local_validator: Option<bool>,
355}
356
357#[derive(ValueEnum, Parser, Clone, Copy, PartialEq, Eq, Debug, AbsolutePath)]
358pub enum ValidatorType {
359 Surfpool,
361 Legacy,
363}
364#[derive(Default, Clone, Debug, Serialize, Deserialize)]
365pub struct ToolchainConfig {
366 pub anchor_version: Option<String>,
367 pub solana_version: Option<String>,
368 pub package_manager: Option<PackageManager>,
369}
370
371#[derive(Clone, Debug, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize, AbsolutePath)]
378#[serde(rename_all = "lowercase")]
379pub enum PackageManager {
380 NPM,
382 Yarn,
384 PNPM,
386 Bun,
388}
389
390impl std::fmt::Display for PackageManager {
391 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
392 let pkg_manager_str = match self {
393 PackageManager::NPM => "npm",
394 PackageManager::Yarn => "yarn",
395 PackageManager::PNPM => "pnpm",
396 PackageManager::Bun => "bun",
397 };
398
399 write!(f, "{pkg_manager_str}")
400 }
401}
402
403#[derive(Clone, Debug, Serialize, Deserialize)]
404pub struct FeaturesConfig {
405 #[serde(default = "FeaturesConfig::get_default_resolution")]
409 pub resolution: bool,
410 #[serde(default, rename = "skip-lint")]
412 pub skip_lint: bool,
413}
414
415impl FeaturesConfig {
416 fn get_default_resolution() -> bool {
417 true
418 }
419}
420
421impl Default for FeaturesConfig {
422 fn default() -> Self {
423 Self {
424 resolution: Self::get_default_resolution(),
425 skip_lint: false,
426 }
427 }
428}
429
430#[derive(Debug, Default)]
431pub struct ProviderConfig {
432 pub cluster: Cluster,
433 pub wallet: WalletPath,
434}
435
436pub type ScriptsConfig = BTreeMap<String, String>;
437
438pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
439
440#[derive(Default, Clone, Debug, Serialize, Deserialize)]
441#[serde(deny_unknown_fields)]
442pub struct HooksConfig {
443 #[serde(alias = "pre-build")]
444 pre_build: Option<Hook>,
445 #[serde(alias = "post-build")]
446 post_build: Option<Hook>,
447 #[serde(alias = "pre-test")]
448 pre_test: Option<Hook>,
449 #[serde(alias = "post-test")]
450 post_test: Option<Hook>,
451 #[serde(alias = "pre-deploy")]
452 pre_deploy: Option<Hook>,
453 #[serde(alias = "post-deploy")]
454 post_deploy: Option<Hook>,
455}
456
457#[derive(Clone, Debug, Serialize, Deserialize)]
458#[serde(untagged)]
459enum Hook {
460 Single(String),
461 List(Vec<String>),
462}
463
464impl Hook {
465 pub fn hooks(&self) -> &[String] {
466 match self {
467 Self::Single(h) => std::slice::from_ref(h),
468 Self::List(l) => l.as_slice(),
469 }
470 }
471}
472
473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
474pub enum HookType {
475 PreBuild,
476 PostBuild,
477 PreTest,
478 PostTest,
479 PreDeploy,
480 PostDeploy,
481}
482
483#[derive(Debug, Default, Clone, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct ClientsConfig {
502 #[serde(default, skip_serializing_if = "is_false")]
504 pub auto: bool,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub js: Option<ClientLanguageConfig>,
507 #[serde(
508 default,
509 rename = "js-umi",
510 alias = "js_umi",
511 skip_serializing_if = "Option::is_none"
512 )]
513 pub js_umi: Option<ClientLanguageConfig>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub rust: Option<ClientLanguageConfig>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub go: Option<ClientLanguageConfig>,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(untagged)]
524pub enum ClientLanguageConfig {
525 Enabled(bool),
527 Detailed {
529 #[serde(default = "ClientLanguageConfig::default_enable")]
530 enable: bool,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 path: Option<String>,
533 },
534}
535
536impl ClientLanguageConfig {
537 fn default_enable() -> bool {
538 true
539 }
540
541 pub fn is_enabled(&self) -> bool {
542 match self {
543 Self::Enabled(b) => *b,
544 Self::Detailed { enable, .. } => *enable,
545 }
546 }
547
548 pub fn path(&self) -> Option<&str> {
549 match self {
550 Self::Enabled(_) => None,
551 Self::Detailed { path, .. } => path.as_deref(),
552 }
553 }
554}
555
556pub const CLIENT_LANGUAGES: &[&str] = &["js", "js-umi", "rust", "go"];
559
560impl ClientsConfig {
561 pub fn get(&self, language: &str) -> Option<&ClientLanguageConfig> {
563 match language {
564 "js" => self.js.as_ref(),
565 "js-umi" => self.js_umi.as_ref(),
566 "rust" => self.rust.as_ref(),
567 "go" => self.go.as_ref(),
568 _ => None,
569 }
570 }
571
572 pub fn enabled(&self, base: &Path) -> Vec<(&'static str, PathBuf)> {
575 CLIENT_LANGUAGES
576 .iter()
577 .filter_map(|&lang| {
578 let entry = self.get(lang)?;
579 if !entry.is_enabled() {
580 return None;
581 }
582 let path = entry
583 .path()
584 .map(PathBuf::from)
585 .unwrap_or_else(|| base.join(lang));
586 Some((lang, path))
587 })
588 .collect()
589 }
590}
591
592fn is_false(b: &bool) -> bool {
593 !*b
594}
595
596#[derive(Debug, Default, Clone, Serialize, Deserialize)]
597pub struct WorkspaceConfig {
598 #[serde(default, skip_serializing_if = "Vec::is_empty")]
599 pub members: Vec<String>,
600 #[serde(default, skip_serializing_if = "Vec::is_empty")]
601 pub exclude: Vec<String>,
602 #[serde(default, skip_serializing_if = "String::is_empty")]
603 pub types: String,
604}
605
606#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug, AbsolutePath)]
607pub enum BootstrapMode {
608 None,
609 Debian,
610}
611
612#[derive(Debug, Clone)]
613pub struct BuildConfig {
614 pub verifiable: bool,
615 pub solana_version: Option<String>,
616 pub docker_image: String,
617 pub bootstrap: BootstrapMode,
618}
619
620impl Config {
621 pub fn add_test_config(
622 &mut self,
623 root: impl AsRef<Path>,
624 test_paths: Vec<PathBuf>,
625 ) -> Result<()> {
626 self.test_config = TestConfig::discover(root, test_paths)?;
627 Ok(())
628 }
629
630 pub fn docker(&self) -> String {
631 let version = self
632 .toolchain
633 .anchor_version
634 .as_deref()
635 .unwrap_or(crate::DOCKER_BUILDER_VERSION);
636 format!("solanafoundation/anchor:v{version}")
637 }
638
639 pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
640 Config::_discover().map(|opt| {
641 opt.map(|mut cfg| {
642 if let Some(cluster) = cfg_override.cluster.clone() {
643 cfg.provider.cluster = cluster;
644 }
645 if let Some(wallet) = cfg_override.wallet.clone() {
646 cfg.provider.wallet = wallet;
647 }
648 cfg
649 })
650 })
651 }
652
653 fn _discover() -> Result<Option<WithPath<Config>>> {
655 let _cwd = std::env::current_dir()?;
656 let mut cwd_opt = Some(_cwd.as_path());
657
658 while let Some(cwd) = cwd_opt {
659 for f in fs::read_dir(cwd).with_context(|| {
660 format!("Error reading the directory with path: {}", cwd.display())
661 })? {
662 let p = f
663 .with_context(|| {
664 format!("Error reading the directory with path: {}", cwd.display())
665 })?
666 .path();
667 if let Some(filename) = p.file_name() {
668 if filename.to_str() == Some("Anchor.toml") {
669 let config_dir = p.parent().unwrap();
670 let mut cfg = Config::from_path(&p)?;
672 let deploy_dir = target_dir()?.join("deploy");
673 if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
674 println!("Updating program ids...");
675 fs::create_dir_all(deploy_dir)?;
676 keys_sync(&ConfigOverride::default(), None)?;
677 cfg = Config::from_path(&p)?;
678 }
679 cfg.provider.wallet = cfg.provider.wallet.resolve_relative_to(config_dir);
680
681 return Ok(Some(WithPath::new(cfg, p)));
682 }
683 }
684 }
685
686 cwd_opt = cwd.parent();
687 }
688
689 Ok(None)
690 }
691
692 fn from_path(p: impl AsRef<Path>) -> Result<Self> {
693 fs::read_to_string(&p)
694 .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))?
695 .parse::<Self>()
696 }
697
698 pub fn wallet_kp(&self) -> Result<Keypair> {
699 get_keypair(Path::new(&self.provider.wallet.0))
700 }
701
702 pub fn run_hooks(&self, hook_type: HookType) -> Result<()> {
703 let hooks = match hook_type {
704 HookType::PreBuild => &self.hooks.pre_build,
705 HookType::PostBuild => &self.hooks.post_build,
706 HookType::PreTest => &self.hooks.pre_test,
707 HookType::PostTest => &self.hooks.post_test,
708 HookType::PreDeploy => &self.hooks.pre_deploy,
709 HookType::PostDeploy => &self.hooks.post_deploy,
710 };
711 let cmds = hooks.as_ref().map(Hook::hooks).unwrap_or_default();
712 for cmd in cmds {
713 let status = Command::new("bash")
714 .arg("-c")
715 .arg(cmd)
716 .status()
717 .with_context(|| format!("failed to execute `{cmd}`"))?;
718 if !status.success() {
719 match status.code() {
720 Some(code) => bail!("`{cmd}` failed with exit code {code}"),
721 None => bail!("`{cmd}` killed by signal"),
722 }
723 }
724 }
725 Ok(())
726 }
727}
728
729#[derive(Debug, Serialize, Deserialize)]
730struct _Config {
731 toolchain: Option<ToolchainConfig>,
732 features: Option<FeaturesConfig>,
733 programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
734 provider: Provider,
735 workspace: Option<WorkspaceConfig>,
736 scripts: Option<ScriptsConfig>,
737 hooks: Option<HooksConfig>,
738 test: Option<_TestValidator>,
739 surfpool: Option<_SurfpoolConfig>,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 skip_local_validator: Option<bool>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 clients: Option<ClientsConfig>,
744}
745
746#[derive(Debug, Serialize, Deserialize)]
747struct Provider {
748 #[serde(serialize_with = "ser_cluster", deserialize_with = "des_cluster")]
749 cluster: Cluster,
750 wallet: String,
751}
752
753fn ser_cluster<S: Serializer>(cluster: &Cluster, s: S) -> Result<S::Ok, S::Error> {
754 match cluster {
755 Cluster::Custom(http, ws) => {
756 match (Url::parse(http), Url::parse(ws)) {
757 (Ok(h), Ok(w)) if h.domain() == w.domain() => s.serialize_str(http),
759 _ => {
760 let mut map = s.serialize_map(Some(2))?;
761 map.serialize_entry("http", http)?;
762 map.serialize_entry("ws", ws)?;
763 map.end()
764 }
765 }
766 }
767 _ => s.serialize_str(&cluster.to_string()),
768 }
769}
770
771fn des_cluster<'de, D>(deserializer: D) -> Result<Cluster, D::Error>
772where
773 D: Deserializer<'de>,
774{
775 struct StringOrCustomCluster(PhantomData<fn() -> Cluster>);
776
777 impl<'de> Visitor<'de> for StringOrCustomCluster {
778 type Value = Cluster;
779
780 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
781 formatter.write_str("string or map")
782 }
783
784 fn visit_str<E>(self, value: &str) -> Result<Cluster, E>
785 where
786 E: de::Error,
787 {
788 value.parse().map_err(de::Error::custom)
789 }
790
791 fn visit_map<M>(self, mut map: M) -> Result<Cluster, M::Error>
792 where
793 M: MapAccess<'de>,
794 {
795 if let (Some((http_key, http_value)), Some((ws_key, ws_value))) = (
797 map.next_entry::<String, String>()?,
798 map.next_entry::<String, String>()?,
799 ) {
800 if http_key != "http" || ws_key != "ws" {
802 return Err(de::Error::custom("Invalid key"));
803 }
804
805 Url::parse(&http_value).map_err(de::Error::custom)?;
807 Url::parse(&ws_value).map_err(de::Error::custom)?;
808
809 Ok(Cluster::Custom(http_value, ws_value))
810 } else {
811 Err(de::Error::custom("Invalid entry"))
812 }
813 }
814 }
815 deserializer.deserialize_any(StringOrCustomCluster(PhantomData))
816}
817
818impl fmt::Display for Config {
819 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820 let programs = {
821 let c = ser_programs(&self.programs);
822 if c.is_empty() {
823 None
824 } else {
825 Some(c)
826 }
827 };
828 let cfg = _Config {
829 toolchain: Some(self.toolchain.clone()),
830 features: Some(self.features.clone()),
831 provider: Provider {
832 cluster: self.provider.cluster.clone(),
833 wallet: self.provider.wallet.stringify_with_tilde(),
834 },
835 test: self.test_validator.clone().map(Into::into),
836 scripts: match self.scripts.is_empty() {
837 true => None,
838 false => Some(self.scripts.clone()),
839 },
840 hooks: Some(self.hooks.clone()),
841 programs,
842 workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
843 .then(|| self.workspace.clone()),
844 surfpool: self.surfpool_config.clone().map(Into::into),
845 skip_local_validator: self.skip_local_validator,
846 clients: {
847 let c = &self.clients;
848 let empty = !c.auto
849 && c.js.is_none()
850 && c.js_umi.is_none()
851 && c.rust.is_none()
852 && c.go.is_none();
853 if empty {
854 None
855 } else {
856 Some(c.clone())
857 }
858 },
859 };
860
861 let cfg = toml::to_string(&cfg).expect("Must be well formed");
862 write!(f, "{cfg}")
863 }
864}
865
866impl FromStr for Config {
867 type Err = Error;
868
869 fn from_str(s: &str) -> Result<Self, Self::Err> {
870 let cfg: _Config =
871 toml::from_str(s).map_err(|e| anyhow!("Unable to deserialize config: {e}"))?;
872 Ok(Config {
873 toolchain: cfg.toolchain.unwrap_or_default(),
874 features: cfg.features.unwrap_or_default(),
875 provider: ProviderConfig {
876 cluster: cfg.provider.cluster,
877 wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
878 },
879 scripts: cfg.scripts.unwrap_or_default(),
880 hooks: cfg.hooks.unwrap_or_default(),
881 validator: None, test_validator: cfg.test.map(Into::into),
883 test_config: None,
884 programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
885 workspace: cfg.workspace.unwrap_or_default(),
886 surfpool_config: cfg.surfpool.map(Into::into),
887 skip_local_validator: cfg.skip_local_validator,
888 clients: cfg.clients.unwrap_or_default(),
889 })
890 }
891}
892
893pub fn get_solana_cfg_url() -> Result<String, io::Error> {
894 let config_file = CONFIG_FILE.as_ref().ok_or_else(|| {
895 io::Error::new(
896 io::ErrorKind::NotFound,
897 "Default Solana config was not found",
898 )
899 })?;
900 SolanaConfig::load(config_file).map(|config| config.json_rpc_url)
901}
902
903fn ser_programs(
904 programs: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
905) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
906 programs
907 .iter()
908 .map(|(cluster, programs)| {
909 let cluster = cluster.to_string();
910 let programs = programs
911 .iter()
912 .map(|(name, deployment)| {
913 (
914 name.clone(),
915 to_value(&_ProgramDeployment::from(deployment)),
916 )
917 })
918 .collect::<BTreeMap<String, serde_json::Value>>();
919 (cluster, programs)
920 })
921 .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
922}
923
924fn to_value(dep: &_ProgramDeployment) -> serde_json::Value {
925 if dep.path.is_none() && dep.idl.is_none() {
926 return serde_json::Value::String(dep.address.to_string());
927 }
928 serde_json::to_value(dep).unwrap()
929}
930
931fn deser_programs(
932 programs: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
933) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
934 programs
935 .iter()
936 .map(|(cluster, programs)| {
937 let cluster: Cluster = cluster.parse()?;
938 let programs = programs
939 .iter()
940 .map(|(name, program_id)| {
941 Ok((
942 name.clone(),
943 ProgramDeployment::try_from(match &program_id {
944 serde_json::Value::String(address) => _ProgramDeployment {
945 address: address.parse()?,
946 path: None,
947 idl: None,
948 },
949
950 serde_json::Value::Object(_) => {
951 serde_json::from_value(program_id.clone())
952 .map_err(|_| anyhow!("Unable to read toml"))?
953 }
954 _ => return Err(anyhow!("Invalid toml type")),
955 })?,
956 ))
957 })
958 .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
959 Ok((cluster, programs))
960 })
961 .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
962}
963
964#[derive(Default, Debug, Clone, Serialize, Deserialize)]
965pub struct TestValidator {
966 pub genesis: Option<Vec<GenesisEntry>>,
967 pub validator: Option<Validator>,
968 pub startup_wait: i32,
969 pub shutdown_wait: i32,
970 pub upgradeable: bool,
971}
972
973#[derive(Default, Debug, Clone, Serialize, Deserialize)]
974pub struct SurfpoolConfig {
975 pub startup_wait: i32,
976 pub shutdown_wait: i32,
977 pub rpc_port: u16,
978 pub ws_port: Option<u16>,
979 pub host: String,
980 pub online: Option<bool>,
981 pub datasource_rpc_url: Option<String>,
982 pub airdrop_addresses: Option<Vec<String>>,
983 pub manifest_file_path: Option<String>,
984 pub runbooks: Option<Vec<String>>,
985 pub slot_time: Option<u16>,
986 pub log_level: Option<String>,
987 pub block_production_mode: Option<String>,
988}
989
990#[derive(Default, Debug, Clone, Serialize, Deserialize)]
991pub struct _TestValidator {
992 #[serde(skip_serializing_if = "Option::is_none")]
993 pub genesis: Option<Vec<GenesisEntry>>,
994 #[serde(skip_serializing_if = "Option::is_none")]
995 pub validator: Option<_Validator>,
996 #[serde(skip_serializing_if = "Option::is_none")]
997 pub startup_wait: Option<i32>,
998 #[serde(skip_serializing_if = "Option::is_none")]
999 pub shutdown_wait: Option<i32>,
1000 #[serde(skip_serializing_if = "Option::is_none")]
1001 pub upgradeable: Option<bool>,
1002}
1003
1004#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1005pub struct _SurfpoolConfig {
1006 #[serde(skip_serializing_if = "Option::is_none")]
1007 pub startup_wait: Option<i32>,
1008 #[serde(skip_serializing_if = "Option::is_none")]
1009 pub shutdown_wait: Option<i32>,
1010 #[serde(skip_serializing_if = "Option::is_none")]
1011 pub rpc_port: Option<u16>,
1012 #[serde(skip_serializing_if = "Option::is_none")]
1013 pub ws_port: Option<u16>,
1014 #[serde(skip_serializing_if = "Option::is_none")]
1015 pub host: Option<String>,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub online: Option<bool>,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1019 pub datasource_rpc_url: Option<String>,
1020 #[serde(skip_serializing_if = "Option::is_none")]
1021 pub airdrop_addresses: Option<Vec<String>>,
1022 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub manifest_file_path: Option<String>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub runbooks: Option<Vec<String>>,
1026 #[serde(skip_serializing_if = "Option::is_none")]
1027 pub slot_time: Option<u16>,
1028 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub log_level: Option<String>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1031 pub block_production_mode: Option<String>,
1032}
1033
1034impl From<_SurfpoolConfig> for SurfpoolConfig {
1035 fn from(_surfpool_config: _SurfpoolConfig) -> Self {
1036 Self {
1037 startup_wait: _surfpool_config.startup_wait.unwrap_or(STARTUP_WAIT),
1038 shutdown_wait: _surfpool_config.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
1039 rpc_port: _surfpool_config.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1040 host: _surfpool_config.host.unwrap_or(SURFPOOL_HOST.to_string()),
1041 ws_port: _surfpool_config.ws_port,
1042 online: _surfpool_config.online,
1043 datasource_rpc_url: _surfpool_config.datasource_rpc_url,
1044 airdrop_addresses: _surfpool_config.airdrop_addresses,
1045 manifest_file_path: _surfpool_config.manifest_file_path,
1046 runbooks: _surfpool_config.runbooks,
1047 slot_time: _surfpool_config.slot_time,
1048 log_level: _surfpool_config.log_level,
1049 block_production_mode: _surfpool_config.block_production_mode,
1050 }
1051 }
1052}
1053
1054impl From<SurfpoolConfig> for _SurfpoolConfig {
1055 fn from(surfpool_config: SurfpoolConfig) -> Self {
1056 Self {
1057 startup_wait: Some(surfpool_config.startup_wait),
1058 shutdown_wait: Some(surfpool_config.shutdown_wait),
1059 rpc_port: Some(surfpool_config.rpc_port),
1060 ws_port: surfpool_config.ws_port,
1061 host: Some(surfpool_config.host),
1062 online: surfpool_config.online,
1063 datasource_rpc_url: surfpool_config.datasource_rpc_url,
1064 airdrop_addresses: surfpool_config.airdrop_addresses,
1065 manifest_file_path: surfpool_config.manifest_file_path,
1066 runbooks: surfpool_config.runbooks,
1067 slot_time: surfpool_config.slot_time,
1068 log_level: surfpool_config.log_level,
1069 block_production_mode: surfpool_config.block_production_mode,
1070 }
1071 }
1072}
1073pub const STARTUP_WAIT: i32 = 5000;
1074pub const SHUTDOWN_WAIT: i32 = 2000;
1075
1076impl From<_TestValidator> for TestValidator {
1077 fn from(_test_validator: _TestValidator) -> Self {
1078 Self {
1079 shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
1080 startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
1081 genesis: _test_validator.genesis,
1082 validator: _test_validator.validator.map(Into::into),
1083 upgradeable: _test_validator.upgradeable.unwrap_or(false),
1084 }
1085 }
1086}
1087
1088impl From<TestValidator> for _TestValidator {
1089 fn from(test_validator: TestValidator) -> Self {
1090 Self {
1091 shutdown_wait: Some(test_validator.shutdown_wait),
1092 startup_wait: Some(test_validator.startup_wait),
1093 genesis: test_validator.genesis,
1094 validator: test_validator.validator.map(Into::into),
1095 upgradeable: Some(test_validator.upgradeable),
1096 }
1097 }
1098}
1099
1100#[derive(Debug, Clone)]
1101pub struct TestConfig {
1102 pub test_suite_configs: HashMap<PathBuf, TestToml>,
1103}
1104
1105impl Deref for TestConfig {
1106 type Target = HashMap<PathBuf, TestToml>;
1107
1108 fn deref(&self) -> &Self::Target {
1109 &self.test_suite_configs
1110 }
1111}
1112
1113impl TestConfig {
1114 pub fn discover(root: impl AsRef<Path>, test_paths: Vec<PathBuf>) -> Result<Option<Self>> {
1115 let walker = WalkDir::new(root).into_iter();
1116 let mut test_suite_configs = HashMap::new();
1117 for entry in walker.filter_entry(|e| !is_hidden(e)) {
1118 let entry = entry?;
1119 if entry.file_name() == "Test.toml" {
1120 let entry_path = entry.path();
1121 let test_toml = TestToml::from_path(entry_path)?;
1122 if test_paths.is_empty() || test_paths.iter().any(|p| entry_path.starts_with(p)) {
1123 test_suite_configs.insert(entry.path().into(), test_toml);
1124 }
1125 }
1126 }
1127
1128 Ok(match test_suite_configs.is_empty() {
1129 true => None,
1130 false => Some(Self { test_suite_configs }),
1131 })
1132 }
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize)]
1138pub struct _TestToml {
1139 pub extends: Option<Vec<String>>,
1140 pub test: Option<_TestValidator>,
1141 pub scripts: Option<ScriptsConfig>,
1142}
1143
1144impl _TestToml {
1145 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
1146 let s = fs::read_to_string(&path)?;
1147 let parsed_toml: Self = toml::from_str(&s)?;
1148 let mut current_toml = _TestToml {
1149 extends: None,
1150 test: None,
1151 scripts: None,
1152 };
1153 if let Some(bases) = &parsed_toml.extends {
1154 for base in bases {
1155 let mut canonical_base = base.clone();
1156 canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
1157 current_toml.merge(_TestToml::from_path(&canonical_base)?);
1158 }
1159 }
1160 current_toml.merge(parsed_toml);
1161
1162 if let Some(test) = &mut current_toml.test {
1163 if let Some(genesis_programs) = &mut test.genesis {
1164 for entry in genesis_programs {
1165 entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
1166 }
1167 }
1168 if let Some(validator) = &mut test.validator {
1169 if let Some(ledger_dir) = &mut validator.ledger {
1170 *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?;
1171 }
1172 if let Some(accounts) = &mut validator.account {
1173 for entry in accounts {
1174 entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
1175 }
1176 }
1177 if let Some(account_dirs) = &mut validator.account_dir {
1178 for entry in account_dirs {
1179 entry.directory =
1180 canonicalize_filepath_from_origin(&entry.directory, &path)?;
1181 }
1182 }
1183 }
1184 }
1185 Ok(current_toml)
1186 }
1187}
1188
1189fn canonicalize_filepath_from_origin(
1194 file_path: impl AsRef<Path>,
1195 origin: impl AsRef<Path>,
1196) -> Result<String> {
1197 let previous_dir = std::env::current_dir()?;
1198 std::env::set_current_dir(origin.as_ref().parent().unwrap())?;
1199 let result = fs::canonicalize(&file_path)
1200 .with_context(|| {
1201 format!(
1202 "Error reading (possibly relative) path: {}. If relative, this is the path that \
1203 was used as the current path: {}",
1204 &file_path.as_ref().display(),
1205 &origin.as_ref().display()
1206 )
1207 })?
1208 .display()
1209 .to_string();
1210 std::env::set_current_dir(previous_dir)?;
1211 Ok(result)
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize)]
1215pub struct TestToml {
1216 #[serde(skip_serializing_if = "Option::is_none")]
1217 pub test: Option<TestValidator>,
1218 pub scripts: ScriptsConfig,
1219}
1220
1221impl TestToml {
1222 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
1223 WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
1224 }
1225}
1226
1227impl Merge for _TestToml {
1228 fn merge(&mut self, other: Self) {
1229 let mut my_scripts = self.scripts.take();
1230 match &mut my_scripts {
1231 None => my_scripts = other.scripts,
1232 Some(my_scripts) => {
1233 if let Some(other_scripts) = other.scripts {
1234 for (name, script) in other_scripts {
1235 my_scripts.insert(name, script);
1236 }
1237 }
1238 }
1239 }
1240
1241 let mut my_test = self.test.take();
1242 match &mut my_test {
1243 Some(my_test) => {
1244 if let Some(other_test) = other.test {
1245 if let Some(startup_wait) = other_test.startup_wait {
1246 my_test.startup_wait = Some(startup_wait);
1247 }
1248 if let Some(other_genesis) = other_test.genesis {
1249 match &mut my_test.genesis {
1250 Some(my_genesis) => {
1251 for other_entry in other_genesis {
1252 match my_genesis
1253 .iter()
1254 .position(|g| *g.address == other_entry.address)
1255 {
1256 None => my_genesis.push(other_entry),
1257 Some(i) => my_genesis[i] = other_entry,
1258 }
1259 }
1260 }
1261 None => my_test.genesis = Some(other_genesis),
1262 }
1263 }
1264 let mut my_validator = my_test.validator.take();
1265 match &mut my_validator {
1266 None => my_validator = other_test.validator,
1267 Some(my_validator) => {
1268 if let Some(other_validator) = other_test.validator {
1269 my_validator.merge(other_validator)
1270 }
1271 }
1272 }
1273
1274 my_test.validator = my_validator;
1275 }
1276 }
1277 None => my_test = other.test,
1278 };
1279
1280 *self = Self {
1284 test: my_test,
1285 scripts: my_scripts,
1286 extends: self.extends.take(),
1287 };
1288 }
1289}
1290
1291impl TryFrom<WithPath<_TestToml>> for TestToml {
1292 type Error = Error;
1293
1294 fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
1295 Ok(Self {
1296 test: value.test.take().map(Into::into),
1297 scripts: value
1298 .scripts
1299 .take()
1300 .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
1301 })
1302 }
1303}
1304
1305#[derive(Debug, Clone, Serialize, Deserialize)]
1306pub struct GenesisEntry {
1307 pub address: String,
1309 pub program: String,
1311 pub upgradeable: Option<bool>,
1313}
1314
1315#[derive(Debug, Clone, Serialize, Deserialize)]
1316pub struct CloneEntry {
1317 pub address: String,
1319}
1320
1321#[derive(Debug, Clone, Serialize, Deserialize)]
1322pub struct AccountEntry {
1323 pub address: String,
1325 pub filename: String,
1327}
1328
1329#[derive(Debug, Clone, Serialize, Deserialize)]
1330pub struct AccountDirEntry {
1331 pub directory: String,
1333}
1334
1335#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1336pub struct _Validator {
1337 #[serde(skip_serializing_if = "Option::is_none")]
1339 pub account: Option<Vec<AccountEntry>>,
1340 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub account_dir: Option<Vec<AccountDirEntry>>,
1343 #[serde(skip_serializing_if = "Option::is_none")]
1345 pub bind_address: Option<String>,
1346 #[serde(skip_serializing_if = "Option::is_none")]
1348 pub clone: Option<Vec<CloneEntry>>,
1349 #[serde(skip_serializing_if = "Option::is_none")]
1351 pub dynamic_port_range: Option<String>,
1352 #[serde(skip_serializing_if = "Option::is_none")]
1354 pub faucet_port: Option<u16>,
1355 #[serde(skip_serializing_if = "Option::is_none")]
1357 pub faucet_sol: Option<String>,
1358 #[serde(skip_serializing_if = "Option::is_none")]
1360 pub geyser_plugin_config: Option<String>,
1361 #[serde(skip_serializing_if = "Option::is_none")]
1363 pub gossip_host: Option<String>,
1364 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub gossip_port: Option<u16>,
1367 #[serde(skip_serializing_if = "Option::is_none")]
1369 pub url: Option<String>,
1370 #[serde(skip_serializing_if = "Option::is_none")]
1372 pub ledger: Option<String>,
1373 #[serde(skip_serializing_if = "Option::is_none")]
1375 pub limit_ledger_size: Option<String>,
1376 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub rpc_port: Option<u16>,
1379 #[serde(skip_serializing_if = "Option::is_none")]
1381 pub slots_per_epoch: Option<String>,
1382 #[serde(skip_serializing_if = "Option::is_none")]
1384 pub ticks_per_slot: Option<u16>,
1385 #[serde(skip_serializing_if = "Option::is_none")]
1387 pub warp_slot: Option<Slot>,
1388 #[serde(skip_serializing_if = "Option::is_none")]
1390 pub deactivate_feature: Option<Vec<String>>,
1391}
1392
1393#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1394pub struct Validator {
1395 #[serde(skip_serializing_if = "Option::is_none")]
1396 pub account: Option<Vec<AccountEntry>>,
1397 #[serde(skip_serializing_if = "Option::is_none")]
1398 pub account_dir: Option<Vec<AccountDirEntry>>,
1399 pub bind_address: String,
1400 #[serde(skip_serializing_if = "Option::is_none")]
1401 pub clone: Option<Vec<CloneEntry>>,
1402 #[serde(skip_serializing_if = "Option::is_none")]
1403 pub dynamic_port_range: Option<String>,
1404 #[serde(skip_serializing_if = "Option::is_none")]
1405 pub faucet_port: Option<u16>,
1406 #[serde(skip_serializing_if = "Option::is_none")]
1407 pub faucet_sol: Option<String>,
1408 #[serde(skip_serializing_if = "Option::is_none")]
1409 pub geyser_plugin_config: Option<String>,
1410 #[serde(skip_serializing_if = "Option::is_none")]
1411 pub gossip_host: Option<String>,
1412 #[serde(skip_serializing_if = "Option::is_none")]
1413 pub gossip_port: Option<u16>,
1414 #[serde(skip_serializing_if = "Option::is_none")]
1415 pub url: Option<String>,
1416 pub ledger: String,
1417 #[serde(skip_serializing_if = "Option::is_none")]
1418 pub limit_ledger_size: Option<String>,
1419 pub rpc_port: u16,
1420 #[serde(skip_serializing_if = "Option::is_none")]
1421 pub slots_per_epoch: Option<String>,
1422 #[serde(skip_serializing_if = "Option::is_none")]
1423 pub ticks_per_slot: Option<u16>,
1424 #[serde(skip_serializing_if = "Option::is_none")]
1425 pub warp_slot: Option<Slot>,
1426 #[serde(skip_serializing_if = "Option::is_none")]
1427 pub deactivate_feature: Option<Vec<String>>,
1428}
1429
1430impl From<_Validator> for Validator {
1431 fn from(_validator: _Validator) -> Self {
1432 Self {
1433 account: _validator.account,
1434 account_dir: _validator.account_dir,
1435 bind_address: _validator
1436 .bind_address
1437 .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
1438 clone: _validator.clone,
1439 dynamic_port_range: _validator.dynamic_port_range,
1440 faucet_port: _validator.faucet_port,
1441 faucet_sol: _validator.faucet_sol,
1442 geyser_plugin_config: _validator.geyser_plugin_config,
1443 gossip_host: _validator.gossip_host,
1444 gossip_port: _validator.gossip_port,
1445 url: _validator.url,
1446 ledger: _validator
1447 .ledger
1448 .unwrap_or_else(|| get_default_ledger_path().display().to_string()),
1449 limit_ledger_size: _validator.limit_ledger_size,
1450 rpc_port: _validator.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1451 slots_per_epoch: _validator.slots_per_epoch,
1452 ticks_per_slot: _validator.ticks_per_slot,
1453 warp_slot: _validator.warp_slot,
1454 deactivate_feature: _validator.deactivate_feature,
1455 }
1456 }
1457}
1458
1459impl From<Validator> for _Validator {
1460 fn from(validator: Validator) -> Self {
1461 Self {
1462 account: validator.account,
1463 account_dir: validator.account_dir,
1464 bind_address: Some(validator.bind_address),
1465 clone: validator.clone,
1466 dynamic_port_range: validator.dynamic_port_range,
1467 faucet_port: validator.faucet_port,
1468 faucet_sol: validator.faucet_sol,
1469 geyser_plugin_config: validator.geyser_plugin_config,
1470 gossip_host: validator.gossip_host,
1471 gossip_port: validator.gossip_port,
1472 url: validator.url,
1473 ledger: Some(validator.ledger),
1474 limit_ledger_size: validator.limit_ledger_size,
1475 rpc_port: Some(validator.rpc_port),
1476 slots_per_epoch: validator.slots_per_epoch,
1477 ticks_per_slot: validator.ticks_per_slot,
1478 warp_slot: validator.warp_slot,
1479 deactivate_feature: validator.deactivate_feature,
1480 }
1481 }
1482}
1483
1484pub fn get_default_ledger_path() -> PathBuf {
1485 Path::new(".anchor").join("test-ledger")
1486}
1487
1488const DEFAULT_BIND_ADDRESS: &str = "127.0.0.1";
1489
1490impl Merge for _Validator {
1491 fn merge(&mut self, other: Self) {
1492 *self = Self {
1496 account: match self.account.take() {
1497 None => other.account,
1498 Some(mut entries) => match other.account {
1499 None => Some(entries),
1500 Some(other_entries) => {
1501 for other_entry in other_entries {
1502 match entries
1503 .iter()
1504 .position(|my_entry| *my_entry.address == other_entry.address)
1505 {
1506 None => entries.push(other_entry),
1507 Some(i) => entries[i] = other_entry,
1508 };
1509 }
1510 Some(entries)
1511 }
1512 },
1513 },
1514 account_dir: match self.account_dir.take() {
1515 None => other.account_dir,
1516 Some(mut entries) => match other.account_dir {
1517 None => Some(entries),
1518 Some(other_entries) => {
1519 for other_entry in other_entries {
1520 match entries
1521 .iter()
1522 .position(|my_entry| *my_entry.directory == other_entry.directory)
1523 {
1524 None => entries.push(other_entry),
1525 Some(i) => entries[i] = other_entry,
1526 };
1527 }
1528 Some(entries)
1529 }
1530 },
1531 },
1532 bind_address: other.bind_address.or_else(|| self.bind_address.take()),
1533 clone: match self.clone.take() {
1534 None => other.clone,
1535 Some(mut entries) => match other.clone {
1536 None => Some(entries),
1537 Some(other_entries) => {
1538 for other_entry in other_entries {
1539 match entries
1540 .iter()
1541 .position(|my_entry| *my_entry.address == other_entry.address)
1542 {
1543 None => entries.push(other_entry),
1544 Some(i) => entries[i] = other_entry,
1545 };
1546 }
1547 Some(entries)
1548 }
1549 },
1550 },
1551 dynamic_port_range: other
1552 .dynamic_port_range
1553 .or_else(|| self.dynamic_port_range.take()),
1554 faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
1555 faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
1556 geyser_plugin_config: other
1557 .geyser_plugin_config
1558 .or_else(|| self.geyser_plugin_config.take()),
1559 gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
1560 gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
1561 url: other.url.or_else(|| self.url.take()),
1562 ledger: other.ledger.or_else(|| self.ledger.take()),
1563 limit_ledger_size: other
1564 .limit_ledger_size
1565 .or_else(|| self.limit_ledger_size.take()),
1566 rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
1567 slots_per_epoch: other
1568 .slots_per_epoch
1569 .or_else(|| self.slots_per_epoch.take()),
1570 ticks_per_slot: other.ticks_per_slot.or_else(|| self.ticks_per_slot.take()),
1571 warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
1572 deactivate_feature: other
1573 .deactivate_feature
1574 .or_else(|| self.deactivate_feature.take()),
1575 };
1576 }
1577}
1578
1579#[derive(Debug, Clone)]
1580pub struct Program {
1581 pub lib_name: String,
1582 pub path: PathBuf,
1584 pub idl: Option<Idl>,
1585}
1586
1587impl Program {
1588 pub fn pubkey(&self) -> Result<Pubkey> {
1589 self.keypair().map(|kp| kp.pubkey())
1590 }
1591
1592 pub fn keypair(&self) -> Result<Keypair> {
1593 let file = self.keypair_file()?;
1594 get_keypair(file.path())
1595 }
1596
1597 pub fn keypair_file(&self) -> Result<WithPath<File>> {
1599 let deploy_dir_path = target_dir()?.join("deploy");
1600 fs::create_dir_all(&deploy_dir_path)
1601 .with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
1602 let path = std::env::current_dir()
1603 .expect("Must have current dir")
1604 .join(deploy_dir_path.join(format!("{}-keypair.json", self.lib_name)));
1605 if path.exists() {
1606 return Ok(WithPath::new(
1607 File::open(&path)
1608 .with_context(|| format!("Error opening file with path: {}", path.display()))?,
1609 path,
1610 ));
1611 }
1612 let program_kp = Keypair::new();
1613 let mut file = File::create(&path)
1614 .with_context(|| format!("Error creating file with path: {}", path.display()))?;
1615 file.write_all(format!("{:?}", &program_kp.to_bytes()).as_bytes())?;
1616 Ok(WithPath::new(file, path))
1617 }
1618
1619 pub fn binary_path(&self, verifiable: bool) -> Result<PathBuf> {
1620 let path = target_dir()?
1621 .join(if verifiable { "verifiable" } else { "deploy" })
1622 .join(&self.lib_name)
1623 .with_extension("so");
1624
1625 Ok(std::env::current_dir()
1626 .expect("Must have current dir")
1627 .join(path))
1628 }
1629}
1630
1631#[derive(Debug, Default)]
1632pub struct ProgramDeployment {
1633 pub address: Pubkey,
1634 pub path: Option<String>,
1635 pub idl: Option<String>,
1636}
1637
1638impl TryFrom<_ProgramDeployment> for ProgramDeployment {
1639 type Error = anyhow::Error;
1640 fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
1641 Ok(ProgramDeployment {
1642 address: pd.address.parse()?,
1643 path: pd.path,
1644 idl: pd.idl,
1645 })
1646 }
1647}
1648
1649#[derive(Debug, Default, Serialize, Deserialize)]
1650pub struct _ProgramDeployment {
1651 pub address: String,
1652 pub path: Option<String>,
1653 pub idl: Option<String>,
1654}
1655
1656impl From<&ProgramDeployment> for _ProgramDeployment {
1657 fn from(pd: &ProgramDeployment) -> Self {
1658 Self {
1659 address: pd.address.to_string(),
1660 path: pd.path.clone(),
1661 idl: pd.idl.clone(),
1662 }
1663 }
1664}
1665
1666pub struct ProgramWorkspace {
1667 pub name: String,
1668 pub program_id: Pubkey,
1669 pub idl: Idl,
1670}
1671
1672#[derive(Debug, Serialize, Deserialize)]
1673pub struct AnchorPackage {
1674 pub name: String,
1675 pub address: String,
1676 pub idl: Option<String>,
1677}
1678
1679impl AnchorPackage {
1680 pub fn from(name: String, cfg: &WithPath<Config>) -> Result<Self> {
1681 let cluster = &cfg.provider.cluster;
1682 if cluster != &Cluster::Mainnet {
1683 return Err(anyhow!("Publishing requires the mainnet cluster"));
1684 }
1685 let program_details = cfg
1686 .programs
1687 .get(cluster)
1688 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
1689 .get(&name)
1690 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
1691 let idl = program_details.idl.clone();
1692 let address = program_details.address.to_string();
1693 Ok(Self { name, address, idl })
1694 }
1695}
1696
1697#[derive(Debug, Serialize, Deserialize)]
1698#[serde(rename_all = "camelCase")]
1699pub struct SurfnetInfoResponse {
1700 pub runbook_executions: Vec<RunbookExecution>,
1701}
1702#[derive(Debug, Serialize, Deserialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct RunbookExecution {
1705 #[serde(rename = "startedAt")]
1706 pub started_at: u32,
1707 #[serde(rename = "completedAt")]
1708 pub completed_at: Option<u32>,
1709 #[serde(rename = "runbookId")]
1710 pub runbook_id: String,
1711 pub errors: Option<Vec<String>>,
1712}
1713
1714#[macro_export]
1715macro_rules! home_path {
1716 ($my_struct:ident, $path:literal) => {
1717 #[derive(Clone, Debug, AbsolutePath)]
1718 pub struct $my_struct(::std::path::PathBuf);
1719
1720 impl Default for $my_struct {
1721 fn default() -> Self {
1722 $my_struct(home_dir().unwrap().join($path))
1723 }
1724 }
1725
1726 impl $my_struct {
1727 fn stringify_with_tilde(&self) -> String {
1728 self.0
1729 .display()
1730 .to_string()
1731 .replacen(home_dir().unwrap().to_str().unwrap(), "~", 1)
1732 }
1733 }
1734
1735 impl FromStr for $my_struct {
1736 type Err = anyhow::Error;
1737
1738 fn from_str(s: &str) -> Result<Self, Self::Err> {
1739 Ok(Self(::std::path::PathBuf::from(s)))
1740 }
1741 }
1742
1743 impl fmt::Display for $my_struct {
1744 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1745 write!(f, "{}", self.0.display())
1746 }
1747 }
1748 };
1749}
1750
1751home_path!(WalletPath, ".config/solana/id.json");
1752
1753#[cfg(test)]
1754mod tests {
1755 use super::*;
1756
1757 const BASE_CONFIG: &str = "
1758 [provider]
1759 cluster = \"localnet\"
1760 wallet = \"id.json\"
1761 ";
1762
1763 #[test]
1764 fn parse_custom_cluster_str() {
1765 let config = Config::from_str(
1766 "
1767 [provider]
1768 cluster = \"http://my-url.com\"
1769 wallet = \"id.json\"
1770 ",
1771 )
1772 .unwrap();
1773 assert!(!config.features.skip_lint);
1774
1775 assert!(config
1777 .to_string()
1778 .contains(r#"cluster = "http://my-url.com""#));
1779 }
1780
1781 #[test]
1782 fn parse_custom_cluster_map() {
1783 let config = Config::from_str(
1784 "
1785 [provider]
1786 cluster = { http = \"http://my-url.com\", ws = \"ws://my-url.com\" }
1787 wallet = \"id.json\"
1788 ",
1789 )
1790 .unwrap();
1791 assert!(!config.features.skip_lint);
1792 }
1793
1794 #[test]
1795 fn parse_skip_lint_no_section() {
1796 let config = Config::from_str(BASE_CONFIG).unwrap();
1797 assert!(!config.features.skip_lint);
1798 }
1799
1800 #[test]
1801 fn parse_skip_lint_no_value() {
1802 let string = BASE_CONFIG.to_owned() + "[features]";
1803 let config = Config::from_str(&string).unwrap();
1804 assert!(!config.features.skip_lint);
1805 }
1806
1807 #[test]
1808 fn parse_skip_lint_true() {
1809 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true";
1810 let config = Config::from_str(&string).unwrap();
1811 assert!(config.features.skip_lint);
1812 }
1813
1814 #[test]
1815 fn parse_clients_section() {
1816 let toml = BASE_CONFIG.to_owned()
1817 + r#"
1818[clients]
1819auto = true
1820rust = true
1821js = false
1822js-umi = { enable = true }
1823go = { enable = true, path = "go-client" }
1824"#;
1825 let config = Config::from_str(&toml).unwrap();
1826 let clients = &config.clients;
1827 assert!(clients.auto);
1828 assert!(clients.rust.as_ref().unwrap().is_enabled());
1829 assert!(!clients.js.as_ref().unwrap().is_enabled());
1830 assert!(clients.js_umi.as_ref().unwrap().is_enabled());
1831 let go = clients.go.as_ref().unwrap();
1832 assert!(go.is_enabled());
1833 assert_eq!(go.path(), Some("go-client"));
1834
1835 let resolved = clients.enabled(Path::new("clients"));
1836 assert_eq!(
1840 resolved,
1841 vec![
1842 ("js-umi", PathBuf::from("clients/js-umi")),
1843 ("rust", PathBuf::from("clients/rust")),
1844 ("go", PathBuf::from("go-client")),
1845 ]
1846 );
1847 }
1848
1849 #[test]
1850 fn clients_section_round_trips() {
1851 let toml = BASE_CONFIG.to_owned()
1854 + r#"
1855[clients]
1856auto = true
1857rust = true
1858go = { enable = true, path = "go-client" }
1859"#;
1860 let config = Config::from_str(&toml).unwrap();
1861 let serialized = config.to_string();
1862 let reparsed = Config::from_str(&serialized).unwrap();
1863 assert!(reparsed.clients.auto);
1864 assert!(reparsed.clients.rust.as_ref().unwrap().is_enabled());
1865 assert_eq!(
1866 reparsed.clients.go.as_ref().and_then(|g| g.path()),
1867 Some("go-client"),
1868 );
1869 }
1870
1871 #[test]
1872 fn clients_section_omitted_when_default() {
1873 let config = Config::from_str(BASE_CONFIG).unwrap();
1877 assert!(!config.to_string().contains("[clients]"));
1878 }
1879
1880 #[test]
1881 fn unknown_clients_field_is_rejected() {
1882 let toml = BASE_CONFIG.to_owned() + "[clients]\npython = true\n";
1886 assert!(Config::from_str(&toml).is_err());
1887 }
1888
1889 #[test]
1890 fn parse_skip_lint_false() {
1891 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false";
1892 let config = Config::from_str(&string).unwrap();
1893 assert!(!config.features.skip_lint);
1894 }
1895
1896 #[test]
1897 fn test_toml_resolves_account_dir_relative_to_file() {
1898 let dir = tempfile::tempdir().unwrap();
1899 let suite_dir = dir.path().join("tests").join("suite");
1900 let accounts_dir = suite_dir.join("accounts");
1901 fs::create_dir_all(&accounts_dir).unwrap();
1902
1903 let account_file = accounts_dir.join("account.json");
1904 fs::write(&account_file, "{}").unwrap();
1905
1906 let test_toml = suite_dir.join("Test.toml");
1907 fs::write(
1908 &test_toml,
1909 r#"
1910[scripts]
1911test = "true"
1912
1913[[test.validator.account]]
1914address = "3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj"
1915filename = "accounts/account.json"
1916
1917[[test.validator.account_dir]]
1918directory = "accounts"
1919"#,
1920 )
1921 .unwrap();
1922
1923 let parsed = TestToml::from_path(test_toml).unwrap();
1924 let validator = parsed.test.unwrap().validator.unwrap();
1925
1926 assert_eq!(
1927 validator.account.unwrap()[0].filename,
1928 account_file.canonicalize().unwrap().display().to_string()
1929 );
1930 assert_eq!(
1931 validator.account_dir.unwrap()[0].directory,
1932 accounts_dir.canonicalize().unwrap().display().to_string()
1933 );
1934 }
1935}