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 = "stable@sha256:daeeaaf4383ee0cbfc9f31f079a04ffb0123e49e5f67f2a20b5ce1ac1959a4d6";
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 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}