layer_climb_cli/
handle.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
use layer_climb::prelude::*;
use std::process::{Command, Stdio};

/// This is just a simple helper for running a Docker container with wasmd and cleaning up when done
/// useful for integration tests that need a chain running
///
/// More advanced use-cases with other chains or more control should use third-party tools
///
/// This instance represents a running Docker container. When dropped, it will attempt
/// to kill (and remove) the container automatically.
pub struct CosmosInstance {
    pub chain_config: ChainConfig,
    pub genesis_addresses: Vec<Address>,
    // the name for docker container and volume names, default is "climb-test-{chain_id}"
    pub name: String,
    // StdioKind::Null by default, can be set to StdioKind::Inherit to see logs
    pub stdout: StdioKind,
    // StdioKind::Null by default, can be set to StdioKind::Inherit to see logs
    pub stderr: StdioKind,
    // the block time to use in the chain, default is "200ms"
    pub block_time: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StdioKind {
    Null,
    Inherit,
    Piped,
}

impl From<StdioKind> for Stdio {
    fn from(kind: StdioKind) -> Stdio {
        match kind {
            StdioKind::Null => Stdio::null(),
            StdioKind::Inherit => Stdio::inherit(),
            StdioKind::Piped => Stdio::piped(),
        }
    }
}

impl CosmosInstance {
    pub fn new(chain_config: ChainConfig, genesis_addresses: Vec<Address>) -> Self {
        Self {
            name: format!("climb-test-{}", chain_config.chain_id),
            chain_config,
            genesis_addresses,
            stdout: StdioKind::Null,
            stderr: StdioKind::Null,
            block_time: "200ms".to_string(),
        }
    }

    // simple all-in-one command
    // will return the block height that the chain is at when it is ready
    pub async fn start(&self) -> anyhow::Result<u64> {
        self.setup()?;
        self.run()?;
        self.wait_for_block().await
    }

    pub fn setup(&self) -> std::io::Result<()> {
        // first clean up any old instances
        self.clean();

        let mut args: Vec<String> = [
            "run",
            "--rm",
            "--name",
            &self.name,
            "--mount",
            &format!("type=volume,source={}_data,target=/root", self.name),
            "--env",
            &format!("CHAIN_ID={}", self.chain_config.chain_id),
            "--env",
            &format!("FEE_TOKEN={}", self.chain_config.gas_denom),
            "cosmwasm/wasmd:latest",
            "/opt/setup_wasmd.sh",
        ]
        .into_iter()
        .map(|s| s.to_string())
        .collect();

        for addr in self.genesis_addresses.iter() {
            args.push(addr.to_string());
        }

        let res = Command::new("docker")
            .args(args)
            .stdout(self.stdout)
            .stderr(self.stderr)
            .spawn()?
            .wait()?;

        if !res.success() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "Failed to setup chain",
            ));
        }

        let res = Command::new("docker")
            .args([
                "run",
                "--rm",
                "--name",
                &self.name,
                "--mount",
                &format!("type=volume,source={}_data,target=/root", self.name),
                "cosmwasm/wasmd:latest",
                "sed",
                "-E",
                "-i",
                &format!(
                    "/timeout_(propose|prevote|precommit|commit)/s/[0-9]+m?s/{}/",
                    self.block_time
                ),
                "/root/.wasmd/config/config.toml",
            ])
            .stdout(self.stdout)
            .stderr(self.stderr)
            .spawn()?
            .wait()?;

        if !res.success() {
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "Failed to setup chain",
            ))
        } else {
            Ok(())
        }
    }

    pub fn run(&self) -> std::io::Result<()> {
        let mut ports = vec![("26656", "26656"), ("1317", "1317")];

        if let Some(rpc_endpoint) = &self.chain_config.rpc_endpoint {
            let rpc_port = rpc_endpoint
                .split(':')
                .last()
                .expect("could not get rpc port");
            ports.push((rpc_port, "26657"));
        }

        if let Some(grpc_endpoint) = &self.chain_config.grpc_endpoint {
            let grpc_port = grpc_endpoint
                .split(':')
                .last()
                .expect("could not get grpc port");
            ports.push((grpc_port, "9090"));
        }

        let mut args: Vec<String> = ["run", "-d", "--name", &self.name]
            .into_iter()
            .map(|s| s.to_string())
            .collect();

        for (host_port, container_port) in ports {
            args.push("-p".to_string());
            args.push(format!("{}:{}", host_port, container_port));
        }

        args.extend_from_slice(
            [
                "--mount",
                &format!("type=volume,source={}_data,target=/root", &self.name),
                "cosmwasm/wasmd:latest",
                "/opt/run_wasmd.sh",
            ]
            .into_iter()
            .map(|s| s.to_string())
            .collect::<Vec<_>>()
            .as_slice(),
        );

        let res = Command::new("docker").args(args).spawn()?.wait()?;

        if !res.success() {
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "Failed to setup chain",
            ))
        } else {
            Ok(())
        }
    }

    pub async fn wait_for_block(&self) -> anyhow::Result<u64> {
        let query_client = QueryClient::new(
            self.chain_config.clone(),
            Some(Connection {
                preferred_mode: Some(ConnectionMode::Rpc),
                ..Default::default()
            }),
        )
        .await?;

        tokio::time::timeout(std::time::Duration::from_secs(10), async {
            loop {
                let block_height = query_client.block_height().await.unwrap_or_default();
                if block_height > 0 {
                    break block_height;
                }
                tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
            }
        })
        .await
        .map_err(|_| anyhow::anyhow!("Timeout waiting for block"))
    }

    pub fn clean(&self) {
        if let Ok(mut child) = std::process::Command::new("docker")
            .args(["kill", &self.name])
            .stdout(self.stdout)
            .stderr(self.stderr)
            .spawn()
        {
            let _ = child.wait();
        }

        if let Ok(mut child) = Command::new("docker")
            .args(["rm", &self.name])
            .stdout(self.stdout)
            .stderr(self.stderr)
            .spawn()
        {
            let _ = child.wait();
        }

        if let Ok(mut child) = Command::new("docker")
            .args(["volume", "rm", "-f", &format!("{}_data", self.name)])
            .stdout(self.stdout)
            .stderr(self.stderr)
            .spawn()
        {
            let _ = child.wait();
        }
    }
}

impl Drop for CosmosInstance {
    fn drop(&mut self) {
        self.clean();
    }
}