use std::net::SocketAddr;
use std::path::PathBuf;
use crate::data::{CustomNetwork, DevnetManifest, EvmAddress, EvmNetwork, MultiAddr};
use crate::error::{Error, Result};
pub fn data_dir() -> Result<PathBuf> {
let base = if cfg!(target_os = "macos") {
home_dir()?.join("Library").join("Application Support")
} else if cfg!(target_os = "windows") {
std::env::var("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().unwrap().join("AppData").join("Roaming"))
} else {
std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().unwrap().join(".local").join("share"))
};
Ok(base.join("ant"))
}
pub fn config_dir() -> Result<PathBuf> {
let base = if cfg!(target_os = "macos") {
home_dir()?.join("Library").join("Application Support")
} else if cfg!(target_os = "windows") {
std::env::var("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().unwrap().join("AppData").join("Roaming"))
} else {
std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().unwrap().join(".config"))
};
Ok(base.join("ant"))
}
pub fn log_dir() -> Result<PathBuf> {
if cfg!(target_os = "macos") {
Ok(home_dir()?.join("Library").join("Logs").join("ant"))
} else {
Ok(data_dir()?.join("logs"))
}
}
pub fn load_bootstrap_peers() -> Result<Option<Vec<SocketAddr>>> {
let path = config_dir()?.join("bootstrap_peers.toml");
if !path.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(&path)?;
let config: BootstrapConfig =
toml::from_str(&contents).map_err(|e| Error::BootstrapConfigParse(e.to_string()))?;
let addrs: Vec<SocketAddr> = config.peers.iter().filter_map(|s| s.parse().ok()).collect();
if addrs.is_empty() {
return Ok(None);
}
Ok(Some(addrs))
}
pub fn resolve_bootstrap_peers(
explicit: &[SocketAddr],
manifest: Option<&DevnetManifest>,
) -> Result<Vec<SocketAddr>> {
if !explicit.is_empty() {
return Ok(explicit.to_vec());
}
if let Some(m) = manifest {
let peers: Vec<SocketAddr> = m
.bootstrap
.iter()
.filter_map(MultiAddr::socket_addr)
.collect();
if peers.is_empty() {
return Err(Error::NoBootstrapPeers);
}
return Ok(peers);
}
if let Some(peers) = load_bootstrap_peers()? {
tracing::info!("Loaded {} bootstrap peer(s) from config file", peers.len());
return Ok(peers);
}
Err(Error::NoBootstrapPeers)
}
pub fn resolve_evm_network(
name: Option<&str>,
manifest: Option<&DevnetManifest>,
) -> Result<EvmNetwork> {
match name {
None => {
if manifest.is_some_and(|m| m.evm.is_some()) {
Err(Error::EvmNetworkAmbiguous)
} else {
Ok(EvmNetwork::ArbitrumOne)
}
}
Some("arbitrum-one") => Ok(EvmNetwork::ArbitrumOne),
Some("arbitrum-sepolia") => Ok(EvmNetwork::ArbitrumSepoliaTest),
Some("local") => {
let evm = manifest
.and_then(|m| m.evm.as_ref())
.ok_or(Error::EvmManifestRequired)?;
let rpc_url: reqwest::Url = evm
.rpc_url
.parse()
.map_err(|e| Error::InvalidEvmManifest(format!("invalid RPC URL: {e}")))?;
let payment_token_address: EvmAddress =
evm.payment_token_address.parse().map_err(|e| {
Error::InvalidEvmManifest(format!("invalid payment token address: {e}"))
})?;
let payment_vault_address: EvmAddress =
evm.payment_vault_address.parse().map_err(|e| {
Error::InvalidEvmManifest(format!("invalid payment vault address: {e}"))
})?;
Ok(EvmNetwork::Custom(CustomNetwork {
rpc_url_http: rpc_url,
payment_token_address,
payment_vault_address,
}))
}
Some(other) => Err(Error::UnsupportedEvmNetwork(other.to_string())),
}
}
#[derive(serde::Deserialize)]
struct BootstrapConfig {
peers: Vec<String>,
}
fn home_dir() -> Result<PathBuf> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(PathBuf::from)
.map_err(|_| Error::HomeDirNotFound)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn data_dir_ends_with_ant() {
let dir = data_dir().unwrap();
assert_eq!(dir.file_name().unwrap(), "ant");
}
#[test]
fn config_dir_ends_with_ant() {
let dir = config_dir().unwrap();
assert_eq!(dir.file_name().unwrap(), "ant");
}
#[test]
fn log_dir_contains_ant() {
let dir = log_dir().unwrap();
assert!(
dir.components().any(|c| c.as_os_str() == "ant"),
"log_dir should contain 'ant' component: {:?}",
dir
);
}
fn test_manifest(addrs: Vec<SocketAddr>) -> DevnetManifest {
DevnetManifest {
base_port: 10000,
node_count: addrs.len(),
bootstrap: addrs.into_iter().map(MultiAddr::quic).collect(),
data_dir: PathBuf::new(),
created_at: String::new(),
evm: None,
}
}
#[test]
fn resolve_bootstrap_prefers_explicit_peers() {
let explicit: Vec<SocketAddr> = vec!["10.0.0.1:10000".parse().unwrap()];
let manifest = test_manifest(vec!["10.0.0.2:10000".parse().unwrap()]);
let peers = resolve_bootstrap_peers(&explicit, Some(&manifest)).unwrap();
assert_eq!(peers, explicit);
}
#[test]
fn resolve_bootstrap_uses_manifest_when_no_explicit_peers() {
let addr: SocketAddr = "10.0.0.2:10000".parse().unwrap();
let manifest = test_manifest(vec![addr]);
let peers = resolve_bootstrap_peers(&[], Some(&manifest)).unwrap();
assert_eq!(peers, vec![addr]);
}
fn manifest_with_evm() -> DevnetManifest {
let mut m = test_manifest(vec!["10.0.0.2:10000".parse().unwrap()]);
m.evm = Some(ant_protocol::DevnetEvmInfo {
rpc_url: "http://127.0.0.1:8545".to_string(),
wallet_private_key:
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string(),
payment_token_address: "0x5FbDB2315678afecb367f032d93F642f64180aa3".to_string(),
payment_vault_address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512".to_string(),
});
m
}
#[test]
fn resolve_evm_network_defaults_to_mainnet_without_manifest_evm() {
assert!(matches!(
resolve_evm_network(None, None),
Ok(EvmNetwork::ArbitrumOne)
));
let no_evm = test_manifest(vec![]);
assert!(matches!(
resolve_evm_network(None, Some(&no_evm)),
Ok(EvmNetwork::ArbitrumOne)
));
}
#[test]
fn resolve_evm_network_requires_choice_when_manifest_has_evm() {
assert!(matches!(
resolve_evm_network(None, Some(&manifest_with_evm())),
Err(Error::EvmNetworkAmbiguous)
));
}
#[test]
fn resolve_evm_network_maps_presets() {
assert!(matches!(
resolve_evm_network(Some("arbitrum-one"), None),
Ok(EvmNetwork::ArbitrumOne)
));
assert!(matches!(
resolve_evm_network(Some("arbitrum-sepolia"), None),
Ok(EvmNetwork::ArbitrumSepoliaTest)
));
}
#[test]
fn resolve_evm_network_local_reads_manifest() {
let manifest = manifest_with_evm();
let network = resolve_evm_network(Some("local"), Some(&manifest)).unwrap();
match network {
EvmNetwork::Custom(custom) => {
assert_eq!(custom.rpc_url_http.as_str(), "http://127.0.0.1:8545/");
assert_eq!(
format!("{:?}", custom.payment_token_address).to_lowercase(),
"0x5fbdb2315678afecb367f032d93f642f64180aa3"
);
}
other => panic!("expected Custom network, got {other:?}"),
}
}
#[test]
fn resolve_evm_network_local_requires_manifest_evm_block() {
assert!(matches!(
resolve_evm_network(Some("local"), None),
Err(Error::EvmManifestRequired)
));
let no_evm = test_manifest(vec![]);
assert!(matches!(
resolve_evm_network(Some("local"), Some(&no_evm)),
Err(Error::EvmManifestRequired)
));
}
#[test]
fn resolve_evm_network_rejects_unknown_and_bad_manifest_values() {
assert!(matches!(
resolve_evm_network(Some("mainnet"), None),
Err(Error::UnsupportedEvmNetwork(_))
));
let mut bad = manifest_with_evm();
bad.evm.as_mut().unwrap().payment_vault_address = "not-an-address".to_string();
assert!(matches!(
resolve_evm_network(Some("local"), Some(&bad)),
Err(Error::InvalidEvmManifest(_))
));
}
#[test]
fn resolve_bootstrap_errors_on_empty_manifest() {
let manifest = test_manifest(vec![]);
let err = resolve_bootstrap_peers(&[], Some(&manifest)).unwrap_err();
assert!(matches!(err, Error::NoBootstrapPeers));
}
#[test]
fn resolve_bootstrap_errors_when_all_manifest_peers_filtered() {
let bt: MultiAddr = "/bt/00:11:22:33:44:55/rfcomm/1".parse().unwrap();
assert!(bt.socket_addr().is_none());
let mut manifest = test_manifest(vec![]);
manifest.bootstrap = vec![bt];
manifest.node_count = 1;
let err = resolve_bootstrap_peers(&[], Some(&manifest)).unwrap_err();
assert!(matches!(err, Error::NoBootstrapPeers));
}
#[test]
fn load_bootstrap_peers_returns_none_when_no_file() {
let _result = load_bootstrap_peers();
}
#[test]
fn parse_bootstrap_config() {
let toml_str = r#"
peers = [
"129.212.138.135:10000",
"134.199.138.183:10000",
]
"#;
let config: BootstrapConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.peers.len(), 2);
let addr: SocketAddr = config.peers[0].parse().unwrap();
assert_eq!(addr.port(), 10000);
}
}