use std::env;
use std::num::ParseIntError;
use crate::common::Address;
use crate::contract::payment_vault::handler::PaymentVaultHandler;
use crate::contract::{network_token::NetworkToken, payment_vault};
use crate::reqwest::Url;
use crate::{CustomNetwork, Network};
use alloy::hex::ToHexExt;
use alloy::network::{Ethereum, EthereumWallet};
use alloy::node_bindings::{Anvil, AnvilInstance};
use alloy::providers::fillers::{
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
SimpleNonceManager, WalletFiller,
};
use alloy::providers::{Identity, ProviderBuilder, RootProvider};
use alloy::signers::local::PrivateKeySigner;
#[derive(Debug, thiserror::Error)]
pub enum TestnetError {
#[error("ANVIL_PORT must be a valid u16: {0}")]
InvalidPort(#[from] ParseIntError),
#[error("Could not spawn Anvil node: {0}")]
SpawnFailed(String),
#[error("Failed to parse Anvil RPC URL: {0}")]
InvalidUrl(String),
#[error("Anvil key at index {0} not available")]
MissingKey(usize),
}
pub struct Testnet {
anvil: AnvilInstance,
rpc_url: Url,
network_token_address: Address,
payment_vault_address: Address,
}
impl Testnet {
pub async fn new() -> Result<Self, TestnetError> {
let (node, rpc_url) = start_node()?;
let network_token = deploy_network_token_contract(&rpc_url, &node).await?;
let payment_vault =
deploy_payment_vault_contract(&rpc_url, &node, *network_token.contract.address())
.await?;
Ok(Testnet {
anvil: node,
rpc_url,
network_token_address: *network_token.contract.address(),
payment_vault_address: *payment_vault.contract.address(),
})
}
pub fn to_network(&self) -> Network {
Network::Custom(CustomNetwork {
rpc_url_http: self.rpc_url.clone(),
payment_token_address: self.network_token_address,
payment_vault_address: self.payment_vault_address,
})
}
pub fn default_wallet_private_key(&self) -> Result<String, TestnetError> {
let key = self
.anvil
.keys()
.first()
.ok_or(TestnetError::MissingKey(0))?;
let signer: PrivateKeySigner = key.clone().into();
Ok(signer.to_bytes().encode_hex_with_prefix())
}
pub fn payment_vault_address(&self) -> Address {
self.payment_vault_address
}
}
pub fn start_node() -> Result<(AnvilInstance, Url), TestnetError> {
let host = env::var("ANVIL_IP_ADDR").unwrap_or_else(|_| "localhost".to_string());
let mut builder = Anvil::new();
if let Ok(port_str) = env::var("ANVIL_PORT") {
let port = port_str.parse::<u16>()?;
builder = builder.port(port);
}
let anvil = builder
.try_spawn()
.map_err(|e| TestnetError::SpawnFailed(e.to_string()))?;
let port = anvil.port();
let url = Url::parse(&format!("http://{host}:{port}"))
.map_err(|e| TestnetError::InvalidUrl(e.to_string()))?;
Ok((anvil, url))
}
pub async fn deploy_network_token_contract(
rpc_url: &Url,
anvil: &AnvilInstance,
) -> Result<
NetworkToken<
FillProvider<
JoinFill<
JoinFill<
JoinFill<
Identity,
JoinFill<
GasFiller,
JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>,
>,
>,
NonceFiller<SimpleNonceManager>,
>,
WalletFiller<EthereumWallet>,
>,
RootProvider,
Ethereum,
>,
Ethereum,
>,
TestnetError,
> {
let key = anvil.keys().first().ok_or(TestnetError::MissingKey(0))?;
let signer: PrivateKeySigner = key.clone().into();
let wallet = EthereumWallet::from(signer);
let provider = ProviderBuilder::new()
.with_simple_nonce_management()
.wallet(wallet)
.connect_http(rpc_url.clone());
Ok(NetworkToken::deploy(provider).await)
}
pub async fn deploy_payment_vault_contract(
rpc_url: &Url,
anvil: &AnvilInstance,
token_address: Address,
) -> Result<
PaymentVaultHandler<
FillProvider<
JoinFill<
JoinFill<
JoinFill<
Identity,
JoinFill<
GasFiller,
JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>,
>,
>,
NonceFiller<SimpleNonceManager>,
>,
WalletFiller<EthereumWallet>,
>,
RootProvider,
Ethereum,
>,
Ethereum,
>,
TestnetError,
> {
let key = anvil.keys().get(1).ok_or(TestnetError::MissingKey(1))?;
let signer: PrivateKeySigner = key.clone().into();
let wallet = EthereumWallet::from(signer);
let provider = ProviderBuilder::new()
.with_simple_nonce_management()
.wallet(wallet)
.connect_http(rpc_url.clone());
let payment_vault_contract_address =
payment_vault::implementation::deploy(&provider, token_address).await;
Ok(PaymentVaultHandler::new(
payment_vault_contract_address,
provider,
))
}
#[cfg(test)]
mod tests {
use crate::testnet::Testnet;
#[tokio::test]
async fn test_run_multiple_testnets_in_parallel() {
let (_t1, _t2, _t3, _t4) = tokio::join!(
Testnet::new(),
Testnet::new(),
Testnet::new(),
Testnet::new(),
);
_t1.unwrap();
_t2.unwrap();
_t3.unwrap();
_t4.unwrap();
}
}