use crate::is_hidden; use anchor_client::Cluster; use anchor_syn::idl::Idl; use anyhow::{anyhow, Context, Error, Result}; use clap::{Parser, ValueEnum}; use heck::ToSnakeCase; use serde::{Deserialize, Serialize}; use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signer}; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::fs::{self, File}; use std::io; use std::io::prelude::*; use std::ops::Deref; use std::path::Path; use std::path::PathBuf; use std::str::FromStr; use walkdir::WalkDir; pub trait Merge: Sized { fn merge(&mut self, _other: Self) {} } #[derive(Default, Debug, Parser)] pub struct ConfigOverride { /// Cluster override. #[clap(global = true, long = "provider.cluster")] pub cluster: Option, /// Wallet override. #[clap(global = true, long = "provider.wallet")] pub wallet: Option, } #[derive(Debug)] pub struct WithPath { inner: T, path: PathBuf, } impl WithPath { pub fn new(inner: T, path: PathBuf) -> Self { Self { inner, path } } pub fn path(&self) -> &PathBuf { &self.path } pub fn into_inner(self) -> T { self.inner } } impl std::convert::AsRef for WithPath { fn as_ref(&self) -> &T { &self.inner } } #[derive(Debug, Clone, PartialEq)] pub struct Manifest(cargo_toml::Manifest); impl Manifest { pub fn from_path(p: impl AsRef) -> Result { cargo_toml::Manifest::from_path(&p) .map(Manifest) .map_err(anyhow::Error::from) .with_context(|| format!("Error reading manifest from path: {}", p.as_ref().display())) } pub fn lib_name(&self) -> Result { if self.lib.is_some() && self.lib.as_ref().unwrap().name.is_some() { Ok(self .lib .as_ref() .unwrap() .name .as_ref() .unwrap() .to_string() .to_snake_case()) } else { Ok(self .package .as_ref() .ok_or_else(|| anyhow!("package section not provided"))? .name .to_string() .to_snake_case()) } } pub fn version(&self) -> String { match &self.package { Some(package) => package.version().to_string(), _ => "0.0.0".to_string(), } } // Climbs each parent directory from the current dir until we find a Cargo.toml pub fn discover() -> Result>> { Manifest::discover_from_path(std::env::current_dir()?) } // Climbs each parent directory from a given starting directory until we find a Cargo.toml. pub fn discover_from_path(start_from: PathBuf) -> Result>> { let mut cwd_opt = Some(start_from.as_path()); while let Some(cwd) = cwd_opt { for f in fs::read_dir(cwd).with_context(|| { format!("Error reading the directory with path: {}", cwd.display()) })? { let p = f .with_context(|| { format!("Error reading the directory with path: {}", cwd.display()) })? .path(); if let Some(filename) = p.file_name() { if filename.to_str() == Some("Cargo.toml") { let m = WithPath::new(Manifest::from_path(&p)?, p); return Ok(Some(m)); } } } // Not found. Go up a directory level. cwd_opt = cwd.parent(); } Ok(None) } } impl Deref for Manifest { type Target = cargo_toml::Manifest; fn deref(&self) -> &Self::Target { &self.0 } } impl WithPath { pub fn get_program_list(&self) -> Result> { // Canonicalize the workspace filepaths to compare with relative paths. let (members, exclude) = self.canonicalize_workspace()?; // Get all candidate programs. // // If [workspace.members] exists, then use that. // Otherwise, default to `programs/*`. let program_paths: Vec = { if members.is_empty() { let path = self.path().parent().unwrap().join("programs"); fs::read_dir(path)? .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false)) .map(|dir| dir.map(|d| d.path().canonicalize().unwrap())) .collect::>>() .into_iter() .collect::, std::io::Error>>()? } else { members } }; // Filter out everything part of the exclude array. Ok(program_paths .into_iter() .filter(|m| !exclude.contains(m)) .collect()) } // TODO: this should read idl dir instead of parsing source. pub fn read_all_programs(&self) -> Result> { let mut r = vec![]; for path in self.get_program_list()? { let cargo = Manifest::from_path(&path.join("Cargo.toml"))?; let lib_name = cargo.lib_name()?; let version = cargo.version(); let idl = anchor_syn::idl::file::parse( path.join("src/lib.rs"), version, self.features.seeds, false, false, )?; r.push(Program { lib_name, path, idl, }); } Ok(r) } pub fn canonicalize_workspace(&self) -> Result<(Vec, Vec)> { let members = self .workspace .members .iter() .map(|m| { self.path() .parent() .unwrap() .join(m) .canonicalize() .unwrap_or_else(|_| { panic!("Error reading workspace.members. File {:?} does not exist at path {:?}.", m, self.path) }) }) .collect(); let exclude = self .workspace .exclude .iter() .map(|m| { self.path() .parent() .unwrap() .join(m) .canonicalize() .unwrap_or_else(|_| { panic!("Error reading workspace.exclude. File {:?} does not exist at path {:?}.", m, self.path) }) }) .collect(); Ok((members, exclude)) } pub fn get_program(&self, name: &str) -> Result>> { for program in self.read_all_programs()? { let cargo_toml = program.path.join("Cargo.toml"); if !cargo_toml.exists() { return Err(anyhow!( "Did not find Cargo.toml at the path: {}", program.path.display() )); } let p_lib_name = Manifest::from_path(&cargo_toml)?.lib_name()?; if name == p_lib_name { let path = self .path() .parent() .unwrap() .canonicalize()? .join(&program.path); return Ok(Some(WithPath::new(program, path))); } } Ok(None) } } impl std::ops::Deref for WithPath { type Target = T; fn deref(&self) -> &Self::Target { &self.inner } } impl std::ops::DerefMut for WithPath { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } #[derive(Debug, Default)] pub struct Config { pub anchor_version: Option, pub solana_version: Option, pub features: FeaturesConfig, pub registry: RegistryConfig, pub provider: ProviderConfig, pub programs: ProgramsConfig, pub scripts: ScriptsConfig, pub workspace: WorkspaceConfig, // Separate entry next to test_config because // "anchor localnet" only has access to the Anchor.toml, // not the Test.toml files pub test_validator: Option, pub test_config: Option, } #[derive(Default, Clone, Debug, Serialize, Deserialize)] pub struct FeaturesConfig { #[serde(default)] pub seeds: bool, #[serde(default, rename = "skip-lint")] pub skip_lint: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RegistryConfig { pub url: String, } impl Default for RegistryConfig { fn default() -> Self { Self { url: "https://api.apr.dev".to_string(), } } } #[derive(Debug, Default)] pub struct ProviderConfig { pub cluster: Cluster, pub wallet: WalletPath, } pub type ScriptsConfig = BTreeMap; pub type ProgramsConfig = BTreeMap>; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct WorkspaceConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub members: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub exclude: Vec, #[serde(default, skip_serializing_if = "String::is_empty")] pub types: String, } #[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug)] pub enum BootstrapMode { None, Debian, } #[derive(Debug, Clone)] pub struct BuildConfig { pub verifiable: bool, pub solana_version: Option, pub docker_image: String, pub bootstrap: BootstrapMode, } impl Config { pub fn add_test_config(&mut self, root: impl AsRef) -> Result<()> { self.test_config = TestConfig::discover(root)?; Ok(()) } pub fn docker(&self) -> String { let ver = self .anchor_version .clone() .unwrap_or_else(|| crate::DOCKER_BUILDER_VERSION.to_string()); format!("projectserum/build:v{}", ver) } pub fn discover(cfg_override: &ConfigOverride) -> Result>> { Config::_discover().map(|opt| { opt.map(|mut cfg| { if let Some(cluster) = cfg_override.cluster.clone() { cfg.provider.cluster = cluster; } if let Some(wallet) = cfg_override.wallet.clone() { cfg.provider.wallet = wallet; } cfg }) }) } // Climbs each parent directory until we find an Anchor.toml. fn _discover() -> Result>> { let _cwd = std::env::current_dir()?; let mut cwd_opt = Some(_cwd.as_path()); while let Some(cwd) = cwd_opt { for f in fs::read_dir(cwd).with_context(|| { format!("Error reading the directory with path: {}", cwd.display()) })? { let p = f .with_context(|| { format!("Error reading the directory with path: {}", cwd.display()) })? .path(); if let Some(filename) = p.file_name() { if filename.to_str() == Some("Anchor.toml") { let cfg = Config::from_path(&p)?; return Ok(Some(WithPath::new(cfg, p))); } } } cwd_opt = cwd.parent(); } Ok(None) } fn from_path(p: impl AsRef) -> Result { fs::read_to_string(&p) .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))? .parse::() } pub fn wallet_kp(&self) -> Result { solana_sdk::signature::read_keypair_file(&self.provider.wallet.to_string()) .map_err(|_| anyhow!("Unable to read keypair file")) } } #[derive(Debug, Serialize, Deserialize)] struct _Config { anchor_version: Option, solana_version: Option, features: Option, programs: Option>>, registry: Option, provider: Provider, workspace: Option, scripts: Option, test: Option<_TestValidator>, } #[derive(Debug, Serialize, Deserialize)] struct Provider { cluster: String, wallet: String, } impl ToString for Config { fn to_string(&self) -> String { let programs = { let c = ser_programs(&self.programs); if c.is_empty() { None } else { Some(c) } }; let cfg = _Config { anchor_version: self.anchor_version.clone(), solana_version: self.solana_version.clone(), features: Some(self.features.clone()), registry: Some(self.registry.clone()), provider: Provider { cluster: format!("{}", self.provider.cluster), wallet: self.provider.wallet.to_string(), }, test: self.test_validator.clone().map(Into::into), scripts: match self.scripts.is_empty() { true => None, false => Some(self.scripts.clone()), }, programs, workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty()) .then(|| self.workspace.clone()), }; toml::to_string(&cfg).expect("Must be well formed") } } impl FromStr for Config { type Err = Error; fn from_str(s: &str) -> Result { let cfg: _Config = toml::from_str(s) .map_err(|e| anyhow::format_err!("Unable to deserialize config: {}", e.to_string()))?; Ok(Config { anchor_version: cfg.anchor_version, solana_version: cfg.solana_version, features: cfg.features.unwrap_or_default(), registry: cfg.registry.unwrap_or_default(), provider: ProviderConfig { cluster: cfg.provider.cluster.parse()?, wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?, }, scripts: cfg.scripts.unwrap_or_default(), test_validator: cfg.test.map(Into::into), test_config: None, programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?, workspace: cfg.workspace.unwrap_or_default(), }) } } pub fn get_solana_cfg_url() -> Result { let config_file = CONFIG_FILE.as_ref().ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, "Default Solana config was not found", ) })?; SolanaConfig::load(config_file).map(|config| config.json_rpc_url) } fn ser_programs( programs: &BTreeMap>, ) -> BTreeMap> { programs .iter() .map(|(cluster, programs)| { let cluster = cluster.to_string(); let programs = programs .iter() .map(|(name, deployment)| { ( name.clone(), to_value(&_ProgramDeployment::from(deployment)), ) }) .collect::>(); (cluster, programs) }) .collect::>>() } fn to_value(dep: &_ProgramDeployment) -> serde_json::Value { if dep.path.is_none() && dep.idl.is_none() { return serde_json::Value::String(dep.address.to_string()); } serde_json::to_value(dep).unwrap() } fn deser_programs( programs: BTreeMap>, ) -> Result>> { programs .iter() .map(|(cluster, programs)| { let cluster: Cluster = cluster.parse()?; let programs = programs .iter() .map(|(name, program_id)| { Ok(( name.clone(), ProgramDeployment::try_from(match &program_id { serde_json::Value::String(address) => _ProgramDeployment { address: address.parse()?, path: None, idl: None, }, serde_json::Value::Object(_) => { serde_json::from_value(program_id.clone()) .map_err(|_| anyhow!("Unable to read toml"))? } _ => return Err(anyhow!("Invalid toml type")), })?, )) }) .collect::>>()?; Ok((cluster, programs)) }) .collect::>>>() } #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct TestValidator { pub genesis: Option>, pub validator: Option, pub startup_wait: i32, pub shutdown_wait: i32, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct _TestValidator { #[serde(skip_serializing_if = "Option::is_none")] pub genesis: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub validator: Option<_Validator>, #[serde(skip_serializing_if = "Option::is_none")] pub startup_wait: Option, #[serde(skip_serializing_if = "Option::is_none")] pub shutdown_wait: Option, } pub const STARTUP_WAIT: i32 = 5000; pub const SHUTDOWN_WAIT: i32 = 2000; impl From<_TestValidator> for TestValidator { fn from(_test_validator: _TestValidator) -> Self { Self { shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT), startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT), genesis: _test_validator.genesis, validator: _test_validator.validator.map(Into::into), } } } impl From for _TestValidator { fn from(test_validator: TestValidator) -> Self { Self { shutdown_wait: Some(test_validator.shutdown_wait), startup_wait: Some(test_validator.startup_wait), genesis: test_validator.genesis, validator: test_validator.validator.map(Into::into), } } } #[derive(Debug, Clone)] pub struct TestConfig { pub test_suite_configs: HashMap, } impl Deref for TestConfig { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.test_suite_configs } } impl TestConfig { pub fn discover(root: impl AsRef) -> Result> { let walker = WalkDir::new(root).into_iter(); let mut test_suite_configs = HashMap::new(); for entry in walker.filter_entry(|e| !is_hidden(e)) { let entry = entry?; if entry.file_name() == "Test.toml" { let test_toml = TestToml::from_path(entry.path())?; test_suite_configs.insert(entry.path().into(), test_toml); } } Ok(match test_suite_configs.is_empty() { true => None, false => Some(Self { test_suite_configs }), }) } } // This file needs to have the same (sub)structure as Anchor.toml // so it can be parsed as a base test file from an Anchor.toml #[derive(Debug, Clone, Serialize, Deserialize)] pub struct _TestToml { pub extends: Option>, pub test: Option<_TestValidator>, pub scripts: Option, } impl _TestToml { fn from_path(path: impl AsRef) -> Result { let s = fs::read_to_string(&path)?; let parsed_toml: Self = toml::from_str(&s)?; let mut current_toml = _TestToml { extends: None, test: None, scripts: None, }; if let Some(bases) = &parsed_toml.extends { for base in bases { let mut canonical_base = base.clone(); canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?; current_toml.merge(_TestToml::from_path(&canonical_base)?); } } current_toml.merge(parsed_toml); if let Some(test) = &mut current_toml.test { if let Some(genesis_programs) = &mut test.genesis { for entry in genesis_programs { entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?; } } if let Some(validator) = &mut test.validator { if let Some(ledger_dir) = &mut validator.ledger { *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?; } if let Some(accounts) = &mut validator.account { for entry in accounts { entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?; } } } } Ok(current_toml) } } /// canonicalizes the `file_path` arg. /// uses the `path` arg as the current dir /// from which to turn the relative path /// into a canonical one fn canonicalize_filepath_from_origin( file_path: impl AsRef, origin: impl AsRef, ) -> Result { let previous_dir = std::env::current_dir()?; std::env::set_current_dir(origin.as_ref().parent().unwrap())?; let result = fs::canonicalize(&file_path) .with_context(|| { format!( "Error reading (possibly relative) path: {}. If relative, this is the path that was used as the current path: {}", &file_path.as_ref().display(), &origin.as_ref().display() ) })? .display() .to_string(); std::env::set_current_dir(previous_dir)?; Ok(result) } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestToml { #[serde(skip_serializing_if = "Option::is_none")] pub test: Option, pub scripts: ScriptsConfig, } impl TestToml { pub fn from_path(p: impl AsRef) -> Result { WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into() } } impl Merge for _TestToml { fn merge(&mut self, other: Self) { let mut my_scripts = self.scripts.take(); match &mut my_scripts { None => my_scripts = other.scripts, Some(my_scripts) => { if let Some(other_scripts) = other.scripts { for (name, script) in other_scripts { my_scripts.insert(name, script); } } } } let mut my_test = self.test.take(); match &mut my_test { Some(my_test) => { if let Some(other_test) = other.test { if let Some(startup_wait) = other_test.startup_wait { my_test.startup_wait = Some(startup_wait); } if let Some(other_genesis) = other_test.genesis { match &mut my_test.genesis { Some(my_genesis) => { for other_entry in other_genesis { match my_genesis .iter() .position(|g| *g.address == other_entry.address) { None => my_genesis.push(other_entry), Some(i) => my_genesis[i] = other_entry, } } } None => my_test.genesis = Some(other_genesis), } } let mut my_validator = my_test.validator.take(); match &mut my_validator { None => my_validator = other_test.validator, Some(my_validator) => { if let Some(other_validator) = other_test.validator { my_validator.merge(other_validator) } } } my_test.validator = my_validator; } } None => my_test = other.test, }; // Instantiating a new Self object here ensures that // this function will fail to compile if new fields get added // to Self. This is useful as a reminder if they also require merging *self = Self { test: my_test, scripts: my_scripts, extends: self.extends.take(), }; } } impl TryFrom> for TestToml { type Error = Error; fn try_from(mut value: WithPath<_TestToml>) -> Result { Ok(Self { test: value.test.take().map(Into::into), scripts: value .scripts .take() .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?, }) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenesisEntry { // Base58 pubkey string. pub address: String, // Filepath to the compiled program to embed into the genesis. pub program: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CloneEntry { // Base58 pubkey string. pub address: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccountEntry { // Base58 pubkey string. pub address: String, // Name of JSON file containing the account data. pub filename: String, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct _Validator { // Load an account from the provided JSON file #[serde(skip_serializing_if = "Option::is_none")] pub account: Option>, // IP address to bind the validator ports. [default: 0.0.0.0] #[serde(skip_serializing_if = "Option::is_none")] pub bind_address: Option, // Copy an account from the cluster referenced by the url argument. #[serde(skip_serializing_if = "Option::is_none")] pub clone: Option>, // Range to use for dynamically assigned ports. [default: 1024-65535] #[serde(skip_serializing_if = "Option::is_none")] pub dynamic_port_range: Option, // Enable the faucet on this port [default: 9900]. #[serde(skip_serializing_if = "Option::is_none")] pub faucet_port: Option, // Give the faucet address this much SOL in genesis. [default: 1000000] #[serde(skip_serializing_if = "Option::is_none")] pub faucet_sol: Option, // Geyser plugin config location #[serde(skip_serializing_if = "Option::is_none")] pub geyser_plugin_config: Option, // Gossip DNS name or IP address for the validator to advertise in gossip. [default: 127.0.0.1] #[serde(skip_serializing_if = "Option::is_none")] pub gossip_host: Option, // Gossip port number for the validator #[serde(skip_serializing_if = "Option::is_none")] pub gossip_port: Option, // URL for Solana's JSON RPC or moniker. #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, // Use DIR as ledger location #[serde(skip_serializing_if = "Option::is_none")] pub ledger: Option, // Keep this amount of shreds in root slots. [default: 10000] #[serde(skip_serializing_if = "Option::is_none")] pub limit_ledger_size: Option, // Enable JSON RPC on this port, and the next port for the RPC websocket. [default: 8899] #[serde(skip_serializing_if = "Option::is_none")] pub rpc_port: Option, // Override the number of slots in an epoch. #[serde(skip_serializing_if = "Option::is_none")] pub slots_per_epoch: Option, // Warp the ledger to WARP_SLOT after starting the validator. #[serde(skip_serializing_if = "Option::is_none")] pub warp_slot: Option, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Validator { #[serde(skip_serializing_if = "Option::is_none")] pub account: Option>, pub bind_address: String, #[serde(skip_serializing_if = "Option::is_none")] pub clone: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub dynamic_port_range: Option, #[serde(skip_serializing_if = "Option::is_none")] pub faucet_port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub faucet_sol: Option, #[serde(skip_serializing_if = "Option::is_none")] pub geyser_plugin_config: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gossip_host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gossip_port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, pub ledger: String, #[serde(skip_serializing_if = "Option::is_none")] pub limit_ledger_size: Option, pub rpc_port: u16, #[serde(skip_serializing_if = "Option::is_none")] pub slots_per_epoch: Option, #[serde(skip_serializing_if = "Option::is_none")] pub warp_slot: Option, } impl From<_Validator> for Validator { fn from(_validator: _Validator) -> Self { Self { account: _validator.account, bind_address: _validator .bind_address .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()), clone: _validator.clone, dynamic_port_range: _validator.dynamic_port_range, faucet_port: _validator.faucet_port, faucet_sol: _validator.faucet_sol, geyser_plugin_config: _validator.geyser_plugin_config, gossip_host: _validator.gossip_host, gossip_port: _validator.gossip_port, url: _validator.url, ledger: _validator .ledger .unwrap_or_else(|| DEFAULT_LEDGER_PATH.to_string()), limit_ledger_size: _validator.limit_ledger_size, rpc_port: _validator .rpc_port .unwrap_or(solana_sdk::rpc_port::DEFAULT_RPC_PORT), slots_per_epoch: _validator.slots_per_epoch, warp_slot: _validator.warp_slot, } } } impl From for _Validator { fn from(validator: Validator) -> Self { Self { account: validator.account, bind_address: Some(validator.bind_address), clone: validator.clone, dynamic_port_range: validator.dynamic_port_range, faucet_port: validator.faucet_port, faucet_sol: validator.faucet_sol, geyser_plugin_config: validator.geyser_plugin_config, gossip_host: validator.gossip_host, gossip_port: validator.gossip_port, url: validator.url, ledger: Some(validator.ledger), limit_ledger_size: validator.limit_ledger_size, rpc_port: Some(validator.rpc_port), slots_per_epoch: validator.slots_per_epoch, warp_slot: validator.warp_slot, } } } const DEFAULT_LEDGER_PATH: &str = ".anchor/test-ledger"; const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0"; impl Merge for _Validator { fn merge(&mut self, other: Self) { // Instantiating a new Self object here ensures that // this function will fail to compile if new fields get added // to Self. This is useful as a reminder if they also require merging *self = Self { account: match self.account.take() { None => other.account, Some(mut entries) => match other.account { None => Some(entries), Some(other_entries) => { for other_entry in other_entries { match entries .iter() .position(|my_entry| *my_entry.address == other_entry.address) { None => entries.push(other_entry), Some(i) => entries[i] = other_entry, }; } Some(entries) } }, }, bind_address: other.bind_address.or_else(|| self.bind_address.take()), clone: match self.clone.take() { None => other.clone, Some(mut entries) => match other.clone { None => Some(entries), Some(other_entries) => { for other_entry in other_entries { match entries .iter() .position(|my_entry| *my_entry.address == other_entry.address) { None => entries.push(other_entry), Some(i) => entries[i] = other_entry, }; } Some(entries) } }, }, dynamic_port_range: other .dynamic_port_range .or_else(|| self.dynamic_port_range.take()), faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()), faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()), geyser_plugin_config: other .geyser_plugin_config .or_else(|| self.geyser_plugin_config.take()), gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()), gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()), url: other.url.or_else(|| self.url.take()), ledger: other.ledger.or_else(|| self.ledger.take()), limit_ledger_size: other .limit_ledger_size .or_else(|| self.limit_ledger_size.take()), rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()), slots_per_epoch: other .slots_per_epoch .or_else(|| self.slots_per_epoch.take()), warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()), }; } } #[derive(Debug, Clone)] pub struct Program { pub lib_name: String, // Canonicalized path to the program directory. pub path: PathBuf, pub idl: Option, } impl Program { pub fn pubkey(&self) -> Result { self.keypair().map(|kp| kp.pubkey()) } pub fn keypair(&self) -> Result { let file = self.keypair_file()?; solana_sdk::signature::read_keypair_file(file.path()) .map_err(|_| anyhow!("failed to read keypair for program: {}", self.lib_name)) } // Lazily initializes the keypair file with a new key if it doesn't exist. pub fn keypair_file(&self) -> Result> { let deploy_dir_path = "target/deploy/"; fs::create_dir_all(deploy_dir_path) .with_context(|| format!("Error creating directory with path: {}", deploy_dir_path))?; let path = std::env::current_dir() .expect("Must have current dir") .join(format!("target/deploy/{}-keypair.json", self.lib_name)); if path.exists() { return Ok(WithPath::new( File::open(&path) .with_context(|| format!("Error opening file with path: {}", path.display()))?, path, )); } let program_kp = Keypair::new(); let mut file = File::create(&path) .with_context(|| format!("Error creating file with path: {}", path.display()))?; file.write_all(format!("{:?}", &program_kp.to_bytes()).as_bytes())?; Ok(WithPath::new(file, path)) } pub fn binary_path(&self) -> PathBuf { std::env::current_dir() .expect("Must have current dir") .join(format!("target/deploy/{}.so", self.lib_name)) } } #[derive(Debug, Default)] pub struct ProgramDeployment { pub address: Pubkey, pub path: Option, pub idl: Option, } impl TryFrom<_ProgramDeployment> for ProgramDeployment { type Error = anyhow::Error; fn try_from(pd: _ProgramDeployment) -> Result { Ok(ProgramDeployment { address: pd.address.parse()?, path: pd.path, idl: pd.idl, }) } } #[derive(Debug, Default, Serialize, Deserialize)] pub struct _ProgramDeployment { pub address: String, pub path: Option, pub idl: Option, } impl From<&ProgramDeployment> for _ProgramDeployment { fn from(pd: &ProgramDeployment) -> Self { Self { address: pd.address.to_string(), path: pd.path.clone(), idl: pd.idl.clone(), } } } pub struct ProgramWorkspace { pub name: String, pub program_id: Pubkey, pub idl: Idl, } #[derive(Debug, Serialize, Deserialize)] pub struct AnchorPackage { pub name: String, pub address: String, pub idl: Option, } impl AnchorPackage { pub fn from(name: String, cfg: &WithPath) -> Result { let cluster = &cfg.provider.cluster; if cluster != &Cluster::Mainnet { return Err(anyhow!("Publishing requires the mainnet cluster")); } let program_details = cfg .programs .get(cluster) .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))? .get(&name) .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?; let idl = program_details.idl.clone(); let address = program_details.address.to_string(); Ok(Self { name, address, idl }) } } crate::home_path!(WalletPath, ".config/solana/id.json"); #[cfg(test)] mod tests { use super::*; const BASE_CONFIG: &str = " [provider] cluster = \"localnet\" wallet = \"id.json\" "; #[test] fn parse_skip_lint_no_section() { let config = Config::from_str(BASE_CONFIG).unwrap(); assert!(!config.features.skip_lint); } #[test] fn parse_skip_lint_no_value() { let string = BASE_CONFIG.to_owned() + "[features]"; let config = Config::from_str(&string).unwrap(); assert!(!config.features.skip_lint); } #[test] fn parse_skip_lint_true() { let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true"; let config = Config::from_str(&string).unwrap(); assert!(config.features.skip_lint); } #[test] fn parse_skip_lint_false() { let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false"; let config = Config::from_str(&string).unwrap(); assert!(!config.features.skip_lint); } }