1use layer_climb::prelude::*;
2use std::process::{Command, Stdio};
3
4pub struct CosmosInstance {
12 pub chain_config: ChainConfig,
13 pub genesis_addresses: Vec<Address>,
14 pub name: String,
16 pub stdout: StdioKind,
18 pub stderr: StdioKind,
20 pub block_time: String,
22 pub image: String
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StdioKind {
28 Null,
29 Inherit,
30 Piped,
31}
32
33impl From<StdioKind> for Stdio {
34 fn from(kind: StdioKind) -> Stdio {
35 match kind {
36 StdioKind::Null => Stdio::null(),
37 StdioKind::Inherit => Stdio::inherit(),
38 StdioKind::Piped => Stdio::piped(),
39 }
40 }
41}
42
43impl CosmosInstance {
44 pub fn new(chain_config: ChainConfig, genesis_addresses: Vec<Address>) -> Self {
45 Self {
46 name: format!("climb-test-{}", chain_config.chain_id),
47 chain_config,
48 genesis_addresses,
49 stdout: StdioKind::Null,
50 stderr: StdioKind::Null,
51 block_time: "200ms".to_string(),
52 image: "cosmwasm/wasmd:latest".to_string(),
53 }
54 }
55
56 pub async fn start(&self) -> anyhow::Result<u64> {
59 self.setup()?;
60 self.run()?;
61 self.wait_for_block().await
62 }
63
64 pub fn setup(&self) -> std::io::Result<()> {
65 self.clean();
67
68 let mut args: Vec<String> = [
69 "run",
70 "--rm",
71 "--name",
72 &self.name,
73 "--mount",
74 &format!("type=volume,source={}_data,target=/root", self.name),
75 "--env",
76 &format!("CHAIN_ID={}", self.chain_config.chain_id),
77 "--env",
78 &format!("FEE_TOKEN={}", self.chain_config.gas_denom),
79 self.image.as_str(),
80 "/opt/setup_wasmd.sh",
81 ]
82 .into_iter()
83 .map(|s| s.to_string())
84 .collect();
85
86 for addr in self.genesis_addresses.iter() {
87 args.push(addr.to_string());
88 }
89
90 let res = Command::new("docker")
91 .args(args)
92 .stdout(self.stdout)
93 .stderr(self.stderr)
94 .spawn()?
95 .wait()?;
96
97 if !res.success() {
98 return Err(std::io::Error::new(
99 std::io::ErrorKind::Other,
100 "Failed to setup chain",
101 ));
102 }
103
104 let res = Command::new("docker")
105 .args([
106 "run",
107 "--rm",
108 "--name",
109 &self.name,
110 "--mount",
111 &format!("type=volume,source={}_data,target=/root", self.name),
112 self.image.as_str(),
113 "sed",
114 "-E",
115 "-i",
116 &format!(
117 "/timeout_(propose|prevote|precommit|commit)/s/[0-9]+m?s/{}/",
118 self.block_time
119 ),
120 "/root/.wasmd/config/config.toml",
121 ])
122 .stdout(self.stdout)
123 .stderr(self.stderr)
124 .spawn()?
125 .wait()?;
126
127 if !res.success() {
128 Err(std::io::Error::new(
129 std::io::ErrorKind::Other,
130 "Failed to setup chain",
131 ))
132 } else {
133 Ok(())
134 }
135 }
136
137 pub fn run(&self) -> std::io::Result<()> {
138 let mut ports = vec![("26656", "26656"), ("1317", "1317")];
139
140 if let Some(rpc_endpoint) = &self.chain_config.rpc_endpoint {
141 let rpc_port = rpc_endpoint
142 .split(':')
143 .next_back()
144 .expect("could not get rpc port");
145 ports.push((rpc_port, "26657"));
146 }
147
148 if let Some(grpc_endpoint) = &self.chain_config.grpc_endpoint {
149 let grpc_port = grpc_endpoint
150 .split(':')
151 .next_back()
152 .expect("could not get grpc port");
153 ports.push((grpc_port, "9090"));
154 }
155
156 let mut args: Vec<String> = ["run", "-d", "--name", &self.name]
157 .into_iter()
158 .map(|s| s.to_string())
159 .collect();
160
161 for (host_port, container_port) in ports {
162 args.push("-p".to_string());
163 args.push(format!("{}:{}", host_port, container_port));
164 }
165
166 args.extend_from_slice(
167 [
168 "--mount",
169 &format!("type=volume,source={}_data,target=/root", &self.name),
170 self.image.as_str(),
171 "/opt/run_wasmd.sh",
172 ]
173 .into_iter()
174 .map(|s| s.to_string())
175 .collect::<Vec<_>>()
176 .as_slice(),
177 );
178
179 let res = Command::new("docker").args(args).spawn()?.wait()?;
180
181 if !res.success() {
182 Err(std::io::Error::new(
183 std::io::ErrorKind::Other,
184 "Failed to setup chain",
185 ))
186 } else {
187 Ok(())
188 }
189 }
190
191 pub async fn wait_for_block(&self) -> anyhow::Result<u64> {
192 let query_client = QueryClient::new(
193 self.chain_config.clone(),
194 Some(Connection {
195 preferred_mode: Some(ConnectionMode::Rpc),
196 ..Default::default()
197 }),
198 )
199 .await?;
200
201 tokio::time::timeout(std::time::Duration::from_secs(10), async {
202 loop {
203 let block_height = query_client.block_height().await.unwrap_or_default();
204 if block_height > 0 {
205 break block_height;
206 }
207 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
208 }
209 })
210 .await
211 .map_err(|_| anyhow::anyhow!("Timeout waiting for block"))
212 }
213
214 pub fn clean(&self) {
215 if let Ok(mut child) = std::process::Command::new("docker")
216 .args(["kill", &self.name])
217 .stdout(self.stdout)
218 .stderr(self.stderr)
219 .spawn()
220 {
221 let _ = child.wait();
222 }
223
224 if let Ok(mut child) = Command::new("docker")
225 .args(["rm", &self.name])
226 .stdout(self.stdout)
227 .stderr(self.stderr)
228 .spawn()
229 {
230 let _ = child.wait();
231 }
232
233 if let Ok(mut child) = Command::new("docker")
234 .args(["volume", "rm", "-f", &format!("{}_data", self.name)])
235 .stdout(self.stdout)
236 .stderr(self.stderr)
237 .spawn()
238 {
239 let _ = child.wait();
240 }
241 }
242}
243
244impl Drop for CosmosInstance {
245 fn drop(&mut self) {
246 self.clean();
247 }
248}