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>>> {
Ok(load_bootstrap_multiaddrs()?
.map(|peers| peers.iter().filter_map(MultiAddr::socket_addr).collect()))
}
pub fn load_bootstrap_multiaddrs() -> Result<Option<Vec<MultiAddr>>> {
let path = config_dir()?.join("bootstrap_peers.toml");
if !path.exists() {
return Ok(None);
}
let seeds = crate::network_defaults::parse_bootstrap_seeds(&std::fs::read_to_string(path)?)
.map_err(|e| Error::BootstrapConfigParse(e.to_string()))?;
Ok((!seeds.quic.is_empty()).then_some(seeds.quic))
}
pub fn resolve_bootstrap_multiaddrs(
explicit: &[MultiAddr],
manifest: Option<&DevnetManifest>,
) -> Result<Vec<MultiAddr>> {
let peers = if !explicit.is_empty() {
explicit
.iter()
.map(|addr| crate::network_defaults::parse_quic_seed(&addr.to_string()))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| Error::BootstrapConfigParse(e.to_string()))?
} else if let Some(manifest) = manifest {
manifest
.bootstrap
.iter()
.filter(|addr| addr.is_quic())
.cloned()
.collect()
} else if let Some(peers) = load_bootstrap_multiaddrs()? {
peers
} else {
let seeds = crate::network_defaults::bundled_bootstrap_seeds()
.map_err(|e| Error::BootstrapConfigParse(e.to_string()))?;
tracing::info!(
"No bootstrap config file; using {} bundled bootstrap seed(s)",
seeds.quic.len()
);
seeds.quic
};
if peers.is_empty() {
return Err(Error::NoBootstrapPeers);
}
Ok(peers)
}
pub fn resolve_bootstrap_peers(
explicit: &[SocketAddr],
manifest: Option<&DevnetManifest>,
) -> Result<Vec<SocketAddr>> {
let explicit: Vec<_> = explicit.iter().copied().map(MultiAddr::quic).collect();
Ok(resolve_bootstrap_multiaddrs(&explicit, manifest)?
.iter()
.filter_map(MultiAddr::socket_addr)
.collect())
}
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())),
}
}
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 = crate::network_defaults::parse_bootstrap_seeds(toml_str).unwrap();
assert_eq!(config.quic.len(), 2);
let addr = config.quic[0].socket_addr().unwrap();
assert_eq!(addr.port(), 10000);
}
#[test]
fn multiaddr_resolution_preserves_pins_and_never_falls_back_from_manifest() {
let pin = format!("/ip4/127.0.0.1/udp/10000/quic/p2p/{}", "ab".repeat(32));
let seed: MultiAddr = pin.parse().unwrap();
let mut manifest = test_manifest(vec![]);
manifest.bootstrap = vec![seed.clone()];
assert_eq!(
resolve_bootstrap_multiaddrs(&[], Some(&manifest)).unwrap()[0].to_string(),
pin
);
assert_eq!(
resolve_bootstrap_multiaddrs(&[seed], Some(&test_manifest(vec![]))).unwrap()[0]
.to_string(),
pin
);
assert!(matches!(
resolve_bootstrap_multiaddrs(&[], Some(&test_manifest(vec![]))),
Err(Error::NoBootstrapPeers)
));
}
}