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