use crate::data::client::ClientConfig;
use crate::data::error::{Error, Result};
use crate::data::Client;
use ant_node::devnet::{Devnet, DevnetConfig};
use ant_protocol::evm::testnet::Testnet;
use ant_protocol::evm::{Network as EvmNetwork, Wallet};
use ant_protocol::transport::MultiAddr;
use ant_protocol::{DevnetEvmInfo, DevnetManifest};
use std::path::Path;
use std::time::SystemTime;
use tracing::info;
pub struct LocalDevnet {
devnet: Devnet,
_testnet: Testnet,
manifest: DevnetManifest,
evm_network: EvmNetwork,
wallet_private_key: String,
bootstrap: Vec<MultiAddr>,
}
impl LocalDevnet {
pub async fn start(mut config: DevnetConfig) -> Result<Self> {
info!("Starting local Anvil blockchain...");
let testnet = Testnet::new()
.await
.map_err(|e| Error::Config(format!("failed to start Anvil testnet: {e}")))?;
let network = testnet.to_network();
let wallet_key = testnet
.default_wallet_private_key()
.map_err(|e| Error::Config(format!("failed to get wallet key: {e}")))?;
let (rpc_url, token_addr, vault_addr) = extract_custom_network_info(&network)?;
config.evm_network = Some(network.clone());
info!("Anvil running at {rpc_url}");
let mut devnet = Devnet::new(config)
.await
.map_err(|e| Error::Config(format!("devnet creation failed: {e}")))?;
devnet
.start()
.await
.map_err(|e| Error::Network(format!("devnet start failed: {e}")))?;
let bootstrap = devnet.bootstrap_addrs();
let evm_info = DevnetEvmInfo {
rpc_url,
wallet_private_key: wallet_key.clone(),
payment_token_address: token_addr,
payment_vault_address: vault_addr,
};
let manifest = DevnetManifest {
base_port: devnet.config().base_port,
node_count: devnet.config().node_count,
bootstrap: bootstrap.clone(),
data_dir: devnet.config().data_dir.clone(),
created_at: current_timestamp(),
evm: Some(evm_info),
};
info!(
"Devnet running: {node_count} nodes, bootstrap: {bootstrap:?}",
node_count = devnet.config().node_count,
);
Ok(Self {
devnet,
_testnet: testnet,
manifest,
evm_network: network,
wallet_private_key: wallet_key,
bootstrap,
})
}
pub async fn start_minimal() -> Result<Self> {
Self::start(DevnetConfig::minimal()).await
}
pub async fn start_small() -> Result<Self> {
Self::start(DevnetConfig::small()).await
}
#[must_use]
pub fn bootstrap_addrs(&self) -> Vec<std::net::SocketAddr> {
self.bootstrap
.iter()
.filter_map(MultiAddr::socket_addr)
.collect()
}
#[must_use]
pub fn evm_network(&self) -> &EvmNetwork {
&self.evm_network
}
#[must_use]
pub fn wallet_private_key(&self) -> &str {
&self.wallet_private_key
}
#[must_use]
pub fn manifest(&self) -> &DevnetManifest {
&self.manifest
}
pub async fn write_manifest(&self, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(&self.manifest)
.map_err(|e| Error::Serialization(format!("manifest serialization failed: {e}")))?;
tokio::fs::write(path, json).await?;
info!("Wrote manifest to {}", path.display());
Ok(())
}
pub async fn create_funded_client(&self) -> Result<Client> {
let addrs = self.bootstrap_addrs();
let client = Client::connect(&addrs, ClientConfig::default()).await?;
let key = self.wallet_private_key.trim_start_matches("0x").to_string();
let wallet = Wallet::new_from_private_key(self.evm_network.clone(), &key)
.map_err(|e| Error::Payment(format!("wallet creation failed: {e}")))?;
let client = client.with_wallet(wallet);
client.approve_token_spend().await?;
Ok(client)
}
pub async fn shutdown(&mut self) -> Result<()> {
self.devnet
.shutdown()
.await
.map_err(|e| Error::Network(format!("devnet shutdown failed: {e}")))?;
info!("Devnet shut down");
Ok(())
}
}
fn extract_custom_network_info(network: &EvmNetwork) -> Result<(String, String, String)> {
match network {
EvmNetwork::Custom(custom) => {
let token = custom.payment_token_address;
let vault = custom.payment_vault_address;
Ok((
custom.rpc_url_http.to_string(),
format!("{token:?}"),
format!("{vault:?}"),
))
}
_ => Err(Error::Config(
"Anvil testnet returned non-Custom network".to_string(),
)),
}
}
fn current_timestamp() -> String {
let duration = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
format!("{secs}")
}