Skip to content

Commit

Permalink
feat: add Foundry Anvil image impl (#272)
Browse files Browse the repository at this point in the history
# Community Testcontainers Implementation for [Foundry
Anvil](https://book.getfoundry.sh/anvil/)

This is a community implementation of the
[Testcontainers](https://testcontainers.org/) interface for
[Foundry](https://github.com/foundry-rs/foundry)
[Anvil](https://book.getfoundry.sh/anvil/).

It is not officially supported by Foundry, but it is a community effort
to provide a more user-friendly interface for running Anvil inside a
Docker container.

The endpoint of the container is intended to be injected into your
provider configuration, so that you can easily run tests against a local
Anvil instance. See the `test_anvil_node_container` test in
`src/anvil/mod.rs` for an example.
  • Loading branch information
suchapalaver authored Jan 17, 2025
1 parent d4a6151 commit b179b48
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ blocking = ["testcontainers/blocking"]
watchdog = ["testcontainers/watchdog"]
http_wait = ["testcontainers/http_wait"]
properties-config = ["testcontainers/properties-config"]
anvil = []
clickhouse = ["http_wait"]
cncf_distribution = []
consul = []
Expand Down Expand Up @@ -71,6 +72,9 @@ testcontainers = { version = "0.23.0" }


[dev-dependencies]
alloy-network = "0.9.2"
alloy-provider = "0.9.2"
alloy-transport-http = "0.9.2"
async-nats = "0.38.0"
aws-config = "1.0.1"
aws-sdk-dynamodb = "1.2.0"
Expand Down
140 changes: 140 additions & 0 deletions src/anvil/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::borrow::Cow;

use testcontainers::{
core::{ContainerPort, WaitFor},
Image,
};

const NAME: &str = "ghcr.io/foundry-rs/foundry";
/// Users can override the tag in their code with [`ImageExt::with_tag`](https://docs.rs/testcontainers/0.23.1/testcontainers/core/trait.ImageExt.html#tymethod.with_tag).
const TAG: &str = "stable@sha256:daeeaaf4383ee0cbfc9f31f079a04ffb0123e49e5f67f2a20b5ce1ac1959a4d6";
const PORT: ContainerPort = ContainerPort::Tcp(8545);

/// # Community Testcontainers Implementation for [Foundry Anvil](https://book.getfoundry.sh/anvil/)
///
/// This is a community implementation of the [Testcontainers](https://testcontainers.org/) interface for [Foundry Anvil](https://book.getfoundry.sh/anvil/).
///
/// It is not officially supported by Foundry, but it is a community effort to provide a more user-friendly interface for running Anvil inside a Docker container.
///
/// The endpoint of the container is intended to be injected into your provider configuration, so that you can easily run tests against a local Anvil instance.
/// See the `test_anvil_node_container` test for an example of how to use this.
#[derive(Debug, Clone, Default)]
pub struct AnvilNode {
chain_id: Option<u64>,
fork_url: Option<String>,
fork_block_number: Option<u64>,
}

impl AnvilNode {
/// Specify the chain ID - this will be Ethereum Mainnet by default
pub fn with_chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}

/// Specify the fork URL
pub fn with_fork_url(mut self, fork_url: impl Into<String>) -> Self {
self.fork_url = Some(fork_url.into());
self
}

/// Specify the fork block number
pub fn with_fork_block_number(mut self, block_number: u64) -> Self {
self.fork_block_number = Some(block_number);
self
}
}

impl Image for AnvilNode {
fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
let mut cmd = vec![];

if let Some(chain_id) = self.chain_id {
cmd.push("--chain-id".to_string());
cmd.push(chain_id.to_string());
}

if let Some(ref fork_url) = self.fork_url {
cmd.push("--fork-url".to_string());
cmd.push(fork_url.to_string());
}

if let Some(fork_block_number) = self.fork_block_number {
cmd.push("--fork-block-number".to_string());
cmd.push(fork_block_number.to_string());
}

cmd.into_iter().map(Cow::from)
}

fn entrypoint(&self) -> Option<&str> {
Some("anvil")
}

fn env_vars(
&self,
) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
[("ANVIL_IP_ADDR".to_string(), "0.0.0.0".to_string())].into_iter()
}

fn expose_ports(&self) -> &[ContainerPort] {
&[PORT]
}

fn name(&self) -> &str {
NAME
}

fn tag(&self) -> &str {
TAG
}

fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stdout("Listening on 0.0.0.0:8545")]
}
}

#[cfg(test)]
mod tests {
use alloy_network::AnyNetwork;
use alloy_provider::{Provider, RootProvider};
use alloy_transport_http::Http;
use testcontainers::runners::AsyncRunner;

use super::*;

#[tokio::test]
async fn test_anvil_node_container() {
let _ = pretty_env_logger::try_init();

let node = AnvilNode::default().start().await.unwrap();
let port = node.get_host_port_ipv4(PORT).await.unwrap();

let provider: RootProvider<Http<_>, AnyNetwork> =
RootProvider::new_http(format!("http://localhost:{port}").parse().unwrap());

let block_number = provider.get_block_number().await.unwrap();

assert_eq!(block_number, 0);
}

#[test]
fn test_command_construction() {
let node = AnvilNode::default()
.with_chain_id(1337)
.with_fork_url("http://example.com");

let cmd: Vec<String> = node
.cmd()
.into_iter()
.map(|c| c.into().into_owned())
.collect();

assert_eq!(
cmd,
vec!["--chain-id", "1337", "--fork-url", "http://example.com"]
);

assert_eq!(node.entrypoint(), Some("anvil"));
}
}
6 changes: 5 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
#![doc = include_str!("../README.md")]
//! Please have a look at the documentation of the separate modules for examples on how to use the module.
#[cfg(feature = "anvil")]
#[cfg_attr(docsrs, doc(cfg(feature = "anvil")))]
/// **Anvil** (local blockchain emulator for EVM-compatible development) testcontainer
pub mod anvil;
#[cfg(feature = "clickhouse")]
#[cfg_attr(docsrs, doc(cfg(feature = "clickhouse")))]
/// **Clickhouse** (analytics database) testcontainer
Expand Down Expand Up @@ -150,7 +154,7 @@ pub mod solr;
pub mod surrealdb;
#[cfg(feature = "trufflesuite_ganachecli")]
#[cfg_attr(docsrs, doc(cfg(feature = "trufflesuite_ganachecli")))]
/// **Trufflesuite Ganache CLI** (etherium simulator) testcontainer
/// **Trufflesuite Ganache CLI** (ethereum simulator) testcontainer
pub mod trufflesuite_ganachecli;
#[cfg(feature = "valkey")]
#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]
Expand Down

0 comments on commit b179b48

Please sign in to comment.