Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/gno-client/src/gas.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
use gno_rpc::rpc_types::TxFee;
use num_rational::BigRational;
use unionlabs::cosmos::tx::fee::Fee;

pub mod any;
pub mod dynamic;
pub mod fixed;

pub trait GasFillerT {
async fn max_gas(&self) -> u64;

async fn mk_fee(&self, gas: u64) -> Fee;
async fn mk_fee(&self, gas: u64) -> Result<TxFee, crate::BroadcastTxCommitError>;
}

impl<T: GasFillerT> GasFillerT for &T {
async fn max_gas(&self) -> u64 {
(*self).max_gas().await
}

async fn mk_fee(&self, gas: u64) -> Fee {
async fn mk_fee(&self, gas: u64) -> Result<TxFee, crate::BroadcastTxCommitError> {
(*self).mk_fee(gas).await
}
}
Expand Down
48 changes: 48 additions & 0 deletions lib/gno-client/src/gas/any.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use gno_rpc::rpc_types::TxFee;
use serde::{Deserialize, Serialize};

use crate::gas::{GasFillerT, dynamic, fixed};

#[derive(Debug, Clone)]
pub enum GasFiller {
Fixed(TxFee),
// reuses fixed's pricing formula, but fed the simulated gas_used
Simulate(fixed::GasFiller),
Dynamic(dynamic::GasFiller),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type", content = "config")]
pub enum Config {
Fixed(TxFee),
Simulate(fixed::GasFiller),
Dynamic(dynamic::Config),
}

impl Config {
pub fn into_gas_filler(self, client: gno_rpc::Client) -> GasFiller {
match self {
Config::Fixed(fee) => GasFiller::Fixed(fee),
Config::Simulate(filler) => GasFiller::Simulate(filler),
Config::Dynamic(config) => GasFiller::Dynamic(dynamic::GasFiller::new(config, client)),
}
}
}

impl GasFillerT for GasFiller {
async fn max_gas(&self) -> u64 {
match self {
GasFiller::Fixed(fee) => fee.gas_wanted.try_into().unwrap_or(u64::MAX),
GasFiller::Simulate(f) => f.max_gas().await,
GasFiller::Dynamic(f) => f.max_gas().await,
}
}

async fn mk_fee(&self, gas: u64) -> Result<TxFee, crate::BroadcastTxCommitError> {
match self {
GasFiller::Fixed(fee) => Ok(fee.clone()),
GasFiller::Simulate(f) => f.mk_fee(gas).await,
GasFiller::Dynamic(f) => f.mk_fee(gas).await,
}
}
}
279 changes: 279 additions & 0 deletions lib/gno-client/src/gas/dynamic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
use gno_rpc::rpc_types::TxFee;
use num_rational::BigRational;
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument};

use crate::gas::{GasFillerT, u128_saturating_mul_f64};

#[derive(Debug, Clone)]
pub struct GasFiller {
max_gas: u64,
gas_multiplier: f64,
denom_override: Option<String>,
client: gno_rpc::Client,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub max_gas: u64,
#[serde(with = "::serde_utils::string_opt")]
pub gas_multiplier: Option<f64>,
pub denom: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GasPriceResponse {
#[serde(with = "::serde_utils::string")]
gas: i64,
price: String,
}

/// gno's `GasPrice{ Gas, Price }`; price per unit of gas is `amount / gas`, not `amount` alone.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct GasPrice {
pub gas: i64,
pub amount: u128,
pub denom: String,
}

impl GasFiller {
pub fn new(config: Config, client: gno_rpc::Client) -> Self {
Self {
max_gas: config.max_gas,
gas_multiplier: config.gas_multiplier.unwrap_or(1.0),
denom_override: config.denom,
client,
}
}

pub(crate) async fn get_gas_price(&self) -> Result<GasPrice, crate::BroadcastTxCommitError> {
let response = self
.client
.abci_query("auth/gasprice", &[], None, false)
.await?;

if let Some(error) = response.response.response_base.error {
return Err(crate::BroadcastTxCommitError::TxFailed {
error,
log: response.response.response_base.log,
});
}

let value = response.response.response_base.data.unwrap_or_default();

parse_gas_price_response(&value)
}
}

fn parse_gas_price_response(value: &[u8]) -> Result<GasPrice, crate::BroadcastTxCommitError> {
let gas_price = serde_json::from_slice::<GasPriceResponse>(value)?;

if gas_price.price.is_empty() {
return Ok(GasPrice {
gas: gas_price.gas,
amount: 0,
denom: String::new(),
});
}

let split_at = gas_price
.price
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(gas_price.price.len());

let (amount, denom) = gas_price.price.split_at(split_at);

let amount: u128 = amount
.parse()
.map_err(|_| crate::BroadcastTxCommitError::InvalidGasPrice {
price: gas_price.price.clone(),
})?;

if amount > 0 && gas_price.gas <= 0 {
return Err(crate::BroadcastTxCommitError::InvalidGasPrice {
price: gas_price.price,
});
}

Ok(GasPrice {
gas: gas_price.gas,
amount,
denom: denom.to_owned(),
})
}

impl GasFillerT for GasFiller {
async fn max_gas(&self) -> u64 {
self.max_gas
}

#[instrument(
skip_all,
fields(
self.max_gas = %self.max_gas,
self.gas_multiplier = %self.gas_multiplier,
gas = %gas,
)
)]
async fn mk_fee(&self, gas: u64) -> Result<TxFee, crate::BroadcastTxCommitError> {
// gas limit = provided gas * multiplier, clamped to max_gas
let gas_limit = u128_saturating_mul_f64(gas.into(), self.gas_multiplier)
.try_into()
.unwrap_or(self.max_gas)
.min(self.max_gas);

let price = self.get_gas_price().await?;

let fee = compute_fee(gas_limit, &price, self.denom_override.as_deref())?;

debug!(gas_limit, gas_fee = %fee.gas_fee, "computed fee from dynamic gas price");

Ok(fee)
}
}

