Skip to content

Commit b7acec5

Browse files
dancoombsclaude
andauthored
feat(rpc): add rundler_getUserOperationGasPrice endpoint (#1263)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d723cf6 commit b7acec5

File tree

10 files changed

+255
-20
lines changed

10 files changed

+255
-20
lines changed

bin/rundler/src/cli/rpc.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use clap::Args;
1818
use rundler_builder::RemoteBuilderClient;
1919
use rundler_pool::RemotePoolClient;
2020
use rundler_provider::Providers;
21-
use rundler_rpc::{EthApiSettings, RpcTask, RpcTaskArgs};
21+
use rundler_rpc::{EthApiSettings, RpcTask, RpcTaskArgs, RundlerApiSettings};
2222
use rundler_task::{TaskSpawnerExt, server::connect_with_retries_shutdown};
2323
use rundler_types::chain::{ChainSpec, TryIntoWithSpec};
2424

@@ -98,6 +98,26 @@ pub struct RpcArgs {
9898
default_value = "false"
9999
)]
100100
permissions_enabled: bool,
101+
102+
/// Priority fee buffer percent for gas price suggestions.
103+
/// The suggested priority fee will be this percent above the current required priority fee.
104+
#[arg(
105+
long = "rpc.priority_fee_suggested_buffer_percent",
106+
name = "rpc.priority_fee_suggested_buffer_percent",
107+
env = "RPC_PRIORITY_FEE_SUGGESTED_BUFFER_PERCENT",
108+
default_value = "30"
109+
)]
110+
priority_fee_suggested_buffer_percent: u64,
111+
112+
/// Base fee buffer percent for gas price suggestions.
113+
/// The suggested max fee will use a base fee multiplied by (100 + this value) / 100.
114+
#[arg(
115+
long = "rpc.base_fee_suggested_buffer_percent",
116+
name = "rpc.base_fee_suggested_buffer_percent",
117+
env = "RPC_BASE_FEE_SUGGESTED_BUFFER_PERCENT",
118+
default_value = "50"
119+
)]
120+
base_fee_suggested_buffer_percent: u64,
101121
}
102122

