diff --git a/Cargo.lock b/Cargo.lock index d655ef2dc..6aecb83f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4069,6 +4069,7 @@ dependencies = [ "futures-util", "jsonrpsee", "metrics", + "mockall", "rundler-builder", "rundler-pool", "rundler-provider", diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index 5bcb0892c..d6b7a4045 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -110,6 +110,8 @@ message MempoolOp { // multiple UserOperations in the mempool, otherwise just one UserOperation is // permitted bool account_is_staked = 8; + // The entry point address of this operation + bytes entry_point = 9; } // Defines the gRPC endpoints for a UserOperation mempool service @@ -125,6 +127,9 @@ service OpPool { // Get up to `max_ops` from the mempool. rpc GetOps (GetOpsRequest) returns (GetOpsResponse); + // Get a UserOperation by its hash + rpc GetOpByHash (GetOpByHashRequest) returns (GetOpByHashResponse); + // Removes UserOperations from the mempool rpc RemoveOps(RemoveOpsRequest) returns (RemoveOpsResponse); @@ -133,10 +138,13 @@ service OpPool { // Clears the bundler mempool and reputation data of paymasters/accounts/factories/aggregators rpc DebugClearState (DebugClearStateRequest) returns (DebugClearStateResponse); + // Dumps the current UserOperations mempool rpc DebugDumpMempool (DebugDumpMempoolRequest) returns (DebugDumpMempoolResponse); + // Sets reputation of given addresses. rpc DebugSetReputation (DebugSetReputationRequest) returns (DebugSetReputationResponse); + // Returns the reputation data of all observed addresses. Returns an array of // reputation objects, each with the fields described above in // debug_bundler_setReputation @@ -197,6 +205,20 @@ message GetOpsSuccess { repeated MempoolOp ops = 1; } +message GetOpByHashRequest { + // The serialized UserOperation hash + bytes hash = 1; +} +message GetOpByHashResponse { + oneof result { + GetOpByHashSuccess success = 1; + MempoolError failure = 2; + } +} +message GetOpByHashSuccess { + MempoolOp op = 1; +} + message GetReputationStatusResponse { oneof result { GetReputationStatusSuccess success = 1; diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index cd92cf8f8..3244300d7 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -80,6 +80,9 @@ pub trait Mempool: Send + Sync + 'static { /// Returns the all operations from the pool up to a max size fn all_operations(&self, max: usize) -> Vec>; + /// Looks up a user operation by hash, returns None if not found + fn get_user_operation_by_hash(&self, hash: H256) -> Option>; + /// Debug methods /// Clears the mempool @@ -168,6 +171,8 @@ pub enum OperationOrigin { pub struct PoolOperation { /// The user operation stored in the pool pub uo: UserOperation, + /// The entry point address for this operation + pub entry_point: Address, /// The aggregator address for this operation, if any. pub aggregator: Option
, /// The valid time range for this operation. @@ -264,6 +269,7 @@ mod tests { init_code: factory.as_fixed_bytes().into(), ..Default::default() }, + entry_point: Address::random(), aggregator: Some(aggregator), valid_time_range: ValidTimeRange::all_time(), expected_code_hash: H256::random(), diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index d142721ff..3b9b52f42 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -331,6 +331,7 @@ where let valid_time_range = sim_result.valid_time_range; let pool_op = PoolOperation { uo: op, + entry_point: self.config.entry_point, aggregator: None, valid_time_range, expected_code_hash: sim_result.code_hash, @@ -451,6 +452,10 @@ where self.state.read().pool.best_operations().take(max).collect() } + fn get_user_operation_by_hash(&self, hash: H256) -> Option> { + self.state.read().pool.get_operation_by_hash(hash) + } + fn clear(&self) { self.state.write().pool.clear() } @@ -997,6 +1002,34 @@ mod tests { check_ops(pool.best_operations(1, 0).unwrap(), vec![]); } + #[tokio::test] + async fn test_get_user_op_by_hash() { + let op = create_op(Address::random(), 0, 0); + let pool = create_pool(vec![op.clone()]); + + let hash = pool + .add_operation(OperationOrigin::Local, op.op.clone()) + .await + .unwrap(); + + let pool_op = pool.get_user_operation_by_hash(hash).unwrap(); + assert_eq!(pool_op.uo, op.op); + } + + #[tokio::test] + async fn test_get_user_op_by_hash_not_found() { + let op = create_op(Address::random(), 0, 0); + let pool = create_pool(vec![op.clone()]); + + let _ = pool + .add_operation(OperationOrigin::Local, op.op.clone()) + .await + .unwrap(); + + let pool_op = pool.get_user_operation_by_hash(H256::random()); + assert_eq!(pool_op, None); + } + #[derive(Clone, Debug)] struct OpWithErrors { op: UserOperation, diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index 2f9f90186..566b81614 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -150,6 +150,15 @@ impl PoolServer for LocalPoolHandle { } } + async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { + let req = ServerRequestKind::GetOpByHash { hash }; + let resp = self.send(req).await?; + match resp { + ServerResponse::GetOpByHash { op } => Ok(op), + _ => Err(PoolServerError::UnexpectedResponse), + } + } + async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()> { let req = ServerRequestKind::RemoveOps { entry_point, ops }; let resp = self.send(req).await?; @@ -326,6 +335,15 @@ where .collect()) } + fn get_op_by_hash(&self, hash: H256) -> PoolResult> { + for mempool in self.mempools.values() { + if let Some(op) = mempool.get_user_operation_by_hash(hash) { + return Ok(Some((*op).clone())); + } + } + Ok(None) + } + fn remove_ops(&self, entry_point: Address, ops: &[H256]) -> PoolResult<()> { let mempool = self.get_pool(entry_point)?; mempool.remove_operations(ops); @@ -441,6 +459,12 @@ where Err(e) => Err(e), } }, + ServerRequestKind::GetOpByHash { hash } => { + match self.get_op_by_hash(hash) { + Ok(op) => Ok(ServerResponse::GetOpByHash { op }), + Err(e) => Err(e), + } + } ServerRequestKind::RemoveOps { entry_point, ops } => { match self.remove_ops(entry_point, &ops) { Ok(_) => Ok(ServerResponse::RemoveOps), @@ -535,6 +559,9 @@ enum ServerRequestKind { max_ops: u64, shard_index: u64, }, + GetOpByHash { + hash: H256, + }, RemoveOps { entry_point: Address, ops: Vec, @@ -576,6 +603,9 @@ enum ServerResponse { GetOps { ops: Vec, }, + GetOpByHash { + op: Option, + }, RemoveOps, UpdateEntities, DebugClearState, diff --git a/crates/pool/src/server/mod.rs b/crates/pool/src/server/mod.rs index 4287da371..c854e8be4 100644 --- a/crates/pool/src/server/mod.rs +++ b/crates/pool/src/server/mod.rs @@ -69,6 +69,11 @@ pub trait PoolServer: Send + Sync + 'static { shard_index: u64, ) -> PoolResult>; + /// Get an operation from the pool by hash + /// Checks each entry point in order until the operation is found + /// Returns None if the operation is not found + async fn get_op_by_hash(&self, hash: H256) -> PoolResult>; + /// Remove operations from the pool by hash async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()>; diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 96ac9cbf6..ad5a2e62a 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -34,12 +34,12 @@ use tonic_health::{ use super::protos::{ self, add_op_response, debug_clear_state_response, debug_dump_mempool_response, - debug_dump_reputation_response, debug_set_reputation_response, get_ops_response, - get_reputation_status_response, get_stake_status_response, op_pool_client::OpPoolClient, - remove_ops_response, update_entities_response, AddOpRequest, DebugClearStateRequest, - DebugDumpMempoolRequest, DebugDumpReputationRequest, DebugSetReputationRequest, GetOpsRequest, - GetReputationStatusRequest, GetStakeStatusRequest, RemoveOpsRequest, SubscribeNewHeadsRequest, - SubscribeNewHeadsResponse, UpdateEntitiesRequest, + debug_dump_reputation_response, debug_set_reputation_response, get_op_by_hash_response, + get_ops_response, get_reputation_status_response, get_stake_status_response, + op_pool_client::OpPoolClient, remove_ops_response, update_entities_response, AddOpRequest, + DebugClearStateRequest, DebugDumpMempoolRequest, DebugDumpReputationRequest, + DebugSetReputationRequest, GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, + RemoveOpsRequest, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, }; use crate::{ mempool::{PoolOperation, Reputation, StakeStatus}, @@ -188,6 +188,33 @@ impl PoolServer for RemotePoolClient { } } + async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { + let res = self + .op_pool_client + .clone() + .get_op_by_hash(protos::GetOpByHashRequest { + hash: hash.as_bytes().to_vec(), + }) + .await? + .into_inner() + .result; + + match res { + Some(get_op_by_hash_response::Result::Success(s)) => { + Ok(s.op.map(PoolOperation::try_from).transpose()?) + } + Some(get_op_by_hash_response::Result::Failure(e)) => match e.error { + Some(_) => Err(e.try_into()?), + None => Err(PoolServerError::Other(anyhow::anyhow!( + "should have received error from op pool" + )))?, + }, + None => Err(PoolServerError::Other(anyhow::anyhow!( + "should have received result from op pool" + )))?, + } + } + async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()> { let res = self .op_pool_client diff --git a/crates/pool/src/server/remote/mod.rs b/crates/pool/src/server/remote/mod.rs index d1978c009..718252094 100644 --- a/crates/pool/src/server/remote/mod.rs +++ b/crates/pool/src/server/remote/mod.rs @@ -13,7 +13,7 @@ mod client; mod error; -#[allow(non_snake_case, unreachable_pub)] +#[allow(non_snake_case, unreachable_pub, clippy::large_enum_variant)] mod protos; mod server; diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index 7f68a8a46..083147427 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -234,6 +234,7 @@ impl From<&PoolOperation> for MempoolOp { fn from(op: &PoolOperation) -> Self { MempoolOp { uo: Some(UserOperation::from(&op.uo)), + entry_point: op.entry_point.as_bytes().to_vec(), aggregator: op.aggregator.map_or(vec![], |a| a.as_bytes().to_vec()), valid_after: op.valid_time_range.valid_after.seconds_since_epoch(), valid_until: op.valid_time_range.valid_until.seconds_since_epoch(), @@ -256,6 +257,8 @@ impl TryFrom for PoolOperation { fn try_from(op: MempoolOp) -> Result { let uo = op.uo.context(MISSING_USER_OP_ERR_STR)?.try_into()?; + let entry_point = from_bytes(&op.entry_point)?; + let aggregator: Option
= if op.aggregator.is_empty() { None } else { @@ -278,6 +281,7 @@ impl TryFrom for PoolOperation { Ok(PoolOperation { uo, + entry_point, aggregator, valid_time_range, expected_code_hash, diff --git a/crates/pool/src/server/remote/server.rs b/crates/pool/src/server/remote/server.rs index 4a09c6d78..d93aee8d3 100644 --- a/crates/pool/src/server/remote/server.rs +++ b/crates/pool/src/server/remote/server.rs @@ -31,20 +31,21 @@ use tonic::{transport::Server, Request, Response, Result, Status}; use super::protos::{ add_op_response, debug_clear_state_response, debug_dump_mempool_response, - debug_dump_reputation_response, debug_set_reputation_response, get_ops_response, - get_reputation_status_response, get_stake_status_response, + debug_dump_reputation_response, debug_set_reputation_response, get_op_by_hash_response, + get_ops_response, get_reputation_status_response, get_stake_status_response, op_pool_server::{OpPool, OpPoolServer}, remove_ops_response, update_entities_response, AddOpRequest, AddOpResponse, AddOpSuccess, DebugClearStateRequest, DebugClearStateResponse, DebugClearStateSuccess, DebugDumpMempoolRequest, DebugDumpMempoolResponse, DebugDumpMempoolSuccess, DebugDumpReputationRequest, DebugDumpReputationResponse, DebugDumpReputationSuccess, DebugSetReputationRequest, DebugSetReputationResponse, DebugSetReputationSuccess, - GetOpsRequest, GetOpsResponse, GetOpsSuccess, GetReputationStatusRequest, - GetReputationStatusResponse, GetReputationStatusSuccess, GetStakeStatusRequest, - GetStakeStatusResponse, GetStakeStatusSuccess, GetSupportedEntryPointsRequest, - GetSupportedEntryPointsResponse, MempoolOp, RemoveOpsRequest, RemoveOpsResponse, - RemoveOpsSuccess, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, - UpdateEntitiesResponse, UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET, + GetOpByHashRequest, GetOpByHashResponse, GetOpByHashSuccess, GetOpsRequest, GetOpsResponse, + GetOpsSuccess, GetReputationStatusRequest, GetReputationStatusResponse, + GetReputationStatusSuccess, GetStakeStatusRequest, GetStakeStatusResponse, + GetStakeStatusSuccess, GetSupportedEntryPointsRequest, GetSupportedEntryPointsResponse, + MempoolOp, RemoveOpsRequest, RemoveOpsResponse, RemoveOpsSuccess, SubscribeNewHeadsRequest, + SubscribeNewHeadsResponse, UpdateEntitiesRequest, UpdateEntitiesResponse, + UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET, }; use crate::{ mempool::Reputation, @@ -181,6 +182,33 @@ impl OpPool for OpPoolImpl { Ok(Response::new(resp)) } + async fn get_op_by_hash( + &self, + request: Request, + ) -> Result> { + let req = request.into_inner(); + + if req.hash.len() != 32 { + return Err(Status::invalid_argument("Hash must be 32 bytes long")); + } + let hash = H256::from_slice(&req.hash); + + let resp = match self.local_pool.get_op_by_hash(hash).await { + Ok(op) => GetOpByHashResponse { + result: Some(get_op_by_hash_response::Result::Success( + GetOpByHashSuccess { + op: op.map(|op| MempoolOp::from(&op)), + }, + )), + }, + Err(error) => GetOpByHashResponse { + result: Some(get_op_by_hash_response::Result::Failure(error.into())), + }, + }; + + Ok(Response::new(resp)) + } + async fn remove_ops( &self, request: Request, diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 067d78a90..b3f8a4adb 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -30,3 +30,8 @@ serde.workspace = true strum.workspace = true url.workspace = true futures-util.workspace = true + +[dev-dependencies] +mockall.workspace = true +rundler-provider = { path = "../provider", features = ["test-utils"]} +rundler-pool = { path = "../pool", features = ["test-utils"] } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index fd88a5ff6..f1eca07b0 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -77,14 +77,11 @@ where entry_point: E, estimation_settings: EstimationSettings, fee_estimator: FeeEstimator

, - ) -> Self - where - E: Clone, // Add Clone trait bound for E - { + ) -> Self { let gas_estimator = GasEstimatorImpl::new( chain_id, provider, - entry_point.clone(), + entry_point, estimation_settings, fee_estimator, ); @@ -206,57 +203,27 @@ where )); } - // Get event associated with hash (need to check all entry point addresses associated with this API) - let event = self - .get_user_operation_event_by_hash(hash) - .await - .log_on_error("should have successfully queried for user op events by hash")?; - - let Some(event) = event else { return Ok(None) }; - - // If the event is found, get the TX and entry point - let transaction_hash = event - .transaction_hash - .context("tx_hash should be present")?; - - let tx = self - .provider - .get_transaction(transaction_hash) - .await - .context("should have fetched tx from provider")? - .context("should have found tx")?; - - // We should return null if the tx isn't included in the block yet - if tx.block_hash.is_none() && tx.block_number.is_none() { - return Ok(None); - } - let to = tx - .to - .context("tx.to should be present on transaction containing user operation event")?; - - // Find first op matching the hash - let user_operation = if self.contexts_by_entry_point.contains_key(&to) { - self.get_user_operations_from_tx_data(tx.input) - .into_iter() - .find(|op| op.op_hash(to, self.chain_id) == hash) - .context("matching user operation should be found in tx data")? + // check for the user operation both in the pool and mined on chain + let mined_fut = self.get_mined_user_operation_by_hash(hash); + let pending_fut = self.get_pending_user_operation_by_hash(hash); + let (mined, pending) = tokio::join!(mined_fut, pending_fut); + + // mined takes precedence over pending + if let Ok(Some(mined)) = mined { + Ok(Some(mined)) + } else if let Ok(Some(pending)) = pending { + Ok(Some(pending)) + } else if mined.is_err() || pending.is_err() { + // if either futures errored, and the UO was not found, return the errors + Err(EthRpcError::Internal(anyhow::anyhow!( + "error fetching user operation by hash: mined: {:?}, pending: {:?}", + mined.err().map(|e| e.to_string()).unwrap_or_default(), + pending.err().map(|e| e.to_string()).unwrap_or_default(), + ))) } else { - self.trace_find_user_operation(transaction_hash, hash) - .await - .context("error running trace")? - .context("should have found user operation in trace")? - }; - - Ok(Some(RichUserOperation { - user_operation: user_operation.into(), - entry_point: event.address.into(), - block_number: tx - .block_number - .map(|n| U256::from(n.as_u64())) - .unwrap_or_default(), - block_hash: tx.block_hash.unwrap_or_default(), - transaction_hash, - })) + // not found in either pool or mined + Ok(None) + } } pub(crate) async fn get_user_operation_receipt( @@ -336,6 +303,82 @@ where Ok(self.chain_id.into()) } + async fn get_mined_user_operation_by_hash( + &self, + hash: H256, + ) -> EthResult> { + // Get event associated with hash (need to check all entry point addresses associated with this API) + let event = self + .get_user_operation_event_by_hash(hash) + .await + .log_on_error("should have successfully queried for user op events by hash")?; + + let Some(event) = event else { return Ok(None) }; + + // If the event is found, get the TX and entry point + let transaction_hash = event + .transaction_hash + .context("tx_hash should be present")?; + + let tx = self + .provider + .get_transaction(transaction_hash) + .await + .context("should have fetched tx from provider")? + .context("should have found tx")?; + + // We should return null if the tx isn't included in the block yet + if tx.block_hash.is_none() && tx.block_number.is_none() { + return Ok(None); + } + let to = tx + .to + .context("tx.to should be present on transaction containing user operation event")?; + + // Find first op matching the hash + let user_operation = if self.contexts_by_entry_point.contains_key(&to) { + self.get_user_operations_from_tx_data(tx.input) + .into_iter() + .find(|op| op.op_hash(to, self.chain_id) == hash) + .context("matching user operation should be found in tx data")? + } else { + self.trace_find_user_operation(transaction_hash, hash) + .await + .context("error running trace")? + .context("should have found user operation in trace")? + }; + + Ok(Some(RichUserOperation { + user_operation: user_operation.into(), + entry_point: event.address.into(), + block_number: Some( + tx.block_number + .map(|n| U256::from(n.as_u64())) + .unwrap_or_default(), + ), + block_hash: Some(tx.block_hash.unwrap_or_default()), + transaction_hash: Some(transaction_hash), + })) + } + + async fn get_pending_user_operation_by_hash( + &self, + hash: H256, + ) -> EthResult> { + let res = self + .pool + .get_op_by_hash(hash) + .await + .map_err(EthRpcError::from)?; + Ok(res.map(|op| RichUserOperation { + user_operation: op.uo.into(), + entry_point: op.entry_point.into(), + block_number: None, + block_hash: None, + transaction_hash: None, + })) + } + async fn get_user_operation_event_by_hash(&self, hash: H256) -> EthResult> { let to_block = self.provider.get_block_number().await?; @@ -509,11 +552,15 @@ where #[cfg(test)] mod tests { use ethers::{ - types::{Log, TransactionReceipt}, + abi::AbiEncode, + types::{Log, Transaction, TransactionReceipt}, utils::keccak256, }; - use rundler_pool::MockPoolServer; + use mockall::predicate::eq; + use rundler_pool::{MockPoolServer, PoolOperation}; use rundler_provider::{MockEntryPoint, MockProvider}; + use rundler_sim::PriorityFeeMode; + use rundler_types::contracts::i_entry_point::HandleOpsCall; use super::*; @@ -659,6 +706,126 @@ mod tests { assert!(result.is_err(), "{:?}", result.unwrap()); } + #[tokio::test] + async fn test_get_user_op_by_hash_pending() { + let ep = Address::random(); + let uo = UserOperation::default(); + let hash = uo.op_hash(ep, 1); + + let po = PoolOperation { + uo: uo.clone(), + entry_point: ep, + ..Default::default() + }; + + let mut pool = MockPoolServer::default(); + pool.expect_get_op_by_hash() + .with(eq(hash)) + .times(1) + .returning(move |_| Ok(Some(po.clone()))); + + let mut provider = MockProvider::default(); + provider.expect_get_logs().returning(move |_| Ok(vec![])); + provider.expect_get_block_number().returning(|| Ok(1000)); + + let mut entry_point = MockEntryPoint::default(); + entry_point.expect_address().returning(move || ep); + + let api = create_api(provider, entry_point, pool); + let res = api.get_user_operation_by_hash(hash).await.unwrap(); + let ro = RichUserOperation { + user_operation: uo.into(), + entry_point: ep.into(), + block_number: None, + block_hash: None, + transaction_hash: None, + }; + assert_eq!(res, Some(ro)); + } + + #[tokio::test] + async fn test_get_user_op_by_hash_mined() { + let ep = Address::random(); + let uo = UserOperation::default(); + let hash = uo.op_hash(ep, 1); + let block_number = 1000; + let block_hash = H256::random(); + + let mut pool = MockPoolServer::default(); + pool.expect_get_op_by_hash() + .with(eq(hash)) + .returning(move |_| Ok(None)); + + let mut provider = MockProvider::default(); + provider.expect_get_block_number().returning(|| Ok(1000)); + + let tx_data: Bytes = IEntryPointCalls::HandleOps(HandleOpsCall { + beneficiary: Address::zero(), + ops: vec![uo.clone()], + }) + .encode() + .into(); + let tx = Transaction { + to: Some(ep), + input: tx_data, + block_number: Some(block_number.into()), + block_hash: Some(block_hash), + ..Default::default() + }; + let tx_hash = tx.hash(); + let log = Log { + address: ep, + transaction_hash: Some(tx_hash), + ..Default::default() + }; + + provider + .expect_get_logs() + .returning(move |_| Ok(vec![log.clone()])); + provider + .expect_get_transaction() + .with(eq(tx_hash)) + .returning(move |_| Ok(Some(tx.clone()))); + + let mut entry_point = MockEntryPoint::default(); + entry_point.expect_address().returning(move || ep); + + let api = create_api(provider, entry_point, pool); + let res = api.get_user_operation_by_hash(hash).await.unwrap(); + let ro = RichUserOperation { + user_operation: uo.into(), + entry_point: ep.into(), + block_number: Some(block_number.into()), + block_hash: Some(block_hash), + transaction_hash: Some(tx_hash), + }; + assert_eq!(res, Some(ro)); + } + + #[tokio::test] + async fn test_get_user_op_by_hash_not_found() { + let ep = Address::random(); + let uo = UserOperation::default(); + let hash = uo.op_hash(ep, 1); + + let mut pool = MockPoolServer::default(); + pool.expect_get_op_by_hash() + .with(eq(hash)) + .times(1) + .returning(move |_| Ok(None)); + + let mut provider = MockProvider::default(); + provider.expect_get_logs().returning(move |_| Ok(vec![])); + provider.expect_get_block_number().returning(|| Ok(1000)); + + let mut entry_point = MockEntryPoint::default(); + entry_point.expect_address().returning(move || ep); + + let api = create_api(provider, entry_point, pool); + let res = api.get_user_operation_by_hash(hash).await.unwrap(); + assert_eq!(res, None); + } + fn given_log(topic_0: &str, topic_1: &str) -> Log { Log { topics: vec![ @@ -675,4 +842,39 @@ mod tests { ..Default::default() } } + + fn create_api( + provider: MockProvider, + ep: MockEntryPoint, + pool: MockPoolServer, + ) -> EthApi { + let mut contexts_by_entry_point = HashMap::new(); + let provider = Arc::new(provider); + contexts_by_entry_point.insert( + ep.address(), + EntryPointContext::new( + 1, + Arc::clone(&provider), + ep, + EstimationSettings { + max_verification_gas: 1_000_000, + max_call_gas: 1_000_000, + max_simulate_handle_ops_gas: 1_000_000, + }, + FeeEstimator::new( + Arc::clone(&provider), + 1, + PriorityFeeMode::BaseFeePercent(0), + 0, + ), + ), + ); + EthApi { + contexts_by_entry_point, + provider, + chain_id: 1, + pool, + settings: Settings::new(None), + } + } } diff --git a/crates/rpc/src/types.rs b/crates/rpc/src/types.rs index fbbcfa467..28b2a7c90 100644 --- a/crates/rpc/src/types.rs +++ b/crates/rpc/src/types.rs @@ -79,7 +79,7 @@ pub struct RpcStakeInfo { } /// User operation definition for RPC -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RpcUserOperation { sender: RpcAddress, @@ -132,7 +132,7 @@ impl From for UserOperation { } /// User operation with additional metadata -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RichUserOperation { /// The full user operation @@ -140,11 +140,11 @@ pub struct RichUserOperation { /// The entry point address this operation was sent to pub entry_point: RpcAddress, /// The number of the block this operation was included in - pub block_number: U256, + pub block_number: Option, /// The hash of the block this operation was included in - pub block_hash: H256, + pub block_hash: Option, /// The hash of the transaction this operation was included in - pub transaction_hash: H256, + pub transaction_hash: Option, } /// User operation receipt