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