Skip to content

Commit f825f57

Browse files
Enhance dynamic gas price configuration and querying support for Cosmos EVM, Osmosis, and SkipFeeMarket
1 parent 50873fa commit f825f57

File tree

4 files changed

+183
-35
lines changed

4 files changed

+183
-35
lines changed

config.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,14 +311,24 @@ gas_multiplier = 1.1
311311
# Query the current gas price from the chain instead of using the static `gas_price` from the config.
312312
# Useful for chains which have [EIP-1559][eip]-like dynamic gas price.
313313
#
314-
# At the moment, only chains which support the `osmosis.txfees.v1beta1.Query/GetEipBaseFee`
315-
# query or have enabled Skip's `x/feemarket` module https://github.com/skip-mev/feemarket
314+
# At the moment, only chains which support the `/cosmos.evm.feemarket.v1.Query/BaseFee` query or
315+
# `osmosis.txfees.v1beta1.Query/GetEipBaseFee` query or have enabled Skip's `x/feemarket` module https://github.com/skip-mev/feemarket
316316
# can be used with dynamic gas price enabled.
317317
#
318+
# The `type` field allows you to specify which query method to use:
319+
# - `skip_fee_market`: Use Skip's feemarket module query at `/feemarket.feemarket.v1.Query/GasPrices`
320+
# - `osmosis`: Use Osmosis EIP-1559 query at `/osmosis.txfees.v1beta1.Query/GetEipBaseFee`
321+
# - `cosmos_evm`: Use Cosmos EVM query at `/cosmos.evm.feemarket.v1.Query/BaseFee`
322+
#
323+
# If `type` is not specified, Hermes will attempt to auto-detect for backwards compatibility:
324+
# - Chains starting with "osmosis" or "osmo-test" will use `osmosis` type
325+
# - All other chains will default to `skip_fee_market` type
326+
#
318327
# See this page in the Hermes guide for more information:
319328
# https://hermes.informal.systems/documentation/configuration/dynamic-gas-fees.html
320329
#
321330
# Default: { enabled = false, multiplier = 1.1, max = 0.6 }
331+
# Example with type specified: { enabled = true, multiplier = 1.1, max = 0.6, type = 'cosmos_evm' }
322332
dynamic_gas_price = { enabled = false, multiplier = 1.1, max = 0.6 }
323333

324334
# Specify how many IBC messages at most to include in a single transaction.

crates/relayer/src/chain/cosmos/eip_base_fee.rs

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,29 @@ use tracing::{debug, trace};
1010
use ibc_proto::cosmos::base::v1beta1::{DecCoin, DecProto};
1111
use ibc_relayer_types::core::ics24_host::identifier::ChainId;
1212

13-
use crate::error::Error;
13+
use crate::{config::dynamic_gas::DynamicGasType, error::Error};
1414

