Skip to content

feat(rpc): add get pending user operation by sender nonce RPC #1157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2025
Merged
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
17 changes: 17 additions & 0 deletions crates/pool/proto/op_pool/op_pool.proto
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ service OpPool {
// Get a UserOperation by its hash
rpc GetOpByHash (GetOpByHashRequest) returns (GetOpByHashResponse);

// Get a UserOperation by its id
rpc GetOpById (GetOpByIdRequest) returns (GetOpByIdResponse);

// Removes UserOperations from the mempool
rpc RemoveOps(RemoveOpsRequest) returns (RemoveOpsResponse);

Expand Down Expand Up @@ -381,6 +384,20 @@ message GetOpByHashSuccess {
MempoolOp op = 1;
}

message GetOpByIdRequest {
bytes sender = 1;
bytes nonce = 2;
}
message GetOpByIdResponse {
oneof result {
GetOpByIdSuccess success = 1;
MempoolError failure = 2;
}
}
message GetOpByIdSuccess {
MempoolOp op = 1;
}

message GetReputationStatusResponse {
oneof result {
GetReputationStatusSuccess success = 1;
Expand Down
3 changes: 3 additions & 0 deletions crates/pool/src/mempool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ pub(crate) trait Mempool: Send + Sync {
/// Looks up a user operation by hash, returns None if not found
fn get_user_operation_by_hash(&self, hash: B256) -> Option<Arc<PoolOperation>>;

/// Looks up a user operation by id, returns None if not found
fn get_op_by_id(&self, id: &UserOperationId) -> Option<Arc<PoolOperation>>;

/// Debug methods
/// Clears the mempool of UOs or reputation of all addresses
fn clear_state(&self, clear_mempool: bool, clear_paymaster: bool, clear_reputation: bool);
Expand Down
4 changes: 4 additions & 0 deletions crates/pool/src/mempool/uo_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,10 @@ where
self.ep_specific_metrics.removed_operations.increment(count);
}

fn get_op_by_id(&self, id: &UserOperationId) -> Option<Arc<PoolOperation>> {
self.state.read().pool.get_operation_by_id(id)
}

fn remove_op_by_id(&self, id: &UserOperationId) -> MempoolResult<Option<B256>> {
// Check for the operation in the pool and its age
let po = {
Expand Down
30 changes: 30 additions & 0 deletions crates/pool/src/server/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ impl Pool for LocalPoolHandle {
}
}

async fn get_op_by_id(&self, id: UserOperationId) -> PoolResult<Option<PoolOperation>> {
let req = ServerRequestKind::GetOpById { id };
let resp = self.send(req).await?;
match resp {
ServerResponse::GetOpById { op } => Ok(op),
_ => Err(PoolError::UnexpectedResponse),
}
}

async fn remove_ops(&self, entry_point: Address, ops: Vec<B256>) -> PoolResult<()> {
let req = ServerRequestKind::RemoveOps { entry_point, ops };
let resp = self.send(req).await?;
Expand Down Expand Up @@ -479,6 +488,15 @@ impl LocalPoolServerRunner {
Ok(None)
}

fn get_op_by_id(&self, id: &UserOperationId) -> PoolResult<Option<PoolOperation>> {
for mempool in self.mempools.values() {
if let Some(op) = mempool.get_op_by_id(id) {
return Ok(Some((*op).clone()));
}
}
Ok(None)
}

fn remove_ops(&self, entry_point: Address, ops: &[B256]) -> PoolResult<()> {
let mempool = self.get_pool(entry_point)?;
mempool.remove_operations(ops);
Expand Down Expand Up @@ -708,6 +726,12 @@ impl LocalPoolServerRunner {
Err(e) => Err(e),
}
}
ServerRequestKind::GetOpById { id } => {
match self.get_op_by_id(&id) {
Ok(op) => Ok(ServerResponse::GetOpById { op }),
Err(e) => Err(e),
}
}
ServerRequestKind::RemoveOps { entry_point, ops } => {
match self.remove_ops(entry_point, &ops) {
Ok(_) => Ok(ServerResponse::RemoveOps),
Expand Down Expand Up @@ -815,6 +839,9 @@ enum ServerRequestKind {
GetOpByHash {
hash: B256,
},
GetOpById {
id: UserOperationId,
},
RemoveOps {
entry_point: Address,
ops: Vec<B256>,
Expand Down Expand Up @@ -884,6 +911,9 @@ enum ServerResponse {
GetOpByHash {
op: Option<PoolOperation>,
},
GetOpById {
op: Option<PoolOperation>,
},
RemoveOps,
RemoveOpById {
hash: Option<B256>,
Expand Down
50 changes: 42 additions & 8 deletions crates/pool/src/server/remote/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ use super::protos::{
self, add_op_response, admin_set_tracking_response, debug_clear_state_response,
debug_dump_mempool_response, debug_dump_paymaster_balances_response,
debug_dump_reputation_response, debug_set_reputation_response, get_op_by_hash_response,
get_ops_by_hashes_response, get_ops_response, get_ops_summaries_response,
get_reputation_status_response, get_stake_status_response, op_pool_client::OpPoolClient,
remove_op_by_id_response, remove_ops_response, update_entities_response, AddOpRequest,
AdminSetTrackingRequest, DebugClearStateRequest, DebugDumpMempoolRequest,
DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest, DebugSetReputationRequest,
GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, RemoveOpsRequest,
ReputationStatus as ProtoReputationStatus, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse,
TryUoFromProto, UpdateEntitiesRequest,
get_op_by_id_response, get_ops_by_hashes_response, get_ops_response,
get_ops_summaries_response, get_reputation_status_response, get_stake_status_response,
op_pool_client::OpPoolClient, remove_op_by_id_response, remove_ops_response,
update_entities_response, AddOpRequest, AdminSetTrackingRequest, DebugClearStateRequest,
DebugDumpMempoolRequest, DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest,
DebugSetReputationRequest, GetOpByIdRequest, GetOpsRequest, GetReputationStatusRequest,
GetStakeStatusRequest, RemoveOpsRequest, ReputationStatus as ProtoReputationStatus,
SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, TryUoFromProto, UpdateEntitiesRequest,
};

/// Remote pool client
Expand Down Expand Up @@ -324,6 +324,40 @@ impl Pool for RemotePoolClient {
}
}

async fn get_op_by_id(&self, id: UserOperationId) -> PoolResult<Option<PoolOperation>> {
let res = self
.op_pool_client
.clone()
.get_op_by_id(GetOpByIdRequest {
sender: id.sender.to_proto_bytes(),
nonce: id.nonce.to_proto_bytes(),
})
.await
.map_err(anyhow::Error::from)?
.into_inner()
.result;

match res {
Some(get_op_by_id_response::Result::Success(s)) => Ok(s
.op
.map(|proto_uo| {
PoolOperation::try_uo_from_proto(proto_uo, &self.chain_spec)
.context("should convert proto uo to pool operation")
.map_err(PoolError::from)
})
.transpose()?),
Some(get_op_by_id_response::Result::Failure(e)) => match e.error {
Some(_) => Err(e.try_into()?),
None => Err(PoolError::Other(anyhow::anyhow!(
"should have received error from op pool"
)))?,
},
None => Err(PoolError::Other(anyhow::anyhow!(
"should have received result from op pool"
)))?,
}
}

async fn remove_ops(&self, entry_point: Address, ops: Vec<B256>) -> PoolResult<()> {
let res = self
.op_pool_client
Expand Down
48 changes: 39 additions & 9 deletions crates/pool/src/server/remote/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ use super::protos::{
add_op_response, admin_set_tracking_response, debug_clear_state_response,
debug_dump_mempool_response, debug_dump_paymaster_balances_response,
debug_dump_reputation_response, debug_set_reputation_response, get_op_by_hash_response,
get_ops_by_hashes_response, get_ops_response, get_ops_summaries_response,
get_reputation_status_response, get_stake_status_response,
get_op_by_id_response, get_ops_by_hashes_response, get_ops_response,
get_ops_summaries_response, get_reputation_status_response, get_stake_status_response,
op_pool_server::{OpPool, OpPoolServer},
remove_op_by_id_response, remove_ops_response, update_entities_response, AddOpRequest,
AddOpResponse, AddOpSuccess, AdminSetTrackingRequest, AdminSetTrackingResponse,
Expand All @@ -52,13 +52,14 @@ use super::protos::{
DebugDumpPaymasterBalancesSuccess, DebugDumpReputationRequest, DebugDumpReputationResponse,
DebugDumpReputationSuccess, DebugSetReputationRequest, DebugSetReputationResponse,
DebugSetReputationSuccess, GetOpByHashRequest, GetOpByHashResponse, GetOpByHashSuccess,
GetOpsByHashesRequest, GetOpsByHashesResponse, GetOpsByHashesSuccess, GetOpsRequest,
GetOpsResponse, GetOpsSuccess, GetOpsSummariesRequest, GetOpsSummariesResponse,
GetOpsSummariesSuccess, GetReputationStatusRequest, GetReputationStatusResponse,
GetReputationStatusSuccess, GetStakeStatusRequest, GetStakeStatusResponse,
GetStakeStatusSuccess, GetSupportedEntryPointsRequest, GetSupportedEntryPointsResponse,
MempoolOp, PoolOperationSummary, RemoveOpByIdRequest, RemoveOpByIdResponse,
RemoveOpByIdSuccess, RemoveOpsRequest, RemoveOpsResponse, RemoveOpsSuccess, ReputationStatus,
GetOpByIdRequest, GetOpByIdResponse, GetOpByIdSuccess, GetOpsByHashesRequest,
GetOpsByHashesResponse, GetOpsByHashesSuccess, GetOpsRequest, GetOpsResponse, GetOpsSuccess,
GetOpsSummariesRequest, GetOpsSummariesResponse, GetOpsSummariesSuccess,
GetReputationStatusRequest, GetReputationStatusResponse, GetReputationStatusSuccess,
GetStakeStatusRequest, GetStakeStatusResponse, GetStakeStatusSuccess,
GetSupportedEntryPointsRequest, GetSupportedEntryPointsResponse, MempoolOp,
PoolOperationSummary, RemoveOpByIdRequest, RemoveOpByIdResponse, RemoveOpByIdSuccess,
RemoveOpsRequest, RemoveOpsResponse, RemoveOpsSuccess, ReputationStatus,
SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, TryUoFromProto, UpdateEntitiesRequest,
UpdateEntitiesResponse, UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET,
};
Expand Down Expand Up @@ -302,6 +303,35 @@ impl OpPool for OpPoolImpl {
Ok(Response::new(resp))
}

async fn get_op_by_id(
&self,
request: Request<GetOpByIdRequest>,
) -> Result<Response<GetOpByIdResponse>> {
let req = request.into_inner();

let resp = match self
.local_pool
.get_op_by_id(UserOperationId {
sender: from_bytes(&req.sender)
.map_err(|e| Status::invalid_argument(format!("Invalid sender: {e}")))?,
nonce: from_bytes(&req.nonce)
.map_err(|e| Status::invalid_argument(format!("Invalid nonce: {e}")))?,
})
.await
{
Ok(op) => GetOpByIdResponse {
result: Some(get_op_by_id_response::Result::Success(GetOpByIdSuccess {
op: op.map(|op| MempoolOp::from(&op)),
})),
},
Err(error) => GetOpByIdResponse {
result: Some(get_op_by_id_response::Result::Failure(error.into())),
},
};

Ok(Response::new(resp))
}

async fn remove_ops(
&self,
request: Request<RemoveOpsRequest>,
Expand Down
42 changes: 40 additions & 2 deletions crates/rpc/src/rundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// You should have received a copy of the GNU General Public License along with Rundler.
// If not, see https://www.gnu.org/licenses/.

use alloy_primitives::{Address, B256, U128};
use alloy_primitives::{Address, B256, U128, U256};
use anyhow::Context;
use async_trait::async_trait;
use futures_util::{future, TryFutureExt};
Expand All @@ -20,7 +20,7 @@ use rundler_provider::{EvmProvider, FeeEstimator};
use rundler_types::{
chain::{ChainSpec, IntoWithSpec},
pool::Pool,
UserOperation, UserOperationVariant,
UserOperation, UserOperationId, UserOperationVariant,
};
use tracing::instrument;

Expand Down Expand Up @@ -65,6 +65,14 @@ pub trait RundlerApi {
/// Gets the status of a user operation by user operation hash
#[method(name = "getUserOperationStatus")]
async fn get_user_operation_status(&self, uo_hash: B256) -> RpcResult<RpcUserOperationStatus>;

/// Gets the required fees for a sender nonce
#[method(name = "getPendingUserOperationBySenderNonce")]
async fn get_pending_user_operation_by_sender_nonce(
&self,
sender: Address,
nonce: U256,
) -> RpcResult<Option<RpcUserOperation>>;
}

pub(crate) struct RundlerApi<P, F, E> {
Expand Down Expand Up @@ -126,6 +134,22 @@ where
)
.await
}

#[instrument(
skip_all,
fields(rpc_method = "rundler_getPendingUserOperationForSenderNonce")
)]
async fn get_pending_user_operation_by_sender_nonce(
&self,
sender: Address,
nonce: U256,
) -> RpcResult<Option<RpcUserOperation>> {
utils::safe_call_rpc_handler(
"rundler_getPendingUserOperationBySenderNonce",
RundlerApi::get_pending_user_operation_by_sender_nonce(self, sender, nonce),
)
.await
}
}

impl<P, F, E> RundlerApi<P, F, E>
Expand Down Expand Up @@ -273,4 +297,18 @@ where
receipt: None,
})
}

async fn get_pending_user_operation_by_sender_nonce(
&self,
sender: Address,
nonce: U256,
) -> EthResult<Option<RpcUserOperation>> {
let uo = self
.pool_server
.get_op_by_id(UserOperationId { sender, nonce })
.await
.map_err(EthRpcError::from)?;

Ok(uo.map(|uo| uo.uo.into()))
}
}
4 changes: 4 additions & 0 deletions crates/types/src/pool/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ pub trait Pool: Send + Sync {
/// Returns None if the operation is not found
async fn get_op_by_hash(&self, hash: B256) -> PoolResult<Option<PoolOperation>>;

/// Get an operation from the pool by id
async fn get_op_by_id(&self, id: UserOperationId) -> PoolResult<Option<PoolOperation>>;

/// Remove operations from the pool by hash
async fn remove_ops(&self, entry_point: Address, ops: Vec<B256>) -> PoolResult<()>;

Expand Down Expand Up @@ -196,6 +199,7 @@ mockall::mock! {
hashes: Vec<B256>,
) -> PoolResult<Vec<PoolOperation>>;
async fn get_op_by_hash(&self, hash: B256) -> PoolResult<Option<PoolOperation>>;
async fn get_op_by_id(&self, id: UserOperationId) -> PoolResult<Option<PoolOperation>>;
async fn remove_ops(&self, entry_point: Address, ops: Vec<B256>) -> PoolResult<()>;
async fn remove_op_by_id(
&self,
Expand Down
25 changes: 25 additions & 0 deletions docs/architecture/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Rundler specific methods that are not specified by the ERC-4337 spec. This names
| [`rundler_dropLocalUserOperation`](#rundler_droplocaluseroperation) | ✅ |
| [`rundler_getMinedUserOperation`](#rundler_getmineduseroperation) | ✅ |
| [`rundler_getUserOperationStatus`](#rundler_getuseroperationstatus) | ✅ |
| [`rundler_getPendingUserOperationBySenderNonce`](#rundler_getpendinguseroperationbysendernonce) | ✅ |

#### `rundler_maxPriorityFeePerGas`

Expand Down Expand Up @@ -293,6 +294,30 @@ Possible statuses:
}
```

#### `rundler_getPendingUserOperationBySenderNonce`

Gets a pending user operation for the given sender & nonce. If a user operation is not pending for that sender & nonce pair, it will return null.

```
# Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "rundler_getPendingUserOperationBySenderNonce",
"params": [
"0x...", // sender address
"0x...", // nonce
]
}

# Response
{
"jsonrpc": "2.0",
"id": 1,
"result": null | UserOperation
}
```

### `admin_` Namespace

Administration methods specific to Rundler. This namespace should not be open to the public.
Expand Down