testcontainers_modules/anvil/
mod.rs1use 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#[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 pub fn latest() -> Self {
43 Self {
44 tag: Some("latest".to_string()),
45 ..Default::default()
46 }
47 }
48
49 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
51 self.chain_id = Some(chain_id);
52 self
53 }
54
55 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 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}