1515
pub async fn query_eip_base_fee(
1616
rpc_address: &Url,
1717
gas_price_denom: &str,
18+
dynamic_gas_type: &Option<DynamicGasType>,
1819
chain_id: &ChainId,
1920
) -> Result<f64, Error> {
20-
debug!("Querying Omosis EIP-1559 base fee from {rpc_address}");
21-
22-
let chain_name = chain_id.name();
23-
24-
let is_osmosis = chain_name.starts_with("osmosis") || chain_name.starts_with("osmo-test");
25-
26-
let url = if is_osmosis {
27-
format!(
28-
"{}abci_query?path=\"/osmosis.txfees.v1beta1.Query/GetEipBaseFee\"",
29-
rpc_address
30-
)
31-
} else {
32-
format!(
33-
"{}abci_query?path=\"/feemarket.feemarket.v1.Query/GasPrices\"&denom={}",
34-
rpc_address, gas_price_denom
35-
)
21+
let dynamic_gas_type = match dynamic_gas_type {
22+
Some(dynamic_gas_type) => dynamic_gas_type,
23+
None => {
24+
// backward compatibility for chains that do not specify dynamic gas type
25+
if chain_id.name().starts_with("osmosis") || chain_id.name().starts_with("osmo-test") {
26+
&DynamicGasType::Osmosis
27+
} else {
28+
&DynamicGasType::SkipFeeMarket
29+
}
30+
}
3631
};
3732

33+
debug!("Querying {dynamic_gas_type} base fee from {rpc_address}");
34+
35+
let url = dynamic_gas_type.get_url(rpc_address, gas_price_denom);
3836
let response = reqwest::get(&url).await.map_err(Error::http_request)?;
3937

4038
if !response.status().is_success() {
@@ -58,19 +56,16 @@ pub async fn query_eip_base_fee(
5856

5957
let result: EipBaseFeeHTTPResult = response.json().await.map_err(Error::http_response_body)?;
6058

61-
let amount = if is_osmosis {
62-
extract_dynamic_gas_price_osmosis(result.result.response.value)?
63-
} else {
64-
extract_dynamic_gas_price(result.result.response.value)?
65-
};
59+
let amount = dynamic_gas_type
60+
.extract_dynamic_gas_price(result.result.response.value)?;
6661

6762
trace!("EIP-1559 base fee: {amount}");
6863

6964
Ok(amount)
7065
}
7166

7267
/// This method extracts the gas base fee from Skip's feemarket
73-
fn extract_dynamic_gas_price(encoded: String) -> Result<f64, Error> {
68+
fn extract_dynamic_gas_price_fee_market(encoded: String) -> Result<f64, Error> {
7469
let decoded = base64::decode(encoded).map_err(Error::base64_decode)?;
7570

7671
let gas_price_response: GasPriceResponse =
@@ -84,8 +79,8 @@ fn extract_dynamic_gas_price(encoded: String) -> Result<f64, Error> {
8479
f64::from_str(dec.to_string().as_str()).map_err(Error::parse_float)
8580
}
8681

87-
/// This method extracts the gas base fee from Osmosis EIP-1559
88-
fn extract_dynamic_gas_price_osmosis(encoded: String) -> Result<f64, Error> {
82+
/// This method extracts the gas base fee from Osmosis EIP-1559 and Cosmos EVM EIP-1559
83+
fn extract_dynamic_gas_price(encoded: String) -> Result<f64, Error> {
8984
let decoded = base64::decode(encoded).map_err(Error::base64_decode)?;
9085

9186
let dec_proto: DecProto = prost::Message::decode(decoded.as_ref())
@@ -189,3 +184,74 @@ impl fmt::Display for Decimal {
189184
}
190185
}
191186
}
187+
188+
impl DynamicGasType {
189+
fn get_url(&self, rpc_address: &Url, gas_price_denom: &str) -> String {
190+
match self {
191+
DynamicGasType::SkipFeeMarket => format!(
192+
"{}abci_query?path=\"/feemarket.feemarket.v1.Query/GasPrices\"&denom={}",
193+
rpc_address, gas_price_denom
194+
),
195+
DynamicGasType::Osmosis => format!(
196+
"{}abci_query?path=\"/osmosis.txfees.v1beta1.Query/GetEipBaseFee\"",
197+
rpc_address
198+
),
199+
DynamicGasType::CosmosEvm => format!(
200+
"{}abci_query?path=\"/cosmos.evm.feemarket.v1.Query/BaseFee\"",
201+
rpc_address
202+
),
203+
}
204+
}
205+
206+
fn extract_dynamic_gas_price(&self, encoded: String) -> Result<f64, Error> {
207+
match self {
208+
DynamicGasType::SkipFeeMarket => extract_dynamic_gas_price_fee_market(encoded),
209+
DynamicGasType::Osmosis => extract_dynamic_gas_price(encoded),
210+
DynamicGasType::CosmosEvm => extract_dynamic_gas_price(encoded),
211+
}
212+
}
213+
}
214+
215+
#[cfg(test)]
216+
mod tests {
217+
use super::*;
218+
219+
#[test]
220+
fn test_extract_dynamic_gas_price_fee_market() {
221+
// Test with the provided encoded value
222+
let encoded = "ChgKA3VvbRIRMTAwMDAwMDAwMDAwMDAwMDA".to_string();
223+
224+
let result = extract_dynamic_gas_price_fee_market(encoded);
225+
226+
assert!(result.is_ok());
227+
let gas_price = result.unwrap();
228+
229+
assert_eq!(gas_price, 0.01);
230+
}
231+
232+
#[test]
233+
fn test_extract_dynamic_gas_price_osmosis() {
234+
// Test with the provided encoded value
235+
let encoded = "ChAyNTAwMDAwMDAwMDAwMDAw".to_string();
236+
237+
let result = extract_dynamic_gas_price(encoded);
238+
239+
assert!(result.is_ok());
240+
let gas_price = result.unwrap();
241+
242+
assert_eq!(gas_price, 0.0025);
243+
}
244+
245+
#[test]
246+
fn test_extract_dynamic_gas_price_cosmos_evm() {
247+
// Test with the provided encoded value
248+
let encoded = "ChExMDAwMDAwMDAwMDAwMDAwMA==".to_string();
249+
250+
let result = extract_dynamic_gas_price(encoded);
251+
252+
assert!(result.is_ok());
253+
let gas_price = result.unwrap();
254+
255+
assert_eq!(gas_price, 0.01);
256+
}
257+
}

crates/relayer/src/chain/cosmos/gas.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub async fn dynamic_gas_price(
4343
rpc_address: &Url,
4444
) -> GasPrice {
4545
if config.dynamic_gas_price.enabled {
46-
let dynamic_gas_price = query_eip_base_fee(rpc_address, &config.gas_price.denom, chain_id)
46+
let dynamic_gas_price = query_eip_base_fee(rpc_address, &config.gas_price.denom, &config.dynamic_gas_price.r#type, chain_id)
4747
.await
4848
.map(|base_fee| base_fee * config.dynamic_gas_price.multiplier)
4949
.map(|new_price| GasPrice {

crates/relayer/src/config/dynamic_gas.rs

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::fmt;
12
use serde::de::Error as DeserializeError;
23
use serde::de::Unexpected;
34
use serde::Deserialize;
@@ -15,31 +16,51 @@ flex_error::define_error! {
1516
}
1617
}
1718

19+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
20+
#[serde(rename_all = "snake_case")]
21+
pub enum DynamicGasType {
22+
SkipFeeMarket,
23+
Osmosis,
24+
CosmosEvm,
25+
}
26+
27+
impl fmt::Display for DynamicGasType {
28+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29+
match self {
30+
DynamicGasType::SkipFeeMarket => write!(f, "SkipFeeMarket"),
31+
DynamicGasType::Osmosis => write!(f, "Osmosis"),
32+
DynamicGasType::CosmosEvm => write!(f, "CosmosEVM"),
33+
}
34+
}
35+
}
36+
1837
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
1938
pub struct DynamicGasPrice {
2039
pub enabled: bool,
2140
pub multiplier: f64,
2241
pub max: f64,
42+
pub r#type: Option<DynamicGasType>,
2343
}
2444

2545
impl DynamicGasPrice {
2646
const DEFAULT_MULTIPLIER: f64 = 1.1;
2747
const DEFAULT_MAX: f64 = 0.6;
2848
const MIN_MULTIPLIER: f64 = 1.0;
2949

30-
pub fn enabled(multiplier: f64, max: f64) -> Result<Self, Error> {
31-
Self::new(true, multiplier, max)
50+
pub fn enabled(multiplier: f64, max: f64, r#type: Option<DynamicGasType>) -> Result<Self, Error> {
51+
Self::new(true, multiplier, max, r#type)
3252
}
3353

3454
pub fn disabled() -> Self {
3555
Self {
3656
enabled: false,
3757
multiplier: Self::DEFAULT_MULTIPLIER,
3858
max: Self::DEFAULT_MAX,
59+
r#type: None,
3960
}
4061
}
4162

42-
pub fn new(enabled: bool, multiplier: f64, max: f64) -> Result<Self, Error> {
63+
pub fn new(enabled: bool, multiplier: f64, max: f64, r#type: Option<DynamicGasType>) -> Result<Self, Error> {
4364
if multiplier < Self::MIN_MULTIPLIER {
4465
return Err(Error::multiplier_too_small(multiplier));
4566
}
@@ -48,15 +69,17 @@ impl DynamicGasPrice {
4869
enabled,
4970
multiplier,
5071
max,
72+
r#type,
5173
})
5274
}
5375

5476
// Unsafe GasMultiplier used for test cases only.
55-
pub fn unsafe_new(enabled: bool, multiplier: f64, max: f64) -> Self {
77+
pub fn unsafe_new(enabled: bool, multiplier: f64, max: f64, r#type: Option<DynamicGasType>) -> Self {
5678
Self {
5779
enabled,
5880
multiplier,
5981
max,
82+
r#type,
6083
}
6184
}
6285
}
@@ -77,15 +100,17 @@ impl<'de> Deserialize<'de> for DynamicGasPrice {
77100
enabled: bool,
78101
multiplier: f64,
79102
max: f64,
103+
r#type: Option<DynamicGasType>,
80104
}
81105

82106
let DynGas {
83107
enabled,
84108
multiplier,
85109
max,
110+
r#type,
86111
} = DynGas::deserialize(deserializer)?;
87112

88-
DynamicGasPrice::new(enabled, multiplier, max).map_err(|e| match e.detail() {
113+
DynamicGasPrice::new(enabled, multiplier, max, r#type).map_err(|e| match e.detail() {
89114
ErrorDetail::MultiplierTooSmall(_) => D::Error::invalid_value(
90115
Unexpected::Float(multiplier),
91116
&format!(
@@ -126,7 +151,7 @@ mod tests {
126151

127152
#[test]
128153
fn safe_gas_multiplier() {
129-
let dynamic_gas = DynamicGasPrice::new(true, 0.6, 0.6);
154+
let dynamic_gas = DynamicGasPrice::new(true, 0.6, 0.6, None);
130155
assert!(
131156
dynamic_gas.is_err(),
132157
"Gas multiplier should be an error if value is lower than 1.0: {dynamic_gas:?}"
@@ -135,8 +160,55 @@ mod tests {
135160

136161
#[test]
137162
fn unsafe_gas_multiplier() {
138-
let dynamic_gas = DynamicGasPrice::unsafe_new(true, 0.6, 0.4);
163+
let dynamic_gas = DynamicGasPrice::unsafe_new(true, 0.6, 0.4, None);
139164
assert_eq!(dynamic_gas.multiplier, 0.6);
140165
assert_eq!(dynamic_gas.max, 0.4);
141166
}
167+
168+
#[test]
169+
fn parse_valid_dynamic_gas_type() {
170+
#[derive(Debug, Deserialize)]
171+
struct DummyConfig {
172+
dynamic_gas: DynamicGasPrice,
173+
}
174+
let config: DummyConfig = toml::from_str(
175+
"dynamic_gas = { enabled = true, multiplier = 1.1, max = 0.6, type = 'skip_fee_market' }",
176+
)
177+
.unwrap();
178+
179+
assert_eq!(config.dynamic_gas.multiplier, 1.1);
180+
assert_eq!(config.dynamic_gas.max, 0.6);
181+
assert_eq!(config.dynamic_gas.r#type, Some(DynamicGasType::SkipFeeMarket));
182+
}
183+
184+
#[test]
185+
fn parse_invalid_dynamic_gas_type() {
186+
#[derive(Debug, Deserialize)]
187+
struct DummyConfig {
188+
dynamic_gas: DynamicGasPrice,
189+
}
190+
let err = toml::from_str::<DummyConfig>(
191+
"dynamic_gas = { enabled = true, multiplier = 1.1, max = 0.6, type = 'invalid_type' }",
192+
)
193+
.unwrap_err()
194+
.to_string();
195+
196+
assert!(err.contains("unknown variant `invalid_type`, expected one of `skip_fee_market`, `osmosis`, `cosmos_evm`"));
197+
}
198+
199+
#[test]
200+
fn parse_no_dynamic_gas_type() {
201+
#[derive(Debug, Deserialize)]
202+
struct DummyConfig {
203+
dynamic_gas: DynamicGasPrice,
204+
}
205+
let config: DummyConfig = toml::from_str(
206+
"dynamic_gas = { enabled = true, multiplier = 1.1, max = 0.6 }",
207+
)
208+
.unwrap();
209+
210+
assert_eq!(config.dynamic_gas.multiplier, 1.1);
211+
assert_eq!(config.dynamic_gas.max, 0.6);
212+
assert!(config.dynamic_gas.r#type.is_none());
213+
}
142214
}

0 commit comments

Comments
 (0)