fn compute_fee(
gas_limit: u64,
price: &GasPrice,
denom_override: Option<&str>,
) -> Result<TxFee, crate::BroadcastTxCommitError> {
let denom = if price.denom.is_empty() {
denom_override.ok_or(crate::BroadcastTxCommitError::MissingGasDenom)?
} else {
price.denom.as_str()
};

let price_per_gas = BigRational::new(price.amount.into(), price.gas.max(1).into());

let amount = price_per_gas * BigRational::from_integer(gas_limit.into());
let amount = amount.ceil().to_integer().try_into().unwrap_or(u128::MAX);

Ok(TxFee {
gas_wanted: gas_limit.try_into().unwrap_or(i64::MAX),
gas_fee: format!("{amount}{denom}"),
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_normal_price() {
let price = parse_gas_price_response(br#"{"gas": "1000", "price": "5ugnot"}"#).unwrap();

assert_eq!(
price,
GasPrice {
gas: 1000,
amount: 5,
denom: "ugnot".to_owned()
}
);
}

#[test]
fn parses_zero_price_as_no_price_set() {
let price = parse_gas_price_response(br#"{"gas": "0", "price": ""}"#).unwrap();

assert_eq!(
price,
GasPrice {
gas: 0,
amount: 0,
denom: String::new()
}
);
}

#[test]
fn rejects_malformed_price() {
let err = parse_gas_price_response(br#"{"gas": "1000", "price": "abcugnot"}"#).unwrap_err();

assert!(matches!(
err,
crate::BroadcastTxCommitError::InvalidGasPrice { price } if price == "abcugnot"
));
}

#[test]
fn rejects_nonzero_price_with_non_positive_gas() {
let err = parse_gas_price_response(br#"{"gas": "0", "price": "5ugnot"}"#).unwrap_err();

assert!(matches!(
err,
crate::BroadcastTxCommitError::InvalidGasPrice { price } if price == "5ugnot"
));

let err = parse_gas_price_response(br#"{"gas": "-1", "price": "5ugnot"}"#).unwrap_err();

assert!(matches!(
err,
crate::BroadcastTxCommitError::InvalidGasPrice { price } if price == "5ugnot"
));
}

fn gas_price(gas: i64, amount: u128, denom: &str) -> GasPrice {
GasPrice {
gas,
amount,
denom: denom.to_owned(),
}
}

#[test]
fn compute_fee_prices_gas_limit_at_reported_price() {
let fee = compute_fee(1000, &gas_price(1000, 5, "ugnot"), None).unwrap();

assert_eq!(fee.gas_wanted, 1000);
assert_eq!(fee.gas_fee, "5ugnot");
}

#[test]
fn compute_fee_never_overrides_a_denom_the_chain_reported() {
let fee = compute_fee(1000, &gas_price(1000, 5, "ugnot"), Some("uatom")).unwrap();

assert_eq!(fee.gas_fee, "5ugnot");
}

#[test]
fn compute_fee_falls_back_to_override_when_chain_reports_no_denom() {
let fee = compute_fee(1000, &gas_price(0, 0, ""), Some("ugnot")).unwrap();

assert_eq!(fee.gas_fee, "0ugnot");
}

#[test]
fn compute_fee_errors_instead_of_producing_a_denomless_fee() {
let err = compute_fee(1000, &gas_price(0, 0, ""), None).unwrap_err();

assert!(matches!(
err,
crate::BroadcastTxCommitError::MissingGasDenom
));
}

#[tokio::test]
#[ignore = "hits a live gno node"]
async fn get_gas_price_against_live_node() {
let client = gno_rpc::Client::new("https://sapphire.rpc.onbloc.xyz:443")
.await
.unwrap();

let filler = GasFiller::new(
Config {
max_gas: 1_000_000_000,
gas_multiplier: None,
denom: None,
},
client,
);

let price = filler.get_gas_price().await.unwrap();

println!("{price:?}");

assert!(price.gas > 0);
assert!(!price.denom.is_empty());
}
}
Loading