Skip to content
Closed
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
3 changes: 3 additions & 0 deletions crates/account-abstraction/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt
# alloy
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-provider.workspace = true
op-alloy-network.workspace = true

# rpc
jsonrpsee.workspace = true
Expand All @@ -35,6 +37,7 @@ jsonrpsee.workspace = true
tracing.workspace = true
serde.workspace = true
eyre.workspace = true
url.workspace = true

[dev-dependencies]
serde_json.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/account-abstraction/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod rpc;
mod tips_client;

pub use rpc::{
AccountAbstractionApiImpl, AccountAbstractionApiServer, BaseAccountAbstractionApiImpl,
Expand Down
36 changes: 27 additions & 9 deletions crates/account-abstraction/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ use reth_optimism_chainspec::OpChainSpec;
use reth_provider::{ChainSpecProvider, StateProviderFactory};
use serde::{Deserialize, Serialize};
use tracing::info;
use url::Url;

use crate::tips_client::TipsClient;

/// User Operation as defined by EIP-4337 v0.6
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -192,15 +195,23 @@ pub trait BaseAccountAbstractionApi {
/// Implementation of the account abstraction RPC API
pub struct AccountAbstractionApiImpl<Provider> {
provider: Provider,
tips_client: TipsClient,
}

impl<Provider> AccountAbstractionApiImpl<Provider>
where
Provider: StateProviderFactory + ChainSpecProvider<ChainSpec = OpChainSpec> + Clone,
{
/// Creates a new instance of AccountAbstractionApi
pub fn new(provider: Provider) -> Self {
Self { provider }
///
/// # Arguments
/// * `provider` - The state provider for blockchain access
/// * `tips_url` - URL of the Tips ingress service
pub fn new(provider: Provider, tips_url: Url) -> Self {
Self {
provider,
tips_client: TipsClient::new(tips_url),
}
}
}

Expand All @@ -226,23 +237,30 @@ where
entry_point = %entry_point,
"Received sendUserOperation request (v0.6)"
);
// TODO: Validate v0.6 user operation
// TODO: Submit to bundler pool
}
UserOperation::V07(op) => {
info!(
sender = %op.sender,
entry_point = %entry_point,
"Received sendUserOperation request (v0.7+)"
);
// TODO: Validate v0.7 user operation
// TODO: Convert to PackedUserOperation for on-chain submission
// TODO: Submit to bundler pool
}
}

// TODO: Return user operation hash
Ok(B256::default())
// Send to Tips ingress service
let user_op_hash = self
.tips_client
.send_user_operation(user_operation, entry_point)
.await
.map_err(|e| {
jsonrpsee::types::ErrorObjectOwned::owned(
jsonrpsee::types::error::INTERNAL_ERROR_CODE,
format!("Failed to send user operation to Tips service: {}", e),
None::<String>,
)
})?;

Ok(user_op_hash)
}

async fn estimate_user_operation_gas(
Expand Down
137 changes: 137 additions & 0 deletions crates/account-abstraction/src/tips_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Tips Client for communicating with the Tips Ingress Service
//!
//! This module provides a lightweight client for sending user operations
//! to the Tips ingress service via JSON-RPC.

use alloy_primitives::{Address, B256};
use alloy_provider::{Provider, ProviderBuilder, RootProvider};
use eyre::Result;
use op_alloy_network::Optimism;
use url::Url;

use crate::UserOperation;

/// Client for communicating with the Tips Ingress Service
#[derive(Debug, Clone)]
pub struct TipsClient {
provider: RootProvider<Optimism>,
}

impl TipsClient {
/// Creates a new Tips client connected to the specified URL
///
/// # Arguments
/// * `tips_url` - The URL of the Tips ingress service
///
/// # Example
/// ```no_run
/// use base_reth_account_abstraction::TipsClient;
/// use url::Url;
///
/// let url = Url::parse("http://localhost:8080").unwrap();
/// let client = TipsClient::new(url);
/// ```
pub fn new(tips_url: Url) -> Self {
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.network::<Optimism>()
.connect_http(tips_url);

Self { provider }
}

/// Sends a user operation to the Tips ingress service
///
/// # Arguments
/// * `user_operation` - The user operation to send (supports both v0.6 and v0.7+)
/// * `entry_point` - The entry point contract address
///
/// # Returns
/// The user operation hash if successful
///
/// # Example
/// ```no_run
/// use base_reth_account_abstraction::{TipsClient, UserOperation, UserOperationV06};
/// use alloy_primitives::{Address, Bytes, U256};
/// use url::Url;
///
/// # async fn example() -> eyre::Result<()> {
/// let url = Url::parse("http://localhost:8080")?;
/// let client = TipsClient::new(url);
///
/// let user_op = UserOperation::V06(UserOperationV06 {
/// sender: Address::ZERO,
/// nonce: U256::from(0),
/// init_code: Bytes::new(),
/// call_data: Bytes::new(),
/// call_gas_limit: U256::from(100000),
/// verification_gas_limit: U256::from(100000),
/// pre_verification_gas: U256::from(21000),
/// max_fee_per_gas: U256::from(1000000000),
/// max_priority_fee_per_gas: U256::from(1000000000),
/// paymaster_and_data: Bytes::new(),
/// signature: Bytes::new(),
/// });
///
/// let entry_point = Address::ZERO;
/// let user_op_hash = client.send_user_operation(user_op, entry_point).await?;
/// # Ok(())
/// # }
/// ```
pub async fn send_user_operation(
&self,
user_operation: UserOperation,
entry_point: Address,
) -> Result<B256> {
let user_op_hash = self
.provider
.client()
.request("eth_sendUserOperation", (user_operation, entry_point))
.await?;

Ok(user_op_hash)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::UserOperationV06;
use alloy_primitives::{Bytes, U256};

#[test]
fn test_client_creation() {
let url = Url::parse("http://localhost:8080").unwrap();
let _client = TipsClient::new(url);
}

// Integration test - requires running tips service
#[tokio::test]
#[ignore]
async fn test_send_user_operation() -> Result<()> {
Comment on lines +108 to +111
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I wouldn't check in tests that don't run as part of the CI

let url = Url::parse("http://localhost:8080")?;
let client = TipsClient::new(url);

let user_op = UserOperation::V06(UserOperationV06 {
sender: Address::ZERO,
nonce: U256::from(0),
init_code: Bytes::new(),
call_data: Bytes::new(),
call_gas_limit: U256::from(100000),
verification_gas_limit: U256::from(100000),
pre_verification_gas: U256::from(21000),
max_fee_per_gas: U256::from(1000000000),
max_priority_fee_per_gas: U256::from(1000000000),
paymaster_and_data: Bytes::new(),
signature: Bytes::new(),
});

let entry_point = Address::ZERO;
let user_op_hash = client.send_user_operation(user_op, entry_point).await?;

assert_ne!(user_op_hash, B256::ZERO);

Ok(())
}
}