Skip to content

Commit b179b48

Browse files
authored
feat: add Foundry Anvil image impl (#272)
# 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.
1 parent d4a6151 commit b179b48

File tree

3 files changed

+149
-1
lines changed

3 files changed

+149
-1
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ blocking = ["testcontainers/blocking"]
1919
watchdog = ["testcontainers/watchdog"]
2020
http_wait = ["testcontainers/http_wait"]
2121
properties-config = ["testcontainers/properties-config"]
22+
anvil = []
2223
clickhouse = ["http_wait"]
2324
cncf_distribution = []
2425
consul = []
@@ -71,6 +72,9 @@ testcontainers = { version = "0.23.0" }
7172

7273

7374
[dev-dependencies]
75+
alloy-network = "0.9.2"
76+
alloy-provider = "0.9.2"
77+
alloy-transport-http = "0.9.2"
7478
async-nats = "0.38.0"
7579
aws-config = "1.0.1"
7680
aws-sdk-dynamodb = "1.2.0"

src/anvil/mod.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use std::borrow::Cow;
2+
3+
use testcontainers::{
4+
core::{ContainerPort, WaitFor},
5+
Image,
6+
};
7+
8+
const NAME: &str = "ghcr.io/foundry-rs/foundry";
9+
/// 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).
10+
const TAG: &str = "stable@sha256:daeeaaf4383ee0cbfc9f31f079a04ffb0123e49e5f67f2a20b5ce1ac1959a4d6";
11+
const PORT: ContainerPort = ContainerPort::Tcp(8545);
12+
13+
/// # Community Testcontainers Implementation for [Foundry Anvil](https://book.getfoundry.sh/anvil/)
14+
///
15+
/// This is a community implementation of the [Testcontainers](https://testcontainers.org/) interface for [Foundry Anvil](https://book.getfoundry.sh/anvil/).
16+
///
17+
/// 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.
18+
///
19+
/// 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.
20+
/// See the `test_anvil_node_container` test for an example of how to use this.
21+
#[derive(Debug, Clone, Default)]
22+
pub struct AnvilNode {
23+
chain_id: Option<u64>,
24+
fork_url: Option<String>,
25+
fork_block_number: Option<u64>,
26+
}
27+
28+
impl AnvilNode {
29+
/// Specify the chain ID - this will be Ethereum Mainnet by default
30+
pub fn with_chain_id(mut self, chain_id: u64) -> Self {
31+
self.chain_id = Some(chain_id);
32+
self
33+
}
34+
35+
/// Specify the fork URL
36+
pub fn with_fork_url(mut self, fork_url: impl Into<String>) -> Self {
37+
self.fork_url = Some(fork_url.into());
38+
self
39+
}
40+
41+
/// Specify the fork block number
42+
pub fn with_fork_block_number(mut self, block_number: u64) -> Self {
43+
self.fork_block_number = Some(block_number);
44+
self
45+
}
46+
}
47+
48+
impl Image for AnvilNode {
49+
fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
50+
let mut cmd = vec![];
51+
52+
if let Some(chain_id) = self.chain_id {
53+
cmd.push("--chain-id".to_string());
54+
cmd.push(chain_id.to_string());
55+
}
56+
57+
if let Some(ref fork_url) = self.fork_url {
58+
cmd.push("--fork-url".to_string());
59+
cmd.push(fork_url.to_string());
60+
}
61+
62+
if let Some(fork_block_number) = self.fork_block_number {
63+
cmd.push("--fork-block-number".to_string());
64+
cmd.push(fork_block_number.to_string());
65+
}
66+
67+
cmd.into_iter().map(Cow::from)
68+
}
69+
70+
fn entrypoint(&self) -> Option<&str> {
71+
Some("anvil")
72+
}
73+
74+
fn env_vars(
75+
&self,
76+
) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
77+
[("ANVIL_IP_ADDR".to_string(), "0.0.0.0".to_string())].into_iter()
78+
}
79+
80+
fn expose_ports(&self) -> &[ContainerPort] {
81+
&[PORT]
82+
}
83+
84+
fn name(&self) -> &str {
85+
NAME
86+
}
87+
88+
fn tag(&self) -> &str {
89+
TAG
90+
}
91+
92+
fn ready_conditions(&self) -> Vec<WaitFor> {
93+
vec![WaitFor::message_on_stdout("Listening on 0.0.0.0:8545")]
94+
}
95+
}
96+
97+
#[cfg(test)]
98+
mod tests {
99+
use alloy_network::AnyNetwork;
100+
use alloy_provider::{Provider, RootProvider};
101+
use alloy_transport_http::Http;
102+
use testcontainers::runners::AsyncRunner;
103+
104+
use super::*;
105+
106+
#[tokio::test]
107+
async fn test_anvil_node_container() {
108+
let _ = pretty_env_logger::try_init();
109+
110+
let node = AnvilNode::default().start().await.unwrap();
111+
let port = node.get_host_port_ipv4(PORT).await.unwrap();
112+
113+
let provider: RootProvider<Http<_>, AnyNetwork> =
114+
RootProvider::new_http(format!("http://localhost:{port}").parse().unwrap());
115+
116+
let block_number = provider.get_block_number().await.unwrap();
117+
118+
assert_eq!(block_number, 0);
119+
}
120+
121+
#[test]
122+
fn test_command_construction() {
123+
let node = AnvilNode::default()
124+
.with_chain_id(1337)
125+
.with_fork_url("http://example.com");
126+
127+
let cmd: Vec<String> = node
128+
.cmd()
129+
.into_iter()
130+
.map(|c| c.into().into_owned())
131+
.collect();
132+
133+
assert_eq!(
134+
cmd,
135+
vec!["--chain-id", "1337", "--fork-url", "http://example.com"]
136+
);
137+
138+
assert_eq!(node.entrypoint(), Some("anvil"));
139+
}
140+
}

src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
#![doc = include_str!("../README.md")]
99
//! Please have a look at the documentation of the separate modules for examples on how to use the module.
1010
11+
#[cfg(feature = "anvil")]
12+
#[cfg_attr(docsrs, doc(cfg(feature = "anvil")))]
13+
/// **Anvil** (local blockchain emulator for EVM-compatible development) testcontainer
14+
pub mod anvil;
1115
#[cfg(feature = "clickhouse")]
1216
#[cfg_attr(docsrs, doc(cfg(feature = "clickhouse")))]
1317
/// **Clickhouse** (analytics database) testcontainer
@@ -150,7 +154,7 @@ pub mod solr;
150154
pub mod surrealdb;
151155
#[cfg(feature = "trufflesuite_ganachecli")]
152156
#[cfg_attr(docsrs, doc(cfg(feature = "trufflesuite_ganachecli")))]
153-
/// **Trufflesuite Ganache CLI** (etherium simulator) testcontainer
157+
/// **Trufflesuite Ganache CLI** (ethereum simulator) testcontainer
154158
pub mod trufflesuite_ganachecli;
155159
#[cfg(feature = "valkey")]
156160
#[cfg_attr(docsrs, doc(cfg(feature = "valkey")))]

0 commit comments

Comments
 (0)