1use {
2 crate::{get_keypair, is_hidden, keys_sync, DEFAULT_RPC_PORT},
3 anchor_client::Cluster,
4 anchor_lang_idl::types::Idl,
5 anyhow::{anyhow, bail, Context, Error, Result},
6 clap::{Parser, ValueEnum},
7 dirs::home_dir,
8 heck::ToSnakeCase,
9 reqwest::Url,
10 serde::{
11 de::{self, MapAccess, Visitor},
12 ser::SerializeMap,
13 Deserialize, Deserializer, Serialize, Serializer,
14 },
15 solana_cli_config::{Config as SolanaConfig, CONFIG_FILE},
16 solana_clock::Slot,
17 solana_commitment_config::CommitmentLevel,
18 solana_keypair::Keypair,
19 solana_pubkey::Pubkey,
20 solana_signer::Signer,
21 std::{
22 collections::{BTreeMap, HashMap},
23 convert::TryFrom,
24 fmt,
25 fs::{self, File},
26 io::{self, prelude::*},
27 marker::PhantomData,
28 ops::Deref,
29 path::{Path, PathBuf},
30 process::Command,
31 str::FromStr,
32 },
33 walkdir::WalkDir,
34};
35
36pub const SURFPOOL_HOST: &str = "127.0.0.1";
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct CaseInsensitiveCommitmentLevel(pub CommitmentLevel);
40
41impl FromStr for CaseInsensitiveCommitmentLevel {
42 type Err = String;
43
44 fn from_str(s: &str) -> Result<Self, Self::Err> {
45 let lowercase = s.to_lowercase();
47 let commitment = CommitmentLevel::from_str(&lowercase).map_err(|_| {
48 format!(
49 "Invalid commitment level '{}'. Valid values are: processed, confirmed, finalized",
50 s
51 )
52 })?;
53 Ok(CaseInsensitiveCommitmentLevel(commitment))
54 }
55}
56
57impl From<CaseInsensitiveCommitmentLevel> for CommitmentLevel {
58 fn from(val: CaseInsensitiveCommitmentLevel) -> Self {
59 val.0
60 }
61}
62
63pub trait Merge: Sized {
64 fn merge(&mut self, _other: Self) {}
65}
66
67#[derive(Default, Debug, Parser)]
68pub struct ConfigOverride {
69 #[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 = Path::new("target")
228 .join("idl")
229 .join(&lib_name)
230 .with_extension("json");
231 let idl = fs::read(idl_filepath)
232 .ok()
233 .map(|bytes| serde_json::from_reader(&*bytes))
234 .transpose()?;
235
236 r.push(Program {
237 lib_name,
238 path,
239 idl,
240 });
241 }
242 Ok(r)
243 }
244
245 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<T> std::ops::Deref for WithPath<T> {
312 type Target = T;
313 fn deref(&self) -> &Self::Target {
314 &self.inner
315 }
316}
317
318impl<T> std::ops::DerefMut for WithPath<T> {
319 fn deref_mut(&mut self) -> &mut Self::Target {
320 &mut self.inner
321 }
322}
323
324#[derive(Debug, Default)]
325pub struct Config {
326 pub toolchain: ToolchainConfig,
327 pub features: FeaturesConfig,
328 pub provider: ProviderConfig,
329 pub programs: ProgramsConfig,
330 pub scripts: ScriptsConfig,
331 pub hooks: HooksConfig,
332 pub workspace: WorkspaceConfig,
333 pub validator: Option<ValidatorType>,
337 pub test_validator: Option<TestValidator>,
338 pub test_config: Option<TestConfig>,
339 pub surfpool_config: Option<SurfpoolConfig>,
340}
341
342#[derive(ValueEnum, Parser, Clone, Copy, PartialEq, Eq, Debug)]
343pub enum ValidatorType {
344 Surfpool,
346 Legacy,
348}
349#[derive(Default, Clone, Debug, Serialize, Deserialize)]
350pub struct ToolchainConfig {
351 pub anchor_version: Option<String>,
352 pub solana_version: Option<String>,
353 pub package_manager: Option<PackageManager>,
354}
355
356#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize)]
358#[serde(rename_all = "lowercase")]
359pub enum PackageManager {
360 NPM,
362 #[default]
364 Yarn,
365 PNPM,
367 Bun,
369}
370
371impl std::fmt::Display for PackageManager {
372 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
373 let pkg_manager_str = match self {
374 PackageManager::NPM => "npm",
375 PackageManager::Yarn => "yarn",
376 PackageManager::PNPM => "pnpm",
377 PackageManager::Bun => "bun",
378 };
379
380 write!(f, "{pkg_manager_str}")
381 }
382}
383
384#[derive(Clone, Debug, Serialize, Deserialize)]
385pub struct FeaturesConfig {
386 #[serde(default = "FeaturesConfig::get_default_resolution")]
390 pub resolution: bool,
391 #[serde(default, rename = "skip-lint")]
393 pub skip_lint: bool,
394}
395
396impl FeaturesConfig {
397 fn get_default_resolution() -> bool {
398 true
399 }
400}
401
402impl Default for FeaturesConfig {
403 fn default() -> Self {
404 Self {
405 resolution: Self::get_default_resolution(),
406 skip_lint: false,
407 }
408 }
409}
410
411#[derive(Debug, Default)]
412pub struct ProviderConfig {
413 pub cluster: Cluster,
414 pub wallet: WalletPath,
415}
416
417pub type ScriptsConfig = BTreeMap<String, String>;
418
419pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
420
421#[derive(Default, Clone, Debug, Serialize, Deserialize)]
422#[serde(deny_unknown_fields)]
423pub struct HooksConfig {
424 #[serde(alias = "pre-build")]
425 pre_build: Option<Hook>,
426 #[serde(alias = "post-build")]
427 post_build: Option<Hook>,
428 #[serde(alias = "pre-test")]
429 pre_test: Option<Hook>,
430 #[serde(alias = "post-test")]
431 post_test: Option<Hook>,
432 #[serde(alias = "pre-deploy")]
433 pre_deploy: Option<Hook>,
434 #[serde(alias = "post-deploy")]
435 post_deploy: Option<Hook>,
436}
437
438#[derive(Clone, Debug, Serialize, Deserialize)]
439#[serde(untagged)]
440enum Hook {
441 Single(String),
442 List(Vec<String>),
443}
444
445impl Hook {
446 pub fn hooks(&self) -> &[String] {
447 match self {
448 Self::Single(h) => std::slice::from_ref(h),
449 Self::List(l) => l.as_slice(),
450 }
451 }
452}
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq)]
455pub enum HookType {
456 PreBuild,
457 PostBuild,
458 PreTest,
459 PostTest,
460 PreDeploy,
461 PostDeploy,
462}
463
464#[derive(Debug, Default, Clone, Serialize, Deserialize)]
465pub struct WorkspaceConfig {
466 #[serde(default, skip_serializing_if = "Vec::is_empty")]
467 pub members: Vec<String>,
468 #[serde(default, skip_serializing_if = "Vec::is_empty")]
469 pub exclude: Vec<String>,
470 #[serde(default, skip_serializing_if = "String::is_empty")]
471 pub types: String,
472}
473
474#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug)]
475pub enum BootstrapMode {
476 None,
477 Debian,
478}
479
480#[derive(Debug, Clone)]
481pub struct BuildConfig {
482 pub verifiable: bool,
483 pub solana_version: Option<String>,
484 pub docker_image: String,
485 pub bootstrap: BootstrapMode,
486}
487
488impl Config {
489 pub fn add_test_config(
490 &mut self,
491 root: impl AsRef<Path>,
492 test_paths: Vec<PathBuf>,
493 ) -> Result<()> {
494 self.test_config = TestConfig::discover(root, test_paths)?;
495 Ok(())
496 }
497
498 pub fn docker(&self) -> String {
499 let version = self
500 .toolchain
501 .anchor_version
502 .as_deref()
503 .unwrap_or(crate::DOCKER_BUILDER_VERSION);
504 format!("solanafoundation/anchor:v{version}")
505 }
506
507 pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
508 Config::_discover().map(|opt| {
509 opt.map(|mut cfg| {
510 if let Some(cluster) = cfg_override.cluster.clone() {
511 cfg.provider.cluster = cluster;
512 }
513 if let Some(wallet) = cfg_override.wallet.clone() {
514 cfg.provider.wallet = wallet;
515 }
516 cfg
517 })
518 })
519 }
520
521 fn _discover() -> Result<Option<WithPath<Config>>> {
523 let _cwd = std::env::current_dir()?;
524 let mut cwd_opt = Some(_cwd.as_path());
525
526 while let Some(cwd) = cwd_opt {
527 for f in fs::read_dir(cwd).with_context(|| {
528 format!("Error reading the directory with path: {}", cwd.display())
529 })? {
530 let p = f
531 .with_context(|| {
532 format!("Error reading the directory with path: {}", cwd.display())
533 })?
534 .path();
535 if let Some(filename) = p.file_name() {
536 if filename.to_str() == Some("Anchor.toml") {
537 let mut cfg = Config::from_path(&p)?;
539 let deploy_dir = p.parent().unwrap().join("target").join("deploy");
540 if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
541 println!("Updating program ids...");
542 fs::create_dir_all(deploy_dir)?;
543 keys_sync(&ConfigOverride::default(), None)?;
544 cfg = Config::from_path(&p)?;
545 }
546
547 return Ok(Some(WithPath::new(cfg, p)));
548 }
549 }
550 }
551
552 cwd_opt = cwd.parent();
553 }
554
555 Ok(None)
556 }
557
558 fn from_path(p: impl AsRef<Path>) -> Result<Self> {
559 fs::read_to_string(&p)
560 .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))?
561 .parse::<Self>()
562 }
563
564 pub fn wallet_kp(&self) -> Result<Keypair> {
565 get_keypair(&self.provider.wallet.to_string())
566 }
567
568 pub fn run_hooks(&self, hook_type: HookType) -> Result<()> {
569 let hooks = match hook_type {
570 HookType::PreBuild => &self.hooks.pre_build,
571 HookType::PostBuild => &self.hooks.post_build,
572 HookType::PreTest => &self.hooks.pre_test,
573 HookType::PostTest => &self.hooks.post_test,
574 HookType::PreDeploy => &self.hooks.pre_deploy,
575 HookType::PostDeploy => &self.hooks.post_deploy,
576 };
577 let cmds = hooks.as_ref().map(Hook::hooks).unwrap_or_default();
578 for cmd in cmds {
579 let status = Command::new("bash")
580 .arg("-c")
581 .arg(cmd)
582 .status()
583 .with_context(|| format!("failed to execute `{cmd}`"))?;
584 if !status.success() {
585 match status.code() {
586 Some(code) => bail!("`{cmd}` failed with exit code {code}"),
587 None => bail!("`{cmd}` killed by signal"),
588 }
589 }
590 }
591 Ok(())
592 }
593}
594
595#[derive(Debug, Serialize, Deserialize)]
596struct _Config {
597 toolchain: Option<ToolchainConfig>,
598 features: Option<FeaturesConfig>,
599 programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
600 provider: Provider,
601 workspace: Option<WorkspaceConfig>,
602 scripts: Option<ScriptsConfig>,
603 hooks: Option<HooksConfig>,
604 test: Option<_TestValidator>,
605 surfpool: Option<_SurfpoolConfig>,
606}
607
608#[derive(Debug, Serialize, Deserialize)]
609struct Provider {
610 #[serde(serialize_with = "ser_cluster", deserialize_with = "des_cluster")]
611 cluster: Cluster,
612 wallet: String,
613}
614
615fn ser_cluster<S: Serializer>(cluster: &Cluster, s: S) -> Result<S::Ok, S::Error> {
616 match cluster {
617 Cluster::Custom(http, ws) => {
618 match (Url::parse(http), Url::parse(ws)) {
619 (Ok(h), Ok(w)) if h.domain() == w.domain() => s.serialize_str(http),
621 _ => {
622 let mut map = s.serialize_map(Some(2))?;
623 map.serialize_entry("http", http)?;
624 map.serialize_entry("ws", ws)?;
625 map.end()
626 }
627 }
628 }
629 _ => s.serialize_str(&cluster.to_string()),
630 }
631}
632
633fn des_cluster<'de, D>(deserializer: D) -> Result<Cluster, D::Error>
634where
635 D: Deserializer<'de>,
636{
637 struct StringOrCustomCluster(PhantomData<fn() -> Cluster>);
638
639 impl<'de> Visitor<'de> for StringOrCustomCluster {
640 type Value = Cluster;
641
642 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
643 formatter.write_str("string or map")
644 }
645
646 fn visit_str<E>(self, value: &str) -> Result<Cluster, E>
647 where
648 E: de::Error,
649 {
650 value.parse().map_err(de::Error::custom)
651 }
652
653 fn visit_map<M>(self, mut map: M) -> Result<Cluster, M::Error>
654 where
655 M: MapAccess<'de>,
656 {
657 if let (Some((http_key, http_value)), Some((ws_key, ws_value))) = (
659 map.next_entry::<String, String>()?,
660 map.next_entry::<String, String>()?,
661 ) {
662 if http_key != "http" || ws_key != "ws" {
664 return Err(de::Error::custom("Invalid key"));
665 }
666
667 Url::parse(&http_value).map_err(de::Error::custom)?;
669 Url::parse(&ws_value).map_err(de::Error::custom)?;
670
671 Ok(Cluster::Custom(http_value, ws_value))
672 } else {
673 Err(de::Error::custom("Invalid entry"))
674 }
675 }
676 }
677 deserializer.deserialize_any(StringOrCustomCluster(PhantomData))
678}
679
680impl fmt::Display for Config {
681 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682 let programs = {
683 let c = ser_programs(&self.programs);
684 if c.is_empty() {
685 None
686 } else {
687 Some(c)
688 }
689 };
690 let cfg = _Config {
691 toolchain: Some(self.toolchain.clone()),
692 features: Some(self.features.clone()),
693 provider: Provider {
694 cluster: self.provider.cluster.clone(),
695 wallet: self.provider.wallet.stringify_with_tilde(),
696 },
697 test: self.test_validator.clone().map(Into::into),
698 scripts: match self.scripts.is_empty() {
699 true => None,
700 false => Some(self.scripts.clone()),
701 },
702 hooks: Some(self.hooks.clone()),
703 programs,
704 workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
705 .then(|| self.workspace.clone()),
706 surfpool: self.surfpool_config.clone().map(Into::into),
707 };
708
709 let cfg = toml::to_string(&cfg).expect("Must be well formed");
710 write!(f, "{cfg}")
711 }
712}
713
714impl FromStr for Config {
715 type Err = Error;
716
717 fn from_str(s: &str) -> Result<Self, Self::Err> {
718 let cfg: _Config =
719 toml::from_str(s).map_err(|e| anyhow!("Unable to deserialize config: {e}"))?;
720 Ok(Config {
721 toolchain: cfg.toolchain.unwrap_or_default(),
722 features: cfg.features.unwrap_or_default(),
723 provider: ProviderConfig {
724 cluster: cfg.provider.cluster,
725 wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
726 },
727 scripts: cfg.scripts.unwrap_or_default(),
728 hooks: cfg.hooks.unwrap_or_default(),
729 validator: None, test_validator: cfg.test.map(Into::into),
731 test_config: None,
732 programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
733 workspace: cfg.workspace.unwrap_or_default(),
734 surfpool_config: cfg.surfpool.map(Into::into),
735 })
736 }
737}
738
739pub fn get_solana_cfg_url() -> Result<String, io::Error> {
740 let config_file = CONFIG_FILE.as_ref().ok_or_else(|| {
741 io::Error::new(
742 io::ErrorKind::NotFound,
743 "Default Solana config was not found",
744 )
745 })?;
746 SolanaConfig::load(config_file).map(|config| config.json_rpc_url)
747}
748
749fn ser_programs(
750 programs: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
751) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
752 programs
753 .iter()
754 .map(|(cluster, programs)| {
755 let cluster = cluster.to_string();
756 let programs = programs
757 .iter()
758 .map(|(name, deployment)| {
759 (
760 name.clone(),
761 to_value(&_ProgramDeployment::from(deployment)),
762 )
763 })
764 .collect::<BTreeMap<String, serde_json::Value>>();
765 (cluster, programs)
766 })
767 .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
768}
769
770fn to_value(dep: &_ProgramDeployment) -> serde_json::Value {
771 if dep.path.is_none() && dep.idl.is_none() {
772 return serde_json::Value::String(dep.address.to_string());
773 }
774 serde_json::to_value(dep).unwrap()
775}
776
777fn deser_programs(
778 programs: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
779) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
780 programs
781 .iter()
782 .map(|(cluster, programs)| {
783 let cluster: Cluster = cluster.parse()?;
784 let programs = programs
785 .iter()
786 .map(|(name, program_id)| {
787 Ok((
788 name.clone(),
789 ProgramDeployment::try_from(match &program_id {
790 serde_json::Value::String(address) => _ProgramDeployment {
791 address: address.parse()?,
792 path: None,
793 idl: None,
794 },
795
796 serde_json::Value::Object(_) => {
797 serde_json::from_value(program_id.clone())
798 .map_err(|_| anyhow!("Unable to read toml"))?
799 }
800 _ => return Err(anyhow!("Invalid toml type")),
801 })?,
802 ))
803 })
804 .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
805 Ok((cluster, programs))
806 })
807 .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
808}
809
810#[derive(Default, Debug, Clone, Serialize, Deserialize)]
811pub struct TestValidator {
812 pub genesis: Option<Vec<GenesisEntry>>,
813 pub validator: Option<Validator>,
814 pub startup_wait: i32,
815 pub shutdown_wait: i32,
816 pub upgradeable: bool,
817}
818
819#[derive(Default, Debug, Clone, Serialize, Deserialize)]
820pub struct SurfpoolConfig {
821 pub startup_wait: i32,
822 pub shutdown_wait: i32,
823 pub rpc_port: u16,
824 pub ws_port: Option<u16>,
825 pub host: String,
826 pub online: Option<bool>,
827 pub datasource_rpc_url: Option<String>,
828 pub airdrop_addresses: Option<Vec<String>>,
829 pub manifest_file_path: Option<String>,
830 pub runbooks: Option<Vec<String>>,
831 pub slot_time: Option<u16>,
832 pub log_level: Option<String>,
833 pub block_production_mode: Option<String>,
834}
835
836#[derive(Default, Debug, Clone, Serialize, Deserialize)]
837pub struct _TestValidator {
838 #[serde(skip_serializing_if = "Option::is_none")]
839 pub genesis: Option<Vec<GenesisEntry>>,
840 #[serde(skip_serializing_if = "Option::is_none")]
841 pub validator: Option<_Validator>,
842 #[serde(skip_serializing_if = "Option::is_none")]
843 pub startup_wait: Option<i32>,
844 #[serde(skip_serializing_if = "Option::is_none")]
845 pub shutdown_wait: Option<i32>,
846 #[serde(skip_serializing_if = "Option::is_none")]
847 pub upgradeable: Option<bool>,
848}
849
850#[derive(Default, Debug, Clone, Serialize, Deserialize)]
851pub struct _SurfpoolConfig {
852 #[serde(skip_serializing_if = "Option::is_none")]
853 pub startup_wait: Option<i32>,
854 #[serde(skip_serializing_if = "Option::is_none")]
855 pub shutdown_wait: Option<i32>,
856 #[serde(skip_serializing_if = "Option::is_none")]
857 pub rpc_port: Option<u16>,
858 #[serde(skip_serializing_if = "Option::is_none")]
859 pub ws_port: Option<u16>,
860 #[serde(skip_serializing_if = "Option::is_none")]
861 pub host: Option<String>,
862 #[serde(skip_serializing_if = "Option::is_none")]
863 pub online: Option<bool>,
864 #[serde(skip_serializing_if = "Option::is_none")]
865 pub datasource_rpc_url: Option<String>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 pub airdrop_addresses: Option<Vec<String>>,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 pub manifest_file_path: Option<String>,
870 #[serde(skip_serializing_if = "Option::is_none")]
871 pub runbooks: Option<Vec<String>>,
872 #[serde(skip_serializing_if = "Option::is_none")]
873 pub slot_time: Option<u16>,
874 #[serde(skip_serializing_if = "Option::is_none")]
875 pub log_level: Option<String>,
876 #[serde(skip_serializing_if = "Option::is_none")]
877 pub block_production_mode: Option<String>,
878}
879
880impl From<_SurfpoolConfig> for SurfpoolConfig {
881 fn from(_surfpool_config: _SurfpoolConfig) -> Self {
882 Self {
883 startup_wait: _surfpool_config.startup_wait.unwrap_or(STARTUP_WAIT),
884 shutdown_wait: _surfpool_config.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
885 rpc_port: _surfpool_config.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
886 host: _surfpool_config.host.unwrap_or(SURFPOOL_HOST.to_string()),
887 ws_port: _surfpool_config.ws_port,
888 online: _surfpool_config.online,
889 datasource_rpc_url: _surfpool_config.datasource_rpc_url,
890 airdrop_addresses: _surfpool_config.airdrop_addresses,
891 manifest_file_path: _surfpool_config.manifest_file_path,
892 runbooks: _surfpool_config.runbooks,
893 slot_time: _surfpool_config.slot_time,
894 log_level: _surfpool_config.log_level,
895 block_production_mode: _surfpool_config.block_production_mode,
896 }
897 }
898}
899
900impl From<SurfpoolConfig> for _SurfpoolConfig {
901 fn from(surfpool_config: SurfpoolConfig) -> Self {
902 Self {
903 startup_wait: Some(surfpool_config.startup_wait),
904 shutdown_wait: Some(surfpool_config.shutdown_wait),
905 rpc_port: Some(surfpool_config.rpc_port),
906 ws_port: surfpool_config.ws_port,
907 host: Some(surfpool_config.host),
908 online: surfpool_config.online,
909 datasource_rpc_url: surfpool_config.datasource_rpc_url,
910 airdrop_addresses: surfpool_config.airdrop_addresses,
911 manifest_file_path: surfpool_config.manifest_file_path,
912 runbooks: surfpool_config.runbooks,
913 slot_time: surfpool_config.slot_time,
914 log_level: surfpool_config.log_level,
915 block_production_mode: surfpool_config.block_production_mode,
916 }
917 }
918}
919pub const STARTUP_WAIT: i32 = 5000;
920pub const SHUTDOWN_WAIT: i32 = 2000;
921
922impl From<_TestValidator> for TestValidator {
923 fn from(_test_validator: _TestValidator) -> Self {
924 Self {
925 shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
926 startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
927 genesis: _test_validator.genesis,
928 validator: _test_validator.validator.map(Into::into),
929 upgradeable: _test_validator.upgradeable.unwrap_or(false),
930 }
931 }
932}
933
934impl From<TestValidator> for _TestValidator {
935 fn from(test_validator: TestValidator) -> Self {
936 Self {
937 shutdown_wait: Some(test_validator.shutdown_wait),
938 startup_wait: Some(test_validator.startup_wait),
939 genesis: test_validator.genesis,
940 validator: test_validator.validator.map(Into::into),
941 upgradeable: Some(test_validator.upgradeable),
942 }
943 }
944}
945
946#[derive(Debug, Clone)]
947pub struct TestConfig {
948 pub test_suite_configs: HashMap<PathBuf, TestToml>,
949}
950
951impl Deref for TestConfig {
952 type Target = HashMap<PathBuf, TestToml>;
953
954 fn deref(&self) -> &Self::Target {
955 &self.test_suite_configs
956 }
957}
958
959impl TestConfig {
960 pub fn discover(root: impl AsRef<Path>, test_paths: Vec<PathBuf>) -> Result<Option<Self>> {
961 let walker = WalkDir::new(root).into_iter();
962 let mut test_suite_configs = HashMap::new();
963 for entry in walker.filter_entry(|e| !is_hidden(e)) {
964 let entry = entry?;
965 if entry.file_name() == "Test.toml" {
966 let entry_path = entry.path();
967 let test_toml = TestToml::from_path(entry_path)?;
968 if test_paths.is_empty() || test_paths.iter().any(|p| entry_path.starts_with(p)) {
969 test_suite_configs.insert(entry.path().into(), test_toml);
970 }
971 }
972 }
973
974 Ok(match test_suite_configs.is_empty() {
975 true => None,
976 false => Some(Self { test_suite_configs }),
977 })
978 }
979}
980
981#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct _TestToml {
985 pub extends: Option<Vec<String>>,
986 pub test: Option<_TestValidator>,
987 pub scripts: Option<ScriptsConfig>,
988}
989
990impl _TestToml {
991 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
992 let s = fs::read_to_string(&path)?;
993 let parsed_toml: Self = toml::from_str(&s)?;
994 let mut current_toml = _TestToml {
995 extends: None,
996 test: None,
997 scripts: None,
998 };
999 if let Some(bases) = &parsed_toml.extends {
1000 for base in bases {
1001 let mut canonical_base = base.clone();
1002 canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
1003 current_toml.merge(_TestToml::from_path(&canonical_base)?);
1004 }
1005 }
1006 current_toml.merge(parsed_toml);
1007
1008 if let Some(test) = &mut current_toml.test {
1009 if let Some(genesis_programs) = &mut test.genesis {
1010 for entry in genesis_programs {
1011 entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
1012 }
1013 }
1014 if let Some(validator) = &mut test.validator {
1015 if let Some(ledger_dir) = &mut validator.ledger {
1016 *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?;
1017 }
1018 if let Some(accounts) = &mut validator.account {
1019 for entry in accounts {
1020 entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
1021 }
1022 }
1023 }
1024 }
1025 Ok(current_toml)
1026 }
1027}
1028
1029fn canonicalize_filepath_from_origin(
1034 file_path: impl AsRef<Path>,
1035 origin: impl AsRef<Path>,
1036) -> Result<String> {
1037 let previous_dir = std::env::current_dir()?;
1038 std::env::set_current_dir(origin.as_ref().parent().unwrap())?;
1039 let result = fs::canonicalize(&file_path)
1040 .with_context(|| {
1041 format!(
1042 "Error reading (possibly relative) path: {}. If relative, this is the path that \
1043 was used as the current path: {}",
1044 &file_path.as_ref().display(),
1045 &origin.as_ref().display()
1046 )
1047 })?
1048 .display()
1049 .to_string();
1050 std::env::set_current_dir(previous_dir)?;
1051 Ok(result)
1052}
1053
1054#[derive(Debug, Clone, Serialize, Deserialize)]
1055pub struct TestToml {
1056 #[serde(skip_serializing_if = "Option::is_none")]
1057 pub test: Option<TestValidator>,
1058 pub scripts: ScriptsConfig,
1059}
1060
1061impl TestToml {
1062 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
1063 WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
1064 }
1065}
1066
1067impl Merge for _TestToml {
1068 fn merge(&mut self, other: Self) {
1069 let mut my_scripts = self.scripts.take();
1070 match &mut my_scripts {
1071 None => my_scripts = other.scripts,
1072 Some(my_scripts) => {
1073 if let Some(other_scripts) = other.scripts {
1074 for (name, script) in other_scripts {
1075 my_scripts.insert(name, script);
1076 }
1077 }
1078 }
1079 }
1080
1081 let mut my_test = self.test.take();
1082 match &mut my_test {
1083 Some(my_test) => {
1084 if let Some(other_test) = other.test {
1085 if let Some(startup_wait) = other_test.startup_wait {
1086 my_test.startup_wait = Some(startup_wait);
1087 }
1088 if let Some(other_genesis) = other_test.genesis {
1089 match &mut my_test.genesis {
1090 Some(my_genesis) => {
1091 for other_entry in other_genesis {
1092 match my_genesis
1093 .iter()
1094 .position(|g| *g.address == other_entry.address)
1095 {
1096 None => my_genesis.push(other_entry),
1097 Some(i) => my_genesis[i] = other_entry,
1098 }
1099 }
1100 }
1101 None => my_test.genesis = Some(other_genesis),
1102 }
1103 }
1104 let mut my_validator = my_test.validator.take();
1105 match &mut my_validator {
1106 None => my_validator = other_test.validator,
1107 Some(my_validator) => {
1108 if let Some(other_validator) = other_test.validator {
1109 my_validator.merge(other_validator)
1110 }
1111 }
1112 }
1113
1114 my_test.validator = my_validator;
1115 }
1116 }
1117 None => my_test = other.test,
1118 };
1119
1120 *self = Self {
1124 test: my_test,
1125 scripts: my_scripts,
1126 extends: self.extends.take(),
1127 };
1128 }
1129}
1130
1131impl TryFrom<WithPath<_TestToml>> for TestToml {
1132 type Error = Error;
1133
1134 fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
1135 Ok(Self {
1136 test: value.test.take().map(Into::into),
1137 scripts: value
1138 .scripts
1139 .take()
1140 .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
1141 })
1142 }
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146pub struct GenesisEntry {
1147 pub address: String,
1149 pub program: String,
1151 pub upgradeable: Option<bool>,
1153}
1154
1155#[derive(Debug, Clone, Serialize, Deserialize)]
1156pub struct CloneEntry {
1157 pub address: String,
1159}
1160
1161#[derive(Debug, Clone, Serialize, Deserialize)]
1162pub struct AccountEntry {
1163 pub address: String,
1165 pub filename: String,
1167}
1168
1169#[derive(Debug, Clone, Serialize, Deserialize)]
1170pub struct AccountDirEntry {
1171 pub directory: String,
1173}
1174
1175#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1176pub struct _Validator {
1177 #[serde(skip_serializing_if = "Option::is_none")]
1179 pub account: Option<Vec<AccountEntry>>,
1180 #[serde(skip_serializing_if = "Option::is_none")]
1182 pub account_dir: Option<Vec<AccountDirEntry>>,
1183 #[serde(skip_serializing_if = "Option::is_none")]
1185 pub bind_address: Option<String>,
1186 #[serde(skip_serializing_if = "Option::is_none")]
1188 pub clone: Option<Vec<CloneEntry>>,
1189 #[serde(skip_serializing_if = "Option::is_none")]
1191 pub dynamic_port_range: Option<String>,
1192 #[serde(skip_serializing_if = "Option::is_none")]
1194 pub faucet_port: Option<u16>,
1195 #[serde(skip_serializing_if = "Option::is_none")]
1197 pub faucet_sol: Option<String>,
1198 #[serde(skip_serializing_if = "Option::is_none")]
1200 pub geyser_plugin_config: Option<String>,
1201 #[serde(skip_serializing_if = "Option::is_none")]
1203 pub gossip_host: Option<String>,
1204 #[serde(skip_serializing_if = "Option::is_none")]
1206 pub gossip_port: Option<u16>,
1207 #[serde(skip_serializing_if = "Option::is_none")]
1209 pub url: Option<String>,
1210 #[serde(skip_serializing_if = "Option::is_none")]
1212 pub ledger: Option<String>,
1213 #[serde(skip_serializing_if = "Option::is_none")]
1215 pub limit_ledger_size: Option<String>,
1216 #[serde(skip_serializing_if = "Option::is_none")]
1218 pub rpc_port: Option<u16>,
1219 #[serde(skip_serializing_if = "Option::is_none")]
1221 pub slots_per_epoch: Option<String>,
1222 #[serde(skip_serializing_if = "Option::is_none")]
1224 pub ticks_per_slot: Option<u16>,
1225 #[serde(skip_serializing_if = "Option::is_none")]
1227 pub warp_slot: Option<Slot>,
1228 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub deactivate_feature: Option<Vec<String>>,
1231}
1232
1233#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1234pub struct Validator {
1235 #[serde(skip_serializing_if = "Option::is_none")]
1236 pub account: Option<Vec<AccountEntry>>,
1237 #[serde(skip_serializing_if = "Option::is_none")]
1238 pub account_dir: Option<Vec<AccountDirEntry>>,
1239 pub bind_address: String,
1240 #[serde(skip_serializing_if = "Option::is_none")]
1241 pub clone: Option<Vec<CloneEntry>>,
1242 #[serde(skip_serializing_if = "Option::is_none")]
1243 pub dynamic_port_range: Option<String>,
1244 #[serde(skip_serializing_if = "Option::is_none")]
1245 pub faucet_port: Option<u16>,
1246 #[serde(skip_serializing_if = "Option::is_none")]
1247 pub faucet_sol: Option<String>,
1248 #[serde(skip_serializing_if = "Option::is_none")]
1249 pub geyser_plugin_config: Option<String>,
1250 #[serde(skip_serializing_if = "Option::is_none")]
1251 pub gossip_host: Option<String>,
1252 #[serde(skip_serializing_if = "Option::is_none")]
1253 pub gossip_port: Option<u16>,
1254 #[serde(skip_serializing_if = "Option::is_none")]
1255 pub url: Option<String>,
1256 pub ledger: String,
1257 #[serde(skip_serializing_if = "Option::is_none")]
1258 pub limit_ledger_size: Option<String>,
1259 pub rpc_port: u16,
1260 #[serde(skip_serializing_if = "Option::is_none")]
1261 pub slots_per_epoch: Option<String>,
1262 #[serde(skip_serializing_if = "Option::is_none")]
1263 pub ticks_per_slot: Option<u16>,
1264 #[serde(skip_serializing_if = "Option::is_none")]
1265 pub warp_slot: Option<Slot>,
1266 #[serde(skip_serializing_if = "Option::is_none")]
1267 pub deactivate_feature: Option<Vec<String>>,
1268}
1269
1270impl From<_Validator> for Validator {
1271 fn from(_validator: _Validator) -> Self {
1272 Self {
1273 account: _validator.account,
1274 account_dir: _validator.account_dir,
1275 bind_address: _validator
1276 .bind_address
1277 .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
1278 clone: _validator.clone,
1279 dynamic_port_range: _validator.dynamic_port_range,
1280 faucet_port: _validator.faucet_port,
1281 faucet_sol: _validator.faucet_sol,
1282 geyser_plugin_config: _validator.geyser_plugin_config,
1283 gossip_host: _validator.gossip_host,
1284 gossip_port: _validator.gossip_port,
1285 url: _validator.url,
1286 ledger: _validator
1287 .ledger
1288 .unwrap_or_else(|| get_default_ledger_path().display().to_string()),
1289 limit_ledger_size: _validator.limit_ledger_size,
1290 rpc_port: _validator.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
1291 slots_per_epoch: _validator.slots_per_epoch,
1292 ticks_per_slot: _validator.ticks_per_slot,
1293 warp_slot: _validator.warp_slot,
1294 deactivate_feature: _validator.deactivate_feature,
1295 }
1296 }
1297}
1298
1299impl From<Validator> for _Validator {
1300 fn from(validator: Validator) -> Self {
1301 Self {
1302 account: validator.account,
1303 account_dir: validator.account_dir,
1304 bind_address: Some(validator.bind_address),
1305 clone: validator.clone,
1306 dynamic_port_range: validator.dynamic_port_range,
1307 faucet_port: validator.faucet_port,
1308 faucet_sol: validator.faucet_sol,
1309 geyser_plugin_config: validator.geyser_plugin_config,
1310 gossip_host: validator.gossip_host,
1311 gossip_port: validator.gossip_port,
1312 url: validator.url,
1313 ledger: Some(validator.ledger),
1314 limit_ledger_size: validator.limit_ledger_size,
1315 rpc_port: Some(validator.rpc_port),
1316 slots_per_epoch: validator.slots_per_epoch,
1317 ticks_per_slot: validator.ticks_per_slot,
1318 warp_slot: validator.warp_slot,
1319 deactivate_feature: validator.deactivate_feature,
1320 }
1321 }
1322}
1323
1324pub fn get_default_ledger_path() -> PathBuf {
1325 Path::new(".anchor").join("test-ledger")
1326}
1327
1328const DEFAULT_BIND_ADDRESS: &str = "127.0.0.1";
1329
1330impl Merge for _Validator {
1331 fn merge(&mut self, other: Self) {
1332 *self = Self {
1336 account: match self.account.take() {
1337 None => other.account,
1338 Some(mut entries) => match other.account {
1339 None => Some(entries),
1340 Some(other_entries) => {
1341 for other_entry in other_entries {
1342 match entries
1343 .iter()
1344 .position(|my_entry| *my_entry.address == other_entry.address)
1345 {
1346 None => entries.push(other_entry),
1347 Some(i) => entries[i] = other_entry,
1348 };
1349 }
1350 Some(entries)
1351 }
1352 },
1353 },
1354 account_dir: match self.account_dir.take() {
1355 None => other.account_dir,
1356 Some(mut entries) => match other.account_dir {
1357 None => Some(entries),
1358 Some(other_entries) => {
1359 for other_entry in other_entries {
1360 match entries
1361 .iter()
1362 .position(|my_entry| *my_entry.directory == other_entry.directory)
1363 {
1364 None => entries.push(other_entry),
1365 Some(i) => entries[i] = other_entry,
1366 };
1367 }
1368 Some(entries)
1369 }
1370 },
1371 },
1372 bind_address: other.bind_address.or_else(|| self.bind_address.take()),
1373 clone: match self.clone.take() {
1374 None => other.clone,
1375 Some(mut entries) => match other.clone {
1376 None => Some(entries),
1377 Some(other_entries) => {
1378 for other_entry in other_entries {
1379 match entries
1380 .iter()
1381 .position(|my_entry| *my_entry.address == other_entry.address)
1382 {
1383 None => entries.push(other_entry),
1384 Some(i) => entries[i] = other_entry,
1385 };
1386 }
1387 Some(entries)
1388 }
1389 },
1390 },
1391 dynamic_port_range: other
1392 .dynamic_port_range
1393 .or_else(|| self.dynamic_port_range.take()),
1394 faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
1395 faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
1396 geyser_plugin_config: other
1397 .geyser_plugin_config
1398 .or_else(|| self.geyser_plugin_config.take()),
1399 gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
1400 gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
1401 url: other.url.or_else(|| self.url.take()),
1402 ledger: other.ledger.or_else(|| self.ledger.take()),
1403 limit_ledger_size: other
1404 .limit_ledger_size
1405 .or_else(|| self.limit_ledger_size.take()),
1406 rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
1407 slots_per_epoch: other
1408 .slots_per_epoch
1409 .or_else(|| self.slots_per_epoch.take()),
1410 ticks_per_slot: other.ticks_per_slot.or_else(|| self.ticks_per_slot.take()),
1411 warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
1412 deactivate_feature: other
1413 .deactivate_feature
1414 .or_else(|| self.deactivate_feature.take()),
1415 };
1416 }
1417}
1418
1419#[derive(Debug, Clone)]
1420pub struct Program {
1421 pub lib_name: String,
1422 pub path: PathBuf,
1424 pub idl: Option<Idl>,
1425}
1426
1427impl Program {
1428 pub fn pubkey(&self) -> Result<Pubkey> {
1429 self.keypair().map(|kp| kp.pubkey())
1430 }
1431
1432 pub fn keypair(&self) -> Result<Keypair> {
1433 let file = self.keypair_file()?;
1434 get_keypair(file.path().to_str().unwrap())
1435 }
1436
1437 pub fn keypair_file(&self) -> Result<WithPath<File>> {
1439 let deploy_dir_path = Path::new("target").join("deploy");
1440 fs::create_dir_all(&deploy_dir_path)
1441 .with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
1442 let path = std::env::current_dir()
1443 .expect("Must have current dir")
1444 .join(deploy_dir_path.join(format!("{}-keypair.json", self.lib_name)));
1445 if path.exists() {
1446 return Ok(WithPath::new(
1447 File::open(&path)
1448 .with_context(|| format!("Error opening file with path: {}", path.display()))?,
1449 path,
1450 ));
1451 }
1452 let program_kp = Keypair::new();
1453 let mut file = File::create(&path)
1454 .with_context(|| format!("Error creating file with path: {}", path.display()))?;
1455 file.write_all(format!("{:?}", &program_kp.to_bytes()).as_bytes())?;
1456 Ok(WithPath::new(file, path))
1457 }
1458
1459 pub fn binary_path(&self, verifiable: bool) -> PathBuf {
1460 let path = Path::new("target")
1461 .join(if verifiable { "verifiable" } else { "deploy" })
1462 .join(&self.lib_name)
1463 .with_extension("so");
1464
1465 std::env::current_dir()
1466 .expect("Must have current dir")
1467 .join(path)
1468 }
1469}
1470
1471#[derive(Debug, Default)]
1472pub struct ProgramDeployment {
1473 pub address: Pubkey,
1474 pub path: Option<String>,
1475 pub idl: Option<String>,
1476}
1477
1478impl TryFrom<_ProgramDeployment> for ProgramDeployment {
1479 type Error = anyhow::Error;
1480 fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
1481 Ok(ProgramDeployment {
1482 address: pd.address.parse()?,
1483 path: pd.path,
1484 idl: pd.idl,
1485 })
1486 }
1487}
1488
1489#[derive(Debug, Default, Serialize, Deserialize)]
1490pub struct _ProgramDeployment {
1491 pub address: String,
1492 pub path: Option<String>,
1493 pub idl: Option<String>,
1494}
1495
1496impl From<&ProgramDeployment> for _ProgramDeployment {
1497 fn from(pd: &ProgramDeployment) -> Self {
1498 Self {
1499 address: pd.address.to_string(),
1500 path: pd.path.clone(),
1501 idl: pd.idl.clone(),
1502 }
1503 }
1504}
1505
1506pub struct ProgramWorkspace {
1507 pub name: String,
1508 pub program_id: Pubkey,
1509 pub idl: Idl,
1510}
1511
1512#[derive(Debug, Serialize, Deserialize)]
1513pub struct AnchorPackage {
1514 pub name: String,
1515 pub address: String,
1516 pub idl: Option<String>,
1517}
1518
1519impl AnchorPackage {
1520 pub fn from(name: String, cfg: &WithPath<Config>) -> Result<Self> {
1521 let cluster = &cfg.provider.cluster;
1522 if cluster != &Cluster::Mainnet {
1523 return Err(anyhow!("Publishing requires the mainnet cluster"));
1524 }
1525 let program_details = cfg
1526 .programs
1527 .get(cluster)
1528 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
1529 .get(&name)
1530 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
1531 let idl = program_details.idl.clone();
1532 let address = program_details.address.to_string();
1533 Ok(Self { name, address, idl })
1534 }
1535}
1536
1537#[derive(Debug, Serialize, Deserialize)]
1538#[serde(rename_all = "camelCase")]
1539pub struct SurfnetInfoResponse {
1540 pub runbook_executions: Vec<RunbookExecution>,
1541}
1542#[derive(Debug, Serialize, Deserialize)]
1543#[serde(rename_all = "camelCase")]
1544pub struct RunbookExecution {
1545 #[serde(rename = "startedAt")]
1546 pub started_at: u32,
1547 #[serde(rename = "completedAt")]
1548 pub completed_at: Option<u32>,
1549 #[serde(rename = "runbookId")]
1550 pub runbook_id: String,
1551 pub errors: Option<Vec<String>>,
1552}
1553
1554#[macro_export]
1555macro_rules! home_path {
1556 ($my_struct:ident, $path:literal) => {
1557 #[derive(Clone, Debug)]
1558 pub struct $my_struct(String);
1559
1560 impl Default for $my_struct {
1561 fn default() -> Self {
1562 $my_struct(
1563 home_dir()
1564 .unwrap()
1565 .join($path.replace('/', std::path::MAIN_SEPARATOR_STR))
1566 .display()
1567 .to_string(),
1568 )
1569 }
1570 }
1571
1572 impl $my_struct {
1573 fn stringify_with_tilde(&self) -> String {
1574 self.0
1575 .replacen(home_dir().unwrap().to_str().unwrap(), "~", 1)
1576 }
1577 }
1578
1579 impl FromStr for $my_struct {
1580 type Err = anyhow::Error;
1581
1582 fn from_str(s: &str) -> Result<Self, Self::Err> {
1583 Ok(Self(s.to_owned()))
1584 }
1585 }
1586
1587 impl fmt::Display for $my_struct {
1588 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1589 write!(f, "{}", self.0)
1590 }
1591 }
1592 };
1593}
1594
1595home_path!(WalletPath, ".config/solana/id.json");
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::*;
1600
1601 const BASE_CONFIG: &str = "
1602 [provider]
1603 cluster = \"localnet\"
1604 wallet = \"id.json\"
1605 ";
1606
1607 #[test]
1608 fn parse_custom_cluster_str() {
1609 let config = Config::from_str(
1610 "
1611 [provider]
1612 cluster = \"http://my-url.com\"
1613 wallet = \"id.json\"
1614 ",
1615 )
1616 .unwrap();
1617 assert!(!config.features.skip_lint);
1618
1619 assert!(config
1621 .to_string()
1622 .contains(r#"cluster = "http://my-url.com""#));
1623 }
1624
1625 #[test]
1626 fn parse_custom_cluster_map() {
1627 let config = Config::from_str(
1628 "
1629 [provider]
1630 cluster = { http = \"http://my-url.com\", ws = \"ws://my-url.com\" }
1631 wallet = \"id.json\"
1632 ",
1633 )
1634 .unwrap();
1635 assert!(!config.features.skip_lint);
1636 }
1637
1638 #[test]
1639 fn parse_skip_lint_no_section() {
1640 let config = Config::from_str(BASE_CONFIG).unwrap();
1641 assert!(!config.features.skip_lint);
1642 }
1643
1644 #[test]
1645 fn parse_skip_lint_no_value() {
1646 let string = BASE_CONFIG.to_owned() + "[features]";
1647 let config = Config::from_str(&string).unwrap();
1648 assert!(!config.features.skip_lint);
1649 }
1650
1651 #[test]
1652 fn parse_skip_lint_true() {
1653 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true";
1654 let config = Config::from_str(&string).unwrap();
1655 assert!(config.features.skip_lint);
1656 }
1657
1658 #[test]
1659 fn parse_skip_lint_false() {
1660 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false";
1661 let config = Config::from_str(&string).unwrap();
1662 assert!(!config.features.skip_lint);
1663 }
1664}