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    // the image to use for the container, default is "cosmwasm/wasmd:latest"
23    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    // simple all-in-one command
57    // will return the block height that the chain is at when it is ready
58    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        // first clean up any old instances
66        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}