use crate::error::{Error, Result};
use crate::tx::{Coin, Fee};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use serde_json::Value;
pub const DEFAULT_GAS_MULTIPLIER: f64 = 1.4;
pub const DEFAULT_GAS_PRICE: &str = "0.025uqor";
pub const GAS_AUTO: &str = "auto";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GasPrice {
pub scaled: u128,
pub scale: u32,
pub denom: String,
}
impl GasPrice {
pub fn parse(s: &str) -> Result<Self> {
let s = s.trim();
let split = s
.find(|c: char| !(c.is_ascii_digit() || c == '.'))
.unwrap_or(s.len());
if split == 0 || split == s.len() {
return Err(Error::Denom(format!("invalid gas price: {s:?}")));
}
let (num, denom) = s.split_at(split);
let (int_part, frac_part) = match num.split_once('.') {
Some((i, f)) => (i, f),
None => (num, ""),
};
if int_part.is_empty() && frac_part.is_empty() {
return Err(Error::Denom(format!("invalid gas price amount: {num:?}")));
}
if !int_part.bytes().all(|b| b.is_ascii_digit())
|| !frac_part.bytes().all(|b| b.is_ascii_digit())
{
return Err(Error::Denom(format!("invalid gas price amount: {num:?}")));
}
let combined = format!("{int_part}{frac_part}");
let scaled = combined
.parse::<u128>()
.map_err(|_| Error::Denom(format!("invalid gas price amount: {num:?}")))?;
Ok(GasPrice {
scaled,
scale: frac_part.len() as u32,
denom: denom.to_string(),
})
}
}
pub fn calculate_fee(gas_limit: u64, multiplier: f64, price: &GasPrice) -> Fee {
let multiplier = if multiplier <= 0.0 {
DEFAULT_GAS_MULTIPLIER
} else {
multiplier
};
let fee_gas = (gas_limit as f64 * multiplier).ceil() as u64;
let numerator = (fee_gas as u128) * price.scaled;
let divisor = 10u128.pow(price.scale);
let amount = numerator.div_ceil(divisor);
Fee {
amount: vec![Coin {
denom: price.denom.clone(),
amount: amount.to_string(),
}],
gas: fee_gas.to_string(),
granter: String::new(),
payer: String::new(),
}
}
pub async fn estimate_gas(rest_url: &str, tx_bytes: &[u8]) -> Result<u64> {
let url = format!(
"{}/cosmos/tx/v1beta1/simulate",
rest_url.trim_end_matches('/')
);
let payload = serde_json::json!({ "tx_bytes": BASE64.encode(tx_bytes) });
let resp = reqwest::Client::new()
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.json(&payload)
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
if !status.is_success() {
return Err(Error::Http {
status: status.as_u16(),
url,
body,
});
}
let v: Value =
serde_json::from_str(&body).map_err(|e| Error::InvalidResponse(e.to_string()))?;
let gas_used = &v["gas_info"]["gas_used"];
let gas_used = match gas_used {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => {
return Err(Error::InvalidResponse(format!(
"simulate response missing gas_info.gas_used: {body}"
)))
}
};
gas_used
.parse::<u64>()
.map_err(|_| Error::InvalidResponse(format!("invalid gas_used: {gas_used:?}")))
}
pub async fn estimate_fee(
rest_url: &str,
tx_bytes: &[u8],
multiplier: f64,
price_str: &str,
) -> Result<Fee> {
let price_str = if price_str.is_empty() {
DEFAULT_GAS_PRICE
} else {
price_str
};
let price = GasPrice::parse(price_str)?;
let gas_used = estimate_gas(rest_url, tx_bytes).await?;
Ok(calculate_fee(gas_used, multiplier, &price))
}