layer_climb_cli/
handle.rs

1use layer_climb::prelude::*;
2use std::process::{Command, Stdio};
3
4/// This is just a simple helper for running a Docker container with wasmd and cleaning up when done
5/// useful for integration tests that need a chain running
6///
7/// More advanced use-cases with other chains or more control should use third-party tools
8///
9/// This instance represents a running Docker container. When dropped, it will attempt
10/// to kill (and remove) the container automatically.
11pub struct CosmosInstance {
12    pub chain_config: ChainConfig,
13    pub genesis_addresses: Vec<Address>,
14    // the name for docker container and volume names, default is "climb-test-{chain_id}"
15    pub name: String,
16    // StdioKind::Null by default, can be set to StdioKind::Inherit to see logs
17    pub stdout: StdioKind,
18    // StdioKind::Null by default, can be set to StdioKind::Inherit to see logs
19    pub stderr: StdioKind,
20    // the block time to use in the chain, default is "200ms"
21    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    // simple all-in-one command
54    // will return the block height that the chain is at when it is ready
55    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        // first clean up any old instances
63        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                .last()
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                .last()
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}