cw_client/
cli.rs

1use std::process::Command;
2
3use color_eyre::{eyre::eyre, Help, Report, Result};
4use cosmrs::{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 + Send>(
149        &self,
150        contract: &Self::Address,
151        chain_id: &Id,
152        gas: u64,
153        sender: &str,
154        msg: M,
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        let mut command = self.new_command()?;
163        let command = command
164            .args(["--node", self.url.as_str()])
165            .args(["--chain-id", chain_id.as_ref()])
166            .args(["tx", "wasm"])
167            .args(["execute", contract.as_ref(), &msg.to_string()])
168            .args(["--amount", pay_amount])
169            .args(["--gas", gas_amount])
170            .args(["--gas-adjustment", "1.3"])
171            .args(["--gas-prices", "0.025untrn"])
172            .args(["--from", sender])
173            .args(["--output", "json"])
174            .arg("-y");
175
176        let output = command.output()?;
177
178        if !output.status.success() {
179            return Err(eyre!("{:?}", output));
180        }
181
182        // TODO: find the rust type for the tx output and return that
183        Ok((String::from_utf8(output.stdout)?).to_string())
184    }
185
186    fn deploy<M: ToString>(
187        &self,
188        chain_id: &Id,
189        sender: &str,
190        wasm_path: M,
191    ) -> Result<String, Self::Error> {
192        let mut command = self.new_command()?;
193        let command = command
194            .args(["--node", self.url.as_str()])
195            .args(["tx", "wasm", "store", &wasm_path.to_string()])
196            .args(["--from", sender])
197            .args(["--chain-id", chain_id.as_ref()])
198            .args(["--gas-prices", &self.gas_price])
199            .args(["--gas", "auto"])
200            .args(["--gas-adjustment", "1.3"])
201            .args(["-o", "json"])
202            .arg("-y");
203
204        let output = command.output()?;
205
206        if !output.status.success() {
207            return Err(eyre!("{:?}", output));
208        }
209
210        // TODO: find the rust type for the tx output and return that
211        Ok((String::from_utf8(output.stdout)?).to_string())
212    }
213
214    fn init<M: ToString>(
215        &self,
216        chain_id: &Id,
217        sender: &str,
218        code_id: u64,
219        init_msg: M,
220        label: &str,
221    ) -> Result<String, Self::Error> {
222        let mut command = self.new_command()?;
223        let command = command
224            .args(["--node", self.url.as_str()])
225            .args(["tx", "wasm", "instantiate"])
226            .args([&code_id.to_string(), &init_msg.to_string()])
227            .args(["--label", label])
228            .args(["--from", sender])
229            .arg("--no-admin")
230            .args(["--chain-id", chain_id.as_ref()])
231            .args(["--gas-prices", &self.gas_price])
232            .args(["--gas", "auto"])
233            .args(["--gas-adjustment", "1.3"])
234            .args(["-o", "json"])
235            .arg("-y");
236
237        let output = command.output()?;
238
239        if !output.status.success() {
240            return Err(eyre!("{:?}", output));
241        }
242
243        // TODO: find the rust type for the tx output and return that
244        Ok((String::from_utf8(output.stdout)?).to_string())
245    }
246
247    fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> {
248        let mut command = self.new_command()?;
249        let command = command.args(["--node", self.url.as_str()]).arg("status");
250
251        let output = command.output()?;
252
253        if !output.status.success() {
254            return Err(eyre!("{:?}", output));
255        }
256
257        let query_result: serde_json::Value =
258            serde_json::from_slice(&output.stdout).unwrap_or_default();
259
260        let sync_info = match self.kind {
261            CliClientType::Wasmd => "SyncInfo",
262            CliClientType::Neutrond => "sync_info",
263        };
264        let trusted_height = query_result[sync_info]["latest_block_height"]
265            .as_str()
266            .ok_or(eyre!("Could not query height"))?;
267
268        let trusted_height = trusted_height.parse::<u64>()?;
269
270        let trusted_hash = query_result[sync_info]["latest_block_hash"]
271            .as_str()
272            .ok_or(eyre!("Could not query height"))?
273            .to_string();
274
275        Ok((trusted_height, trusted_hash))
276    }
277}