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 = "v1.1.0";
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 testcontainers::runners::AsyncRunner;
122
123    use super::*;
124
125    #[tokio::test]
126    async fn test_anvil_node_container() {
127        let _ = pretty_env_logger::try_init();
128
129        let node = AnvilNode::default().start().await.unwrap();
130        let port = node.get_host_port_ipv4(PORT).await.unwrap();
131
132        let provider: RootProvider<AnyNetwork> =
133            RootProvider::new_http(format!("http://localhost:{port}").parse().unwrap());
134
135        let block_number = provider.get_block_number().await.unwrap();
136
137        assert_eq!(block_number, 0);
138    }
139
140    #[test]
141    fn test_command_construction() {
142        let node = AnvilNode::default()
143            .with_chain_id(1337)
144            .with_fork_url("http://example.com");
145
146        let cmd: Vec<String> = node
147            .cmd()
148            .into_iter()
149            .map(|c| c.into().into_owned())
150            .collect();
151
152        assert_eq!(
153            cmd,
154            vec!["--chain-id", "1337", "--fork-url", "http://example.com"]
155        );
156
157        assert_eq!(node.entrypoint(), Some("anvil"));
158    }
159}