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::other("Failed to setup chain"));
99 }
100
101 let res = Command::new("docker")
102 .args([
103 "run",
104 "--rm",
105 "--name",
106 &self.name,
107 "--mount",
108 &format!("type=volume,source={}_data,target=/root", self.name),
109 self.image.as_str(),
110 "sed",
111 "-E",
112 "-i",
113 &format!(
114 "/timeout_(propose|prevote|precommit|commit)/s/[0-9]+m?s/{}/",
115 self.block_time
116 ),
117 "/root/.wasmd/config/config.toml",
118 ])
119 .stdout(self.stdout)
120 .stderr(self.stderr)
121 .spawn()?
122 .wait()?;
123
124 if !res.success() {
125 Err(std::io::Error::other("Failed to setup chain"))
126 } else {
127 Ok(())
128 }
129 }
130
131 pub fn run(&self) -> std::io::Result<()> {
132 let mut ports = vec![("26656", "26656"), ("1317", "1317")];
133
134 if let Some(rpc_endpoint) = &self.chain_config.rpc_endpoint {
135 let rpc_port = rpc_endpoint
136 .split(':')
137 .next_back()
138 .expect("could not get rpc port");
139 ports.push((rpc_port, "26657"));
140 }
141
142 if let Some(grpc_endpoint) = &self.chain_config.grpc_endpoint {
143 let grpc_port = grpc_endpoint
144 .split(':')
145 .next_back()
146 .expect("could not get grpc port");
147 ports.push((grpc_port, "9090"));
148 }
149
150 let mut args: Vec<String> = ["run", "-d", "--name", &self.name]
151 .into_iter()
152 .map(|s| s.to_string())
153 .collect();
154
155 for (host_port, container_port) in ports {
156 args.push("-p".to_string());
157 args.push(format!("{host_port}:{container_port}"));
158 }
159
160 args.extend_from_slice(
161 [
162 "--mount",
163 &format!("type=volume,source={}_data,target=/root", &self.name),
164 self.image.as_str(),
165 "/opt/run_wasmd.sh",
166 ]
167 .into_iter()
168 .map(|s| s.to_string())
169 .collect::<Vec<_>>()
170 .as_slice(),
171 );
172
173 let res = Command::new("docker").args(args).spawn()?.wait()?;
174
175 if !res.success() {
176 Err(std::io::Error::other("Failed to setup chain"))
177 } else {
178 Ok(())
179 }
180 }
181
182 pub async fn wait_for_block(&self) -> anyhow::Result<u64> {
183 let query_client = QueryClient::new(
184 self.chain_config.clone(),
185 Some(Connection {
186 preferred_mode: Some(ConnectionMode::Rpc),
187 ..Default::default()
188 }),
189 )
190 .await?;
191
192 tokio::time::timeout(std::time::Duration::from_secs(10), async {
193 loop {
194 let block_height = query_client.block_height().await.unwrap_or_default();
195 if block_height > 0 {
196 break block_height;
197 }
198 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
199 }
200 })
201 .await
202 .map_err(|_| anyhow::anyhow!("Timeout waiting for block"))
203 }
204
205 pub fn clean(&self) {
206 if let Ok(mut child) = std::process::Command::new("docker")
207 .args(["kill", &self.name])
208 .stdout(self.stdout)
209 .stderr(self.stderr)
210 .spawn()
211 {
212 let _ = child.wait();
213 }
214
215 if let Ok(mut child) = Command::new("docker")
216 .args(["rm", &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(["volume", "rm", "-f", &format!("{}_data", self.name)])
226 .stdout(self.stdout)
227 .stderr(self.stderr)
228 .spawn()
229 {
230 let _ = child.wait();
231 }
232 }
233}
234
235impl Drop for CosmosInstance {
236 fn drop(&mut self) {
237 self.clean();
238 }
239}