103123
impl RpcArgs {
@@ -121,13 +141,19 @@ impl RpcArgs {
121141
.user_operation_event_block_distance_fallback,
122142
};
123143

144+
let rundler_api_settings = RundlerApiSettings {
145+
priority_fee_buffer_percent: self.priority_fee_suggested_buffer_percent,
146+
base_fee_buffer_percent: self.base_fee_suggested_buffer_percent,
147+
};
148+
124149
Ok(RpcTaskArgs {
125150
unsafe_mode: common.unsafe_mode,
126151
port: self.port,
127152
host: self.host.clone(),
128153
rpc_url: common.node_http.clone().context("must provide node_http")?,
129154
api_namespaces: apis,
130155
eth_api_settings,
156+
rundler_api_settings,
131157
estimation_settings: common.try_into_with_spec(&chain_spec)?,
132158
rpc_timeout: Duration::from_secs(self.timeout_seconds.parse()?),
133159
max_connections: self.max_connections,

crates/provider/src/fees/mod.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
// If not, see https://www.gnu.org/licenses/.
1313

1414
use alloy_primitives::B256;
15+
use alloy_rpc_types_eth::BlockNumberOrTag;
1516
use anyhow::Context;
1617
use rundler_types::{GasFees, PriorityFeeMode, chain::ChainSpec};
1718
use rundler_utils::{cache::LruMap, math};
1819
use tokio::{sync::Mutex as TokioMutex, try_join};
1920
use tracing::instrument;
2021

21-
use crate::{EvmProvider, FeeEstimator};
22+
use crate::{EvmProvider, FeeEstimator, LatestFeeEstimate};
2223

2324
mod oracle;
2425
use oracle::*;
@@ -83,8 +84,15 @@ impl<P: EvmProvider, O: FeeOracle> FeeEstimatorImpl<P, O> {
8384
}
8485

8586
#[instrument(skip_all)]
86-
async fn get_pending_base_fee(&self) -> anyhow::Result<u128> {
87-
Ok(self.provider.get_pending_base_fee().await?)
87+
async fn get_pending_base_fee_and_block_number(&self) -> anyhow::Result<(u128, u64)> {
88+
let fee_history = self
89+
.provider
90+
.fee_history(1, BlockNumberOrTag::Latest, &[])
91+
.await?;
92+
let base_fee = fee_history
93+
.next_block_base_fee()
94+
.context("should have a next block base fee")?;
95+
Ok((base_fee, fee_history.oldest_block))
8896
}
8997

9098
#[instrument(skip_all)]
@@ -100,10 +108,35 @@ impl<P: EvmProvider, O: FeeOracle> FeeEstimatorImpl<P, O> {
100108
impl<P: EvmProvider, O: FeeOracle> FeeEstimator for FeeEstimatorImpl<P, O> {
101109
#[instrument(skip_all)]
102110
async fn latest_bundle_fees(&self) -> anyhow::Result<(GasFees, u128)> {
103-
let (base_fee, priority_fee) =
104-
try_join!(self.get_pending_base_fee(), self.get_priority_fee())?;
111+
let estimate = self.latest_fee_estimate().await?;
112+
let bundle_fees = GasFees {
113+
max_fee_per_gas: estimate
114+
.required_base_fee
115+
.saturating_add(estimate.required_priority_fee),
116+
max_priority_fee_per_gas: estimate.required_priority_fee,
117+
};
118+
119+
Ok((bundle_fees, estimate.required_base_fee))
120+
}
105121

106-
Ok(self.calc_bundle_fees(base_fee, priority_fee, None))
122+
#[instrument(skip_all)]
123+
async fn latest_fee_estimate(&self) -> anyhow::Result<LatestFeeEstimate> {
124+
let ((base_fee, block_number), priority_fee) = try_join!(
125+
self.get_pending_base_fee_and_block_number(),
126+
self.get_priority_fee()
127+
)?;
128+
129+
let required_base_fee =
130+
math::increase_by_percent(base_fee, self.bundle_base_fee_overhead_percent);
131+
let required_priority_fee =
132+
math::increase_by_percent(priority_fee, self.bundle_priority_fee_overhead_percent);
133+
134+
Ok(LatestFeeEstimate {
135+
block_number,
136+
base_fee,
137+
required_base_fee,
138+
required_priority_fee,
139+
})
107140
}
108141

109142
#[instrument(skip_all)]
@@ -116,8 +149,10 @@ impl<P: EvmProvider, O: FeeOracle> FeeEstimator for FeeEstimatorImpl<P, O> {
116149
let entry = match cache.get(&block_hash) {
117150
Some(entry) => *entry,
118151
None => {
119-
let (base_fee, priority_fee) =
120-
try_join!(self.get_pending_base_fee(), self.get_priority_fee())?;
152+
let ((base_fee, _), priority_fee) = try_join!(
153+
self.get_pending_base_fee_and_block_number(),
154+
self.get_priority_fee()
155+
)?;
121156

122157
let entry = CacheEntry {
123158
base_fee,

crates/provider/src/traits/fee_estimator.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
use alloy_primitives::B256;
1515
use rundler_types::GasFees;
1616

17+
/// Latest fee estimates for bundling.
18+
#[derive(Debug, Clone, Copy)]
19+
pub struct LatestFeeEstimate {
20+
/// Block number this estimate is based on.
21+
pub block_number: u64,
22+
/// Current pending base fee (without bundler overhead).
23+
pub base_fee: u128,
24+
/// Base fee with bundler overhead applied.
25+
pub required_base_fee: u128,
26+
/// Priority fee with bundler overhead applied.
27+
pub required_priority_fee: u128,
28+
}
29+
1730
/// Trait for a fee estimator.
1831
#[async_trait::async_trait]
1932
#[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)]
@@ -34,6 +47,9 @@ pub trait FeeEstimator: Send + Sync {
3447
/// Returns the latest bundle fees.
3548
async fn latest_bundle_fees(&self) -> anyhow::Result<(GasFees, u128)>;
3649

50+
/// Returns the latest fee estimate including base fee, priority fee (with bundler overhead), and block number.
51+
async fn latest_fee_estimate(&self) -> anyhow::Result<LatestFeeEstimate>;
52+
3753
/// Returns the required operation fees for the given bundle fees.
3854
fn required_op_fees(&self, bundle_fees: GasFees) -> GasFees;
3955
}

crates/provider/src/traits/test_utils.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ use super::error::ProviderResult;
3232
use crate::{
3333
AggregatorOut, Block, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasOracleSync,
3434
DAGasProvider, DepositInfo, EntryPoint, EntryPointProvider, EvmCall,
35-
EvmProvider as EvmProviderTrait, ExecutionResult, FeeEstimator, HandleOpsOut, RpcRecv, RpcSend,
36-
SignatureAggregator, SimulationProvider, Transaction, TransactionReceipt, TransactionRequest,
35+
EvmProvider as EvmProviderTrait, ExecutionResult, FeeEstimator, HandleOpsOut,
36+
LatestFeeEstimate, RpcRecv, RpcSend, SignatureAggregator, SimulationProvider, Transaction,
37+
TransactionReceipt, TransactionRequest,
3738
};
3839

3940
mockall::mock! {
@@ -392,6 +393,7 @@ mockall::mock! {
392393
min_fees: Option<GasFees>,
393394
) -> anyhow::Result<(GasFees, u128)>;
394395
async fn latest_bundle_fees(&self) -> anyhow::Result<(GasFees, u128)>;
396+
async fn latest_fee_estimate(&self) -> anyhow::Result<LatestFeeEstimate>;
395397
fn required_op_fees(&self, bundle_fees: GasFees) -> GasFees;
396398
}
397399
}

crates/rpc/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub use eth::{EthApiClient, EthApiSettings};
3333
mod health;
3434

3535
mod rundler;
36-
pub use rundler::RundlerApiClient;
36+
pub use rundler::{RundlerApiClient, RundlerApiSettings};
3737

3838
mod task;
3939
pub use task::{Args as RpcTaskArgs, RpcTask};

crates/rpc/src/rundler.rs

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,44 @@
1111
// You should have received a copy of the GNU General Public License along with Rundler.
1212
// If not, see https://www.gnu.org/licenses/.
1313

14-
use alloy_primitives::{Address, B256, U128, U256};
14+
use alloy_primitives::{Address, B256, U64, U128, U256};
1515
use anyhow::Context;
1616
use async_trait::async_trait;
1717
use futures_util::future;
1818
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
1919
use rundler_provider::{EvmProvider, FeeEstimator};
2020
use rundler_types::{
21-
UserOperation, UserOperationId, UserOperationVariant, chain::ChainSpec, pool::Pool,
21+
GasFees, UserOperation, UserOperationId, UserOperationVariant, chain::ChainSpec, pool::Pool,
2222
};
2323
use tracing::instrument;
2424

2525
use crate::{
2626
eth::{EntryPointRouter, EthResult, EthRpcError},
2727
types::{
28-
RpcMinedUserOperation, RpcUserOperation, RpcUserOperationStatus, UOStatusEnum,
29-
UserOperationStatusEnum,
28+
RpcMinedUserOperation, RpcSuggestedGasFees, RpcUserOperation, RpcUserOperationGasPrice,
29+
RpcUserOperationStatus, UOStatusEnum, UserOperationStatusEnum,
3030
},
3131
utils::{self, TryIntoRundlerType},
3232
};
3333

34+
/// Settings for the rundler API
35+
#[derive(Clone, Copy, Debug)]
36+
pub struct RundlerApiSettings {
37+
/// Priority fee buffer percent for gas price suggestions (e.g., 30 for 30% above current)
38+
pub priority_fee_buffer_percent: u64,
39+
/// Base fee buffer percent for gas price suggestions (e.g., 50 for 1.5x base fee)
40+
pub base_fee_buffer_percent: u64,
41+
}
42+
43+
impl Default for RundlerApiSettings {
44+
fn default() -> Self {
45+
Self {
46+
priority_fee_buffer_percent: 30,
47+
base_fee_buffer_percent: 50,
48+
}
49+
}
50+
}
51+
3452
#[rpc(client, server, namespace = "rundler")]
3553
pub trait RundlerApi {
3654
/// Returns the maximum priority fee per gas required by Rundler
@@ -72,6 +90,10 @@ pub trait RundlerApi {
7290
sender: Address,
7391
nonce: U256,
7492
) -> RpcResult<Option<RpcUserOperation>>;
93+
94+
/// Returns suggested gas prices for user operations
95+
#[method(name = "getUserOperationGasPrice")]
96+
async fn get_user_operation_gas_price(&self) -> RpcResult<RpcUserOperationGasPrice>;
7597
}
7698

7799
pub(crate) struct RundlerApi<P, F, E> {
@@ -80,6 +102,7 @@ pub(crate) struct RundlerApi<P, F, E> {
80102
pool_server: P,
81103
entry_point_router: EntryPointRouter,
82104
evm_provider: E,
105+
settings: RundlerApiSettings,
83106
}
84107

85108
#[async_trait]
@@ -149,6 +172,15 @@ where
149172
)
150173
.await
151174
}
175+
176+
#[instrument(skip_all, fields(rpc_method = "rundler_getUserOperationGasPrice"))]
177+
async fn get_user_operation_gas_price(&self) -> RpcResult<RpcUserOperationGasPrice> {
178+
utils::safe_call_rpc_handler(
179+
"rundler_getUserOperationGasPrice",
180+
RundlerApi::get_user_operation_gas_price(self),
181+
)
182+
.await
183+
}
152184
}
153185

154186
impl<P, F, E> RundlerApi<P, F, E>
@@ -163,22 +195,30 @@ where
163195
pool_server: P,
164196
fee_estimator: F,
165197
evm_provider: E,
198+
settings: RundlerApiSettings,
166199
) -> Self {
167200
Self {
168201
chain_spec: chain_spec.clone(),
169202
entry_point_router,
170203
pool_server,
171204
fee_estimator,
172205
evm_provider,
206+
settings,
173207
}
174208
}
175209
#[instrument(skip_all)]
176210
async fn max_priority_fee_per_gas(&self) -> EthResult<U128> {
177-
let (bundle_fees, _) = self
211+
let estimate = self
178212
.fee_estimator
179-
.latest_bundle_fees()
213+
.latest_fee_estimate()
180214
.await
181215
.context("should get required fees")?;
216+
let bundle_fees = GasFees {
217+
max_fee_per_gas: estimate
218+
.required_base_fee
219+
.saturating_add(estimate.required_priority_fee),
220+
max_priority_fee_per_gas: estimate.required_priority_fee,
221+
};
182222
Ok(U128::from(
183223
self.fee_estimator
184224
.required_op_fees(bundle_fees)
@@ -344,4 +384,50 @@ where
344384

345385
Ok(uo.map(|uo| uo.uo.into()))
346386
}
387+
388+
#[instrument(skip_all)]
389+
async fn get_user_operation_gas_price(&self) -> EthResult<RpcUserOperationGasPrice> {
390+
// Get base fee, priority fee (with bundler overhead), and block number
391+
let estimate = self
392+
.fee_estimator
393+
.latest_fee_estimate()
394+
.await
395+
.context("should get fee estimate")?;
396+
let bundle_fees = GasFees {
397+
max_fee_per_gas: estimate
398+
.required_base_fee
399+
.saturating_add(estimate.required_priority_fee),
400+
max_priority_fee_per_gas: estimate.required_priority_fee,
401+
};
402+
403+
// Convert to user operation fees (current required)
404+
let op_fees = self.fee_estimator.required_op_fees(bundle_fees);
405+
let priority_fee = op_fees.max_priority_fee_per_gas;
406+
407+
// Calculate suggested priority fee with configured buffer
408+
let priority_multiplier =
409+
100u128.saturating_add(u128::from(self.settings.priority_fee_buffer_percent));
410+
let suggested_priority_fee = priority_fee
411+
.saturating_mul(priority_multiplier)
412+
.saturating_div(100);
413+
414+
// Calculate suggested max fee with configured base fee buffer
415+
let base_fee_multiplier =
416+
100u128.saturating_add(u128::from(self.settings.base_fee_buffer_percent));
417+
let suggested_max_fee = estimate
418+
.required_base_fee
419+
.saturating_mul(base_fee_multiplier)
420+
.saturating_div(100)
421+
.saturating_add(suggested_priority_fee);
422+
423+
Ok(RpcUserOperationGasPrice {
424+
priority_fee: U128::from(priority_fee),
425+
base_fee: U128::from(estimate.base_fee),
426+
block_number: U64::from(estimate.block_number),
427+
suggested: RpcSuggestedGasFees {
428+
max_priority_fee_per_gas: U128::from(suggested_priority_fee),
429+
max_fee_per_gas: U128::from(suggested_max_fee),
430+
},
431+
})
432+
}
347433
}

crates/rpc/src/task.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::{
4141
},
4242
health::{HealthChecker, SystemApiServer},
4343
rpc_metrics::{HttpMetricMiddlewareLayer, RpcMetricsMiddlewareLayer},
44-
rundler::{RundlerApi, RundlerApiServer},
44+
rundler::{RundlerApi, RundlerApiServer, RundlerApiSettings},
4545
types::ApiNamespace,
4646
};
4747

@@ -62,6 +62,8 @@ pub struct Args {
6262
pub rpc_url: String,
6363
/// eth_ API settings.
6464
pub eth_api_settings: EthApiSettings,
65+
/// rundler_ API settings.
66+
pub rundler_api_settings: RundlerApiSettings,
6567
/// Estimation settings.
6668
pub estimation_settings: EstimationSettings,
6769
/// RPC timeout.
@@ -292,6 +294,7 @@ where
292294
self.pool.clone(),
293295
fee_estimator,
294296
self.providers.evm().clone(),
297+
self.args.rundler_api_settings,
295298
)
296299
.into_rpc(),
297300
)?;

0 commit comments

Comments
 (0)