|
| 1 | +use gno_rpc::rpc_types::TxFee; |
| 2 | +use num_rational::BigRational; |
| 3 | +use serde::{Deserialize, Serialize}; |
| 4 | +use tracing::{debug, instrument}; |
| 5 | + |
| 6 | +use crate::gas::{GasFillerT, u128_saturating_mul_f64}; |
| 7 | + |
| 8 | +#[derive(Debug, Clone)] |
| 9 | +pub struct GasFiller { |
| 10 | + max_gas: u64, |
| 11 | + gas_multiplier: f64, |
| 12 | + denom_override: Option<String>, |
| 13 | + client: gno_rpc::Client, |
| 14 | +} |
| 15 | + |
| 16 | +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| 17 | +pub struct Config { |
| 18 | + pub max_gas: u64, |
| 19 | + #[serde(with = "::serde_utils::string_opt")] |
| 20 | + pub gas_multiplier: Option<f64>, |
| 21 | + pub denom: Option<String>, |
| 22 | +} |
| 23 | + |
| 24 | +#[derive(Debug, Deserialize)] |
| 25 | +struct GasPriceResponse { |
| 26 | + #[serde(with = "::serde_utils::string")] |
| 27 | + gas: i64, |
| 28 | + price: String, |
| 29 | +} |
| 30 | + |
| 31 | +/// gno's `GasPrice{ Gas, Price }`; price per unit of gas is `amount / gas`, not `amount` alone. |
| 32 | +#[derive(Debug, Clone, PartialEq)] |
| 33 | +pub(crate) struct GasPrice { |
| 34 | + pub gas: i64, |
| 35 | + pub amount: u128, |
| 36 | + pub denom: String, |
| 37 | +} |
| 38 | + |
| 39 | +impl GasFiller { |
| 40 | + pub fn new(config: Config, client: gno_rpc::Client) -> Self { |
| 41 | + Self { |
| 42 | + max_gas: config.max_gas, |
| 43 | + gas_multiplier: config.gas_multiplier.unwrap_or(1.0), |
| 44 | + denom_override: config.denom, |
| 45 | + client, |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + pub(crate) async fn get_gas_price(&self) -> Result<GasPrice, crate::BroadcastTxCommitError> { |
| 50 | + let response = self |
| 51 | + .client |
| 52 | + .abci_query("auth/gasprice", &[], None, false) |
| 53 | + .await?; |
| 54 | + |
| 55 | + if let Some(error) = response.response.response_base.error { |
| 56 | + return Err(crate::BroadcastTxCommitError::TxFailed { |
| 57 | + error, |
| 58 | + log: response.response.response_base.log, |
| 59 | + }); |
| 60 | + } |
| 61 | + |
| 62 | + let value = response.response.response_base.data.unwrap_or_default(); |
| 63 | + |
| 64 | + parse_gas_price_response(&value) |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +fn parse_gas_price_response(value: &[u8]) -> Result<GasPrice, crate::BroadcastTxCommitError> { |
| 69 | + let gas_price = serde_json::from_slice::<GasPriceResponse>(value)?; |
| 70 | + |
| 71 | + let split_at = gas_price |
| 72 | + .price |
| 73 | + .find(|c: char| !c.is_ascii_digit()) |
| 74 | + .unwrap_or(gas_price.price.len()); |
| 75 | + |
| 76 | + let (amount, denom) = gas_price.price.split_at(split_at); |
| 77 | + |
| 78 | + let amount: u128 = if gas_price.price.is_empty() { |
| 79 | + 0 |
| 80 | + } else { |
| 81 | + amount |
| 82 | + .parse() |
| 83 | + .map_err(|_| crate::BroadcastTxCommitError::InvalidGasPrice { |
| 84 | + price: gas_price.price.clone(), |
| 85 | + })? |
| 86 | + }; |
| 87 | + |
| 88 | + if amount > 0 && gas_price.gas <= 0 { |
| 89 | + return Err(crate::BroadcastTxCommitError::InvalidGasPrice { |
| 90 | + price: gas_price.price, |
| 91 | + }); |
| 92 | + } |
| 93 | + |
| 94 | + Ok(GasPrice { |
| 95 | + gas: gas_price.gas, |
| 96 | + amount, |
| 97 | + denom: denom.to_owned(), |
| 98 | + }) |
| 99 | +} |
| 100 | + |
| 101 | +impl GasFillerT for GasFiller { |
| 102 | + async fn max_gas(&self) -> u64 { |
| 103 | + self.max_gas |
| 104 | + } |
| 105 | + |
| 106 | + #[instrument( |
| 107 | + skip_all, |
| 108 | + fields( |
| 109 | + self.max_gas = %self.max_gas, |
| 110 | + self.gas_multiplier = %self.gas_multiplier, |
| 111 | + gas = %gas, |
| 112 | + ) |
| 113 | + )] |
| 114 | + async fn mk_fee(&self, gas: u64) -> Result<TxFee, crate::BroadcastTxCommitError> { |
| 115 | + // gas limit = provided gas * multiplier, clamped to max_gas |
| 116 | + let gas_limit = u128_saturating_mul_f64(gas.into(), self.gas_multiplier) |
| 117 | + .try_into() |
| 118 | + .unwrap_or(self.max_gas) |
| 119 | + .min(self.max_gas); |
| 120 | + |
| 121 | + let price = self.get_gas_price().await?; |
| 122 | + |
| 123 | + let fee = compute_fee(gas_limit, &price, self.denom_override.as_deref())?; |
| 124 | + |
| 125 | + debug!(gas_limit, gas_fee = %fee.gas_fee, "computed fee from dynamic gas price"); |
| 126 | + |
| 127 | + Ok(fee) |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +fn compute_fee( |
| 132 | + gas_limit: u64, |
| 133 | + price: &GasPrice, |
| 134 | + denom_override: Option<&str>, |
| 135 | +) -> Result<TxFee, crate::BroadcastTxCommitError> { |
| 136 | + let denom = if price.denom.is_empty() { |
| 137 | + denom_override.ok_or(crate::BroadcastTxCommitError::MissingGasDenom)? |
| 138 | + } else { |
| 139 | + price.denom.as_str() |
| 140 | + }; |
| 141 | + |
| 142 | + let price_per_gas = BigRational::new(price.amount.into(), price.gas.max(1).into()); |
| 143 | + |
| 144 | + let amount = price_per_gas * BigRational::from_integer(gas_limit.into()); |
| 145 | + let amount = amount.ceil().to_integer().try_into().unwrap_or(u128::MAX); |
| 146 | + |
| 147 | + Ok(TxFee { |
| 148 | + gas_wanted: gas_limit.try_into().unwrap_or(i64::MAX), |
| 149 | + gas_fee: format!("{amount}{denom}"), |
| 150 | + }) |
| 151 | +} |
| 152 | + |
| 153 | +#[cfg(test)] |
| 154 | +mod tests { |
| 155 | + use super::*; |
| 156 | + |
| 157 | + #[test] |
| 158 | + fn parses_normal_price() { |
| 159 | + let price = parse_gas_price_response(br#"{"gas": "1000", "price": "5ugnot"}"#).unwrap(); |
| 160 | + |
| 161 | + assert_eq!( |
| 162 | + price, |
| 163 | + GasPrice { |
| 164 | + gas: 1000, |
| 165 | + amount: 5, |
| 166 | + denom: "ugnot".to_owned() |
| 167 | + } |
| 168 | + ); |
| 169 | + } |
| 170 | + |
| 171 | + #[test] |
| 172 | + fn parses_zero_price_as_no_price_set() { |
| 173 | + let price = parse_gas_price_response(br#"{"gas": "0", "price": ""}"#).unwrap(); |
| 174 | + |
| 175 | + assert_eq!( |
| 176 | + price, |
| 177 | + GasPrice { |
| 178 | + gas: 0, |
| 179 | + amount: 0, |
| 180 | + denom: String::new() |
| 181 | + } |
| 182 | + ); |
| 183 | + } |
| 184 | + |
| 185 | + #[test] |
| 186 | + fn rejects_malformed_price() { |
| 187 | + let err = parse_gas_price_response(br#"{"gas": "1000", "price": "abcugnot"}"#).unwrap_err(); |
| 188 | + |
| 189 | + assert!(matches!( |
| 190 | + err, |
| 191 | + crate::BroadcastTxCommitError::InvalidGasPrice { price } if price == "abcugnot" |
| 192 | + )); |
| 193 | + } |
| 194 | + |
| 195 | + #[test] |
| 196 | + fn rejects_nonzero_price_with_non_positive_gas() { |
| 197 | + let err = parse_gas_price_response(br#"{"gas": "0", "price": "5ugnot"}"#).unwrap_err(); |
| 198 | + |
| 199 | + assert!(matches!( |
| 200 | + err, |
| 201 | + crate::BroadcastTxCommitError::InvalidGasPrice { price } if price == "5ugnot" |
| 202 | + )); |
| 203 | + |
| 204 | + let err = parse_gas_price_response(br#"{"gas": "-1", "price": "5ugnot"}"#).unwrap_err(); |
| 205 | + |
| 206 | + assert!(matches!( |
| 207 | + err, |
| 208 | + crate::BroadcastTxCommitError::InvalidGasPrice { price } if price == "5ugnot" |
| 209 | + )); |
| 210 | + } |
| 211 | + |
| 212 | + fn gas_price(gas: i64, amount: u128, denom: &str) -> GasPrice { |
| 213 | + GasPrice { |
| 214 | + gas, |
| 215 | + amount, |
| 216 | + denom: denom.to_owned(), |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + #[test] |
| 221 | + fn compute_fee_prices_gas_limit_at_reported_price() { |
| 222 | + let fee = compute_fee(1000, &gas_price(1000, 5, "ugnot"), None).unwrap(); |
| 223 | + |
| 224 | + assert_eq!(fee.gas_wanted, 1000); |
| 225 | + assert_eq!(fee.gas_fee, "5ugnot"); |
| 226 | + } |
| 227 | + |
| 228 | + #[test] |
| 229 | + fn compute_fee_never_overrides_a_denom_the_chain_reported() { |
| 230 | + let fee = compute_fee(1000, &gas_price(1000, 5, "ugnot"), Some("uatom")).unwrap(); |
| 231 | + |
| 232 | + assert_eq!(fee.gas_fee, "5ugnot"); |
| 233 | + } |
| 234 | + |
| 235 | + #[test] |
| 236 | + fn compute_fee_falls_back_to_override_when_chain_reports_no_denom() { |
| 237 | + let fee = compute_fee(1000, &gas_price(0, 0, ""), Some("ugnot")).unwrap(); |
| 238 | + |
| 239 | + assert_eq!(fee.gas_fee, "0ugnot"); |
| 240 | + } |
| 241 | + |
| 242 | + #[test] |
| 243 | + fn compute_fee_errors_instead_of_producing_a_denomless_fee() { |
| 244 | + let err = compute_fee(1000, &gas_price(0, 0, ""), None).unwrap_err(); |
| 245 | + |
| 246 | + assert!(matches!( |
| 247 | + err, |
| 248 | + crate::BroadcastTxCommitError::MissingGasDenom |
| 249 | + )); |
| 250 | + } |
| 251 | + |
| 252 | + #[tokio::test] |
| 253 | + #[ignore = "hits a live gno node"] |
| 254 | + async fn get_gas_price_against_live_node() { |
| 255 | + let client = gno_rpc::Client::new("https://sapphire.rpc.onbloc.xyz:443") |
| 256 | + .await |
| 257 | + .unwrap(); |
| 258 | + |
| 259 | + let filler = GasFiller::new( |
| 260 | + Config { |
| 261 | + max_gas: 1_000_000_000, |
| 262 | + gas_multiplier: None, |
| 263 | + denom: None, |
| 264 | + }, |
| 265 | + client, |
| 266 | + ); |
| 267 | + |
| 268 | + let price = filler.get_gas_price().await.unwrap(); |
| 269 | + |
| 270 | + println!("{price:?}"); |
| 271 | + |
| 272 | + assert!(price.gas > 0); |
| 273 | + assert!(!price.denom.is_empty()); |
| 274 | + } |
| 275 | +} |
0 commit comments