cw_client/
cli.rs

1use std::process::Command;
2
3use color_eyre::{eyre::eyre, Help, Report, Result};
4use cosmrs::{abci::GasInfo, tendermint::chain::Id, AccountId};
5use reqwest::Url;
6use serde::de::DeserializeOwned;
7
8use crate::CwClient;
9
10#[derive(Clone, Debug)]
11pub enum CliClientType {
12    Wasmd,
13    Neutrond,
14}
15
16impl CliClientType {
17    fn bin(&self) -> String {
18        match self {
19            CliClientType::Wasmd => "wasmd",
20            CliClientType::Neutrond => "neutrond",
21        }
22        .to_string()
23    }
24}
25
26#[derive(Clone, Debug)]
27pub struct CliClient {
28    kind: CliClientType,
29    url: Url,
30    gas_price: String,
31}
32
33impl CliClient {
34    pub fn new(kind: CliClientType, url: Url, gas_price: String) -> Self {
35        Self {
36            kind,
37            url,
38            gas_price,
39        }
40    }
41
42    pub fn wasmd(url: Url) -> Self {
43        Self {
44            kind: CliClientType::Wasmd,
45            url,
46            gas_price: "0.0025ucosm".to_string(),
47        }
48    }
49
50    pub fn neutrond(url: Url) -> Self {
51        Self {
52            kind: CliClientType::Neutrond,
53            url,
54            gas_price: "0.0053untrn".to_string(),
55        }
56    }
57
58    fn new_command(&self) -> Result<Command> {
59        let bin = self.kind.bin();
60        if !self.is_bin_available(&bin) {
61            return Err(eyre!("Binary '{}' not found in PATH", bin)).suggestion(format!(
62                "Have you installed {}? If so, check that it's in your PATH.",
63                bin
64            ));
65        }
66
67        Ok(Command::new(self.kind.bin()))
68    }
69    fn is_bin_available(&self, bin: &str) -> bool {
70        Command::new("which")
71            .arg(bin)
72            .output()
73            .map(|output| output.status.success())
74            .unwrap_or(false)
75    }
76}
77
78#[async_trait::async_trait]
79impl CwClient for CliClient {
80    type Address = AccountId;
81    type Query = serde_json::Value;
82    type RawQuery = String;
83    type ChainId = Id;
84    type Error = Report;
85
86    async fn query_smart<R: DeserializeOwned + Send>(
87        &self,
88        contract: &Self::Address,
89        query: Self::Query,
90    ) -> Result<R, Self::Error> {
91        let mut command = self.new_command()?;
92        let command = command
93            .args(["--node", self.url.as_str()])
94            .args(["query", "wasm"])
95            .args(["contract-state", "smart", contract.as_ref()])
96            .arg(query.to_string())
97            .args(["--output", "json"]);
98
99        let output = command.output()?;
100        if !output.status.success() {
101            return Err(eyre!("{:?}", output));
102        }
103
104        let query_result: R = serde_json::from_slice(&output.stdout)
105            .map_err(|e| eyre!("Error deserializing: {}", e))?;
106        Ok(query_result)
107    }
108
109    async fn query_raw<R: DeserializeOwned + Default>(
110        &self,
111        contract: &Self::Address,
112        query: Self::RawQuery,
113    ) -> Result<R, Self::Error> {
114        let mut command = self.new_command()?;
115        let command = command
116            .args(["--node", self.url.as_str()])
117            .args(["query", "wasm"])
118            .args(["contract-state", "raw", contract.as_ref()])
119            .arg(&query)
120            .args(["--output", "json"]);
121
122        let output = command.output()?;
123        if !output.status.success() {
124            return Err(eyre!("{:?}", output));
125        }
126
127        let query_result: R = serde_json::from_slice(&output.stdout).unwrap_or_default();
128        Ok(query_result)
129    }
130
131    fn query_tx<R: DeserializeOwned + Default>(&self, txhash: &str) -> Result<R, Self::Error> {
132        let mut command = self.new_command()?;
133        let command = command
134            .args(["--node", self.url.as_str()])
135            .args(["query", "tx"])
136            .arg(txhash)
137            .args(["--output", "json"]);
138
139        let output = command.output()?;
140        if !output.status.success() {
141            return Err(eyre!("{:?}", output));
142        }
143
144        let query_result: R = serde_json::from_slice(&output.stdout).unwrap_or_default();
145        Ok(query_result)
146    }
147
148    async fn tx_execute<M: ToString>(
149        &self,
150        contract: &Self::Address,
151        chain_id: &Id,
152        gas: u64,
153        sender: &str,
154        msgs: impl Iterator<Item = M> + Send + Sync,
155        pay_amount: &str,
156    ) -> Result<String, Self::Error> {
157        let gas_amount = match gas {
158            0 => "auto",
159            _ => &gas.to_string(),
160        };
161
162        // only support one message for now
163        let msgs = msgs.collect::<Vec<_>>();
164        let msg = msgs.first().ok_or(eyre!("No messages provided"))?;
165
166        let mut command = self.new_command()?;
167        let command = command
168            .args(["--node", self.url.as_str()])
169            .args(["--chain-id", chain_id.as_ref()])
170            .args(["tx", "wasm"])
171            .args(["execute", contract.as_ref(), &msg.to_string()])
172            .args(["--amount", pay_amount])
173            .args(["--gas", gas_amount])
174            .args(["--gas-adjustment", "1.3"])
175            .args(["--gas-prices", "0.025untrn"])
176            .args(["--from", sender])
177            .args(["--output", "json"])
178            .arg("-y");
179
180        let output = command.output()?;
181
182        if !output.status.success() {
183            return Err(eyre!("{:?}", output));
184        }
185
186        // TODO: find the rust type for the tx output and return that
187        Ok((String::from_utf8(output.stdout)?).to_string())
188    }
189
190    async fn tx_simulate<M: ToString + Send + Sync>(
191        &self,
192        contract: &Self::Address,
193        chain_id: &Id,
194        gas: u64,
195        sender: &str,
196        msgs: impl Iterator<Item = M> + Send + Sync,
197        pay_amount: &str,
198    ) -> std::result::Result<GasInfo, Self::Error> {
199        let gas_amount = match gas {
200            0 => "auto",
201            _ => &gas.to_string(),
202        };
203
204        // only support one message for now
205        let msgs = msgs.collect::<Vec<_>>();
206        let msg = msgs.first().ok_or(eyre!("No messages provided"))?;
207
208        let mut command = self.new_command()?;
209        let command = command
210            .args(["--node", self.url.as_str()])
211            .args(["--chain-id", chain_id.as_ref()])
212            .args(["tx", "wasm"])
213            .args(["execute", contract.as_ref(), &msg.to_string()])
214            .args(["--amount", pay_amount])
215            .args(["--gas", gas_amount])
216            .args(["--gas-adjustment", "1.3"])
217            .args(["--gas-prices", "0.025untrn"])
218            .args(["--from", sender])
219            .args(["--output", "json"])
220            .arg("--dry-run")
221            .arg("-y");
222
223        let output = command.output()?;
224
225        if !output.status.success() {
226            return Err(eyre!("{:?}", output));
227        }
228
229        let gas_info: GasInfo = serde_json::from_slice(&output.stdout)?;
230        Ok(gas_info)
231    }
232
233    fn deploy<M: ToString>(
234        &self,
235        chain_id: &Id,
236        sender: &str,
237        wasm_path: M,
238    ) -> Result<String, Self::Error> {
239        let mut command = self.new_command()?;
240        let command = command
241            .args(["--node", self.url.as_str()])
242            .args(["tx", "wasm", "store", &wasm_path.to_string()])
243            .args(["--from", sender])
244            .args(["--chain-id", chain_id.as_ref()])
245            .args(["--gas-prices", &self.gas_price])
246            .args(["--gas", "auto"])
247            .args(["--gas-adjustment", "1.3"])
248            .args(["-o", "json"])
249            .arg("-y");
250
251        let output = command.output()?;
252
253        if !output.status.success() {
254            return Err(eyre!("{:?}", output));
255        }
256
257        // TODO: find the rust type for the tx output and return that
258        Ok((String::from_utf8(output.stdout)?).to_string())
259    }
260
261    fn init<M: ToString>(
262        &self,
263        chain_id: &Id,
264        sender: &str,
265        admin: Option<&str>,
266        code_id: u64,
267        init_msg: M,
268        label: &str,
269    ) -> Result<String, Self::Error> {
270        let mut command = self.new_command()?;
271        let mut command = command
272            .args(["--node", self.url.as_str()])
273            .args(["tx", "wasm", "instantiate"])
274            .args([&code_id.to_string(), &init_msg.to_string()])
275            .args(["--label", label])
276            .args(["--from", sender])
277            .args(["--chain-id", chain_id.as_ref()])
278            .args(["--gas-prices", &self.gas_price])
279            .args(["--gas", "auto"])
280            .args(["--gas-adjustment", "1.3"])
281            .args(["-o", "json"])
282            .arg("-y");
283
284        if let Some(admin) = admin {
285            command = command.args(["--admin", admin]);
286        } else {
287            command = command.arg("--no-admin");
288        }
289
290        let output = command.output()?;
291
292        if !output.status.success() {
293            return Err(eyre!("{:?}", output));
294        }
295
296        // TODO: find the rust type for the tx output and return that
297        Ok((String::from_utf8(output.stdout)?).to_string())
298    }
299
300    fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> {
301        let mut command = self.new_command()?;
302        let command = command.args(["--node", self.url.as_str()]).arg("status");
303
304        let output = command.output()?;
305
306        if !output.status.success() {
307            return Err(eyre!("{:?}", output));
308        }
309
310        let query_result: serde_json::Value =
311            serde_json::from_slice(&output.stdout).unwrap_or_default();
312
313        let sync_info = match self.kind {
314            CliClientType::Wasmd => "SyncInfo",
315            CliClientType::Neutrond => "sync_info",
316        };
317        let trusted_height = query_result[sync_info]["latest_block_height"]
318            .as_str()
319            .ok_or(eyre!("Could not query height"))?;
320
321        let trusted_height = trusted_height.parse::<u64>()?;
322
323        let trusted_hash = query_result[sync_info]["latest_block_hash"]
324            .as_str()
325            .ok_or(eyre!("Could not query height"))?
326            .to_string();
327
328        Ok((trusted_height, trusted_hash))
329    }
330}