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 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 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 admin: Option<&str>,
219 code_id: u64,
220 init_msg: M,
221 label: &str,
222 ) -> Result<String, Self::Error> {
223 let mut command = self.new_command()?;
224 let mut command = command
225 .args(["--node", self.url.as_str()])
226 .args(["tx", "wasm", "instantiate"])
227 .args([&code_id.to_string(), &init_msg.to_string()])
228 .args(["--label", label])
229 .args(["--from", sender])
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 if let Some(admin) = admin {
238 command = command.args(["--admin", admin]);
239 } else {
240 command = command.arg("--no-admin");
241 }
242
243 let output = command.output()?;
244
245 if !output.status.success() {
246 return Err(eyre!("{:?}", output));
247 }
248
249 Ok((String::from_utf8(output.stdout)?).to_string())
251 }
252
253 fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> {
254 let mut command = self.new_command()?;
255 let command = command.args(["--node", self.url.as_str()]).arg("status");
256
257 let output = command.output()?;
258
259 if !output.status.success() {
260 return Err(eyre!("{:?}", output));
261 }
262
263 let query_result: serde_json::Value =
264 serde_json::from_slice(&output.stdout).unwrap_or_default();
265
266 let sync_info = match self.kind {
267 CliClientType::Wasmd => "SyncInfo",
268 CliClientType::Neutrond => "sync_info",
269 };
270 let trusted_height = query_result[sync_info]["latest_block_height"]
271 .as_str()
272 .ok_or(eyre!("Could not query height"))?;
273
274 let trusted_height = trusted_height.parse::<u64>()?;
275
276 let trusted_hash = query_result[sync_info]["latest_block_hash"]
277 .as_str()
278 .ok_or(eyre!("Could not query height"))?
279 .to_string();
280
281 Ok((trusted_height, trusted_hash))
282 }
283}