testcontainers_modules/anvil/
mod.rs

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