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