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::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}