cycles-quartz/crates/utils/cw-client/src/cli.rs
2025-02-25 13:11:47 +01:00

288 lines
8.5 KiB
Rust

use std::process::Command;
use serde_json::Value;
use color_eyre::{eyre::eyre, Help, Report, Result};
use cosmrs::{tendermint::chain::Id, AccountId};
use reqwest::Url;
use serde::de::DeserializeOwned;
use std::str::FromStr;
use crate::CwClient;
#[derive(Clone, Debug)]
pub enum CliClientType {
Wasmd,
Neutrond,
}
impl CliClientType {
fn bin(&self) -> String {
match self {
CliClientType::Wasmd => "wasmd",
CliClientType::Neutrond => "neutrond",
}
.to_string()
}
}
#[derive(Clone, Debug)]
pub struct CliClient {
kind: CliClientType,
url: Url,
gas_price: String,
}
impl CliClient {
pub fn new(kind: CliClientType, url: Url, gas_price: String) -> Self {
Self {
kind,
url,
gas_price,
}
}
pub fn wasmd(url: Url) -> Self {
Self {
kind: CliClientType::Wasmd,
url,
gas_price: "0.0025ucosm".to_string(),
}
}
fn new_command(&self) -> Result<Command> {
let bin = self.kind.bin();
if !self.is_bin_available(&bin) {
return Err(eyre!("Binary '{}' not found in PATH", bin)).suggestion(format!(
"Have you installed {}? If so, check that it's in your PATH.",
bin
));
}
Ok(Command::new(self.kind.bin()))
}
pub fn neutrond( url: Url) -> Self {
let mut command = Command::new("neutrond");
let command = command
.arg("q")
.arg("feemarket")
.arg("gas-price")
.arg("untrn")
.arg ("-o")
.arg("json");
let output = command.output().expect("command output failed").stdout;
let output_str: String = std::str::from_utf8(&output).expect("unable to parse command output").to_string();
let json: Value = serde_json::from_str(&output_str).expect("could not convert to JSON");
let price = u64::from_str(&json["price"]["amount"].to_string()).expect("couldn't parse gas price");
let gas_price = format!("{}untrn", price);
Self {
kind: CliClientType::Neutrond,
url,
gas_price,
}
}
fn is_bin_available(&self, bin: &str) -> bool {
Command::new("which")
.arg(bin)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
}
#[async_trait::async_trait]
impl CwClient for CliClient {
type Address = AccountId;
type Query = serde_json::Value;
type RawQuery = String;
type ChainId = Id;
type Error = Report;
async fn query_smart<R: DeserializeOwned + Send>(
&self,
contract: &Self::Address,
query: Self::Query,
) -> Result<R, Self::Error> {
let mut command = self.new_command()?;
let command = command
.args(["--node", self.url.as_str()])
.args(["query", "wasm"])
.args(["contract-state", "smart", contract.as_ref()])
.arg(query.to_string())
.args(["--output", "json"]);
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
let query_result: R = serde_json::from_slice(&output.stdout)
.map_err(|e| eyre!("Error deserializing: {}", e))?;
Ok(query_result)
}
fn query_raw<R: DeserializeOwned + Default>(
&self,
contract: &Self::Address,
query: Self::RawQuery,
) -> Result<R, Self::Error> {
let mut command = self.new_command()?;
let command = command
.args(["--node", self.url.as_str()])
.args(["query", "wasm"])
.args(["contract-state", "raw", contract.as_ref()])
.arg(&query)
.args(["--output", "json"]);
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
let query_result: R = serde_json::from_slice(&output.stdout).unwrap_or_default();
Ok(query_result)
}
fn query_tx<R: DeserializeOwned + Default>(&self, txhash: &str) -> Result<R, Self::Error> {
let mut command = self.new_command()?;
let command = command
.args(["--node", self.url.as_str()])
.args(["query", "tx"])
.arg(txhash)
.args(["--output", "json"]);
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
let query_result: R = serde_json::from_slice(&output.stdout).unwrap_or_default();
Ok(query_result)
}
async fn tx_execute<M: ToString + Send>(
&self,
contract: &Self::Address,
chain_id: &Id,
_gas: u64,
sender: &str,
msg: M,
) -> Result<String, Self::Error> {
let mut command = self.new_command()?;
let command = command
.args(["--node", self.url.as_str()])
.args(["--chain-id", chain_id.as_ref()])
.args(["tx", "wasm"])
.args(["execute", contract.as_ref(), &msg.to_string()])
.args(["--gas", "auto"])
.args(["--gas-prices", &self.gas_price])
.args(["--from", sender])
.args(["--gas-adjustment", "1.3"])
.args(["--output", "json"])
.arg("-y");
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
// TODO: find the rust type for the tx output and return that
Ok((String::from_utf8(output.stdout)?).to_string())
}
fn deploy<M: ToString>(
&self,
chain_id: &Id,
sender: &str,
wasm_path: M,
) -> Result<String, Self::Error> {
let mut command = self.new_command()?;
let command = command
.args(["--node", self.url.as_str()])
.args(["tx", "wasm", "store", &wasm_path.to_string()])
.args(["--from", sender])
.args(["--chain-id", chain_id.as_ref()])
.args(["--gas-prices", &self.gas_price])
.args(["--gas", "auto"])
.args(["--gas-adjustment", "1.3"])
.args(["-o", "json"])
.arg("-y");
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
// TODO: find the rust type for the tx output and return that
Ok((String::from_utf8(output.stdout)?).to_string())
}
fn init<M: ToString>(
&self,
chain_id: &Id,
sender: &str,
code_id: u64,
init_msg: M,
label: &str,
) -> Result<String, Self::Error> {
let mut command = self.new_command()?;
let command = command
.args(["--node", self.url.as_str()])
.args(["tx", "wasm", "instantiate"])
.args([&code_id.to_string(), &init_msg.to_string()])
.args(["--label", label])
.args(["--from", sender])
.arg("--no-admin")
.args(["--chain-id", chain_id.as_ref()])
.args(["--gas-prices", "0.0053untrn"])
.args(["--gas", "auto"])
.args(["--gas-adjustment", "1.3"])
.args(["-o", "json"])
.arg("-y");
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
// TODO: find the rust type for the tx output and return that
Ok((String::from_utf8(output.stdout)?).to_string())
}
fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> {
let mut command = self.new_command()?;
let command = command.args(["--node", self.url.as_str()]).arg("status");
let output = command.output()?;
if !output.status.success() {
return Err(eyre!("{:?}", output));
}
let query_result: serde_json::Value =
serde_json::from_slice(&output.stdout).unwrap_or_default();
let sync_info = match self.kind {
CliClientType::Wasmd => "SyncInfo",
CliClientType::Neutrond => "sync_info",
};
let trusted_height = query_result[sync_info]["latest_block_height"]
.as_str()
.ok_or(eyre!("Could not query height"))?;
let trusted_height = trusted_height.parse::<u64>()?;
let trusted_hash = query_result[sync_info]["latest_block_hash"]
.as_str()
.ok_or(eyre!("Could not query height"))?
.to_string();
Ok((trusted_height, trusted_hash))
}
}