Skip to content

Commit

Permalink
feat: add erc4337 endpoint methods to provider (#1176)
Browse files Browse the repository at this point in the history
* feat: add eip4337 eth_sendUserOperation method to provider

* chore: serde rename all eip4337 operation

* chore: use actual entry address for eth eip4337

* chore: nightly fmt

* add feature

* fix: user op elements in test

* chore: rename eip to erc

* chore: rename eip4337 files to erc4337

* chore: resolve pr comment

* fix: address parse

* feat: add new endpoints

* feat: add gas estimate endpoint

* chore: add err code

* chore: use packed User Operation

* chore: update tests

* feat: combine user op for both old and new entry points

* fix: clippy and docs ci

* fix: docs

* fix: geth tests

* fix: rm tests

---------

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
royvardhan and mattsse committed Aug 26, 2024
1 parent ff4e72e commit 155914b
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 45 deletions.
1 change: 1 addition & 0 deletions crates/provider/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ anvil-node = [
"dep:alloy-signer-local",
]
debug-api = ["dep:alloy-rpc-types-trace"]
erc4337-api = []
engine-api = ["dep:alloy-rpc-types-engine"]
net-api = []
trace-api = ["dep:alloy-rpc-types-trace"]
Expand Down
96 changes: 96 additions & 0 deletions crates/provider/src/ext/erc4337.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::Provider;
use alloy_network::Network;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::erc4337::{
SendUserOperation, SendUserOperationResponse, UserOperationGasEstimation, UserOperationReceipt,
};
use alloy_transport::{Transport, TransportResult};

/// ERC-4337 Account Abstraction API
///
/// This module provides support for the `eth_sendUserOperation` RPC method
/// as defined in ERC-4337.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
pub trait Erc4337Api<N, T>: Send + Sync {
/// Sends a user operation to the bundler, as defined in ERC-4337.
///
/// Entry point changes based on the user operation type.
async fn send_user_operation(
&self,
user_op: SendUserOperation,
entry_point: Address,
) -> TransportResult<SendUserOperationResponse>;

/// Returns the list of supported entry points.
async fn supported_entry_points(&self) -> TransportResult<Vec<Address>>;

/// Returns the receipt for any user operation.
///
/// Hash is the same returned by any user operation.
async fn get_user_operation_receipt(
&self,
user_op_hash: Bytes,
) -> TransportResult<UserOperationReceipt>;

/// Estimates the gas for a user operation.
///
/// Entry point changes based on the user operation type.
async fn estimate_user_operation_gas(
&self,
user_op: SendUserOperation,
entry_point: Address,
) -> TransportResult<UserOperationGasEstimation>;
}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl<N, T, P> Erc4337Api<N, T> for P
where
N: Network,
T: Transport + Clone,
P: Provider<T, N>,
{
async fn send_user_operation(
&self,
user_op: SendUserOperation,
entry_point: Address,
) -> TransportResult<SendUserOperationResponse> {
match user_op {
SendUserOperation::EntryPointV06(user_op) => {
self.client().request("eth_sendUserOperation", (user_op, entry_point)).await
}
SendUserOperation::EntryPointV07(packed_user_op) => {
self.client().request("eth_sendUserOperation", (packed_user_op, entry_point)).await
}
}
}

async fn supported_entry_points(&self) -> TransportResult<Vec<Address>> {
self.client().request("eth_supportedEntryPoints", ()).await
}

async fn get_user_operation_receipt(
&self,
user_op_hash: Bytes,
) -> TransportResult<UserOperationReceipt> {
self.client().request("eth_getUserOperationReceipt", (user_op_hash,)).await
}

async fn estimate_user_operation_gas(
&self,
user_op: SendUserOperation,
entry_point: Address,
) -> TransportResult<UserOperationGasEstimation> {
match user_op {
SendUserOperation::EntryPointV06(user_op) => {
self.client().request("eth_estimateUserOperationGas", (user_op, entry_point)).await
}
SendUserOperation::EntryPointV07(packed_user_op) => {
self.client()
.request("eth_estimateUserOperationGas", (packed_user_op, entry_point))
.await
}
}
}
}
5 changes: 5 additions & 0 deletions crates/provider/src/ext/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ pub use rpc::RpcApi;
mod txpool;
#[cfg(feature = "txpool-api")]
pub use txpool::TxPoolApi;

#[cfg(feature = "erc4337-api")]
mod erc4337;
#[cfg(feature = "erc4337-api")]
pub use erc4337::Erc4337Api;
6 changes: 3 additions & 3 deletions crates/rpc-types-eth/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Eth_simulateV1 Request / Response types ([#1042](https://github.com/alloy-rs/alloy/issues/1042))
- Feat(rpc-type-eth) convert vec TxReq to bundle ([#1091](https://github.com/alloy-rs/alloy/issues/1091))
- Feat(provider) : introduction to eth_sendRawTransactionConditional RPC endpoint type ([#1009](https://github.com/alloy-rs/alloy/issues/1009))
- Feat(provider) : introduction to eth_sendRawTransactionConditional RPC endpoint type ([#1009](https://github.com/alloy-rs/alloy/issues/1009))
- [rpc-types-eth] Serde flatten `BlobTransactionSidecar` in tx req ([#1054](https://github.com/alloy-rs/alloy/issues/1054))
- Add authorization list to rpc transaction and tx receipt types ([#1051](https://github.com/alloy-rs/alloy/issues/1051))

Expand All @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Refactor

- Add network-primitives ([#1101](https://github.com/alloy-rs/alloy/issues/1101))
- Replace `U64` with `u64` ([#1057](https://github.com/alloy-rs/alloy/issues/1057))
- Replace `U64` with `u64` ([#1057](https://github.com/alloy-rs/alloy/issues/1057))

### Styling

Expand Down Expand Up @@ -112,7 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Miscellaneous Tasks

- Rm unused txtype mod ([#879](https://github.com/alloy-rs/alloy/issues/879))
- [other] Use type aliases where possible to improve clarity ([#859](https://github.com/alloy-rs/alloy/issues/859))
- [other] Use type aliases where possible to improve clarity ([#859](https://github.com/alloy-rs/alloy/issues/859))
- [docs] Crate completeness and fix typos ([#861](https://github.com/alloy-rs/alloy/issues/861))

### Other
Expand Down
41 changes: 0 additions & 41 deletions crates/rpc-types-eth/src/eip4337.rs

This file was deleted.

171 changes: 171 additions & 0 deletions crates/rpc-types-eth/src/erc4337.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use crate::{Log, TransactionReceipt};
use alloy_primitives::{Address, BlockNumber, Bytes, B256, U256};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Options for conditional raw transaction submissions.
// reference for the implementation <https://notes.ethereum.org/@yoav/SkaX2lS9j#>
// See also <https://pkg.go.dev/github.com/aK0nshin/go-ethereum/arbitrum_types#ConditionalOptions>
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct ConditionalOptions {
/// A map of account addresses to their expected storage states.
/// Each account can have a specified storage root or explicit slot-value pairs.
#[serde(default)]
pub known_accounts: HashMap<Address, AccountStorage>,
/// The minimal block number at which the transaction can be included.
/// `None` indicates no minimum block number constraint.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_number_min: Option<BlockNumber>,
/// The maximal block number at which the transaction can be included.
/// `None` indicates no maximum block number constraint.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_number_max: Option<BlockNumber>,
/// The minimal timestamp at which the transaction can be included.
/// `None` indicates no minimum timestamp constraint.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp_min: Option<u64>,
/// The maximal timestamp at which the transaction can be included.
/// `None` indicates no maximum timestamp constraint.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp_max: Option<u64>,
}

/// Represents the expected state of an account for a transaction to be conditionally accepted.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum AccountStorage {
/// Expected storage root hash of the account.
RootHash(B256),
/// Explicit storage slots and their expected values.
Slots(HashMap<U256, B256>),
}

/// [`UserOperation`] in the spec: Entry Point V0.6
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserOperation {
/// The address of the smart contract account
pub sender: Address,
/// Anti-replay protection; also used as the salt for first-time account creation
pub nonce: U256,
/// Code used to deploy the account if not yet on-chain
pub init_code: Bytes,
/// Data that's passed to the sender for execution
pub call_data: Bytes,
/// Gas limit for execution phase
pub call_gas_limit: U256,
/// Gas limit for verification phase
pub verification_gas_limit: U256,
/// Gas to compensate the bundler
pub pre_verification_gas: U256,
/// Maximum fee per gas
pub max_fee_per_gas: U256,
/// Maximum priority fee per gas
pub max_priority_fee_per_gas: U256,
/// Paymaster Contract address and any extra data required for verification and execution
/// (empty for self-sponsored transaction)
pub paymaster_and_data: Bytes,
/// Used to validate a UserOperation along with the nonce during verification
pub signature: Bytes,
}

/// [`PackedUserOperation`] in the spec: Entry Point V0.7
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackedUserOperation {
/// The account making the operation.
pub sender: Address,
/// Prevents message replay attacks and serves as a randomizing element for initial user
/// registration.
pub nonce: U256,
/// Deployer contract address: Required exclusively for deploying new accounts that don't yet
/// exist on the blockchain.
pub factory: Address,
/// Factory data for the account creation process, applicable only when using a deployer
/// contract.
pub factory_data: Bytes,
/// The call data.
pub call_data: Bytes,
/// The gas limit for the call.
pub call_gas_limit: U256,
/// The gas limit for the verification.
pub verification_gas_limit: U256,
/// Prepaid gas fee: Covers the bundler's costs for initial transaction validation and data
/// transmission.
pub pre_verification_gas: U256,
/// The maximum fee per gas.
pub max_fee_per_gas: U256,
/// The maximum priority fee per gas.
pub max_priority_fee_per_gas: U256,
/// Paymaster contract address: Needed if a third party is covering transaction costs; left
/// blank for self-funded accounts.
pub paymaster: Address,
/// The gas limit for the paymaster verification.
pub paymaster_verification_gas_limit: U256,
/// The gas limit for the paymaster post-operation.
pub paymaster_post_op_gas_limit: U256,
/// The paymaster data.
pub paymaster_data: Bytes,
/// The signature of the transaction.
pub signature: Bytes,
}

/// Send User Operation
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SendUserOperation {
/// User Operation
EntryPointV06(UserOperation),
/// Packed User Operation
EntryPointV07(PackedUserOperation),
}

/// Response to sending a user operation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendUserOperationResponse {
/// The hash of the user operation.
pub user_op_hash: Bytes,
}

/// Represents the receipt of a user operation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserOperationReceipt {
/// The hash of the user operation.
pub user_op_hash: Bytes,
/// The entry point address for the user operation.
pub entry_point: Address,
/// The address of the sender of the user operation.
pub sender: Address,
/// The nonce of the user operation.
pub nonce: U256,
/// The address of the paymaster, if any.
pub paymaster: Address,
/// The actual gas cost incurred by the user operation.
pub actual_gas_cost: U256,
/// The actual gas used by the user operation.
pub actual_gas_used: U256,
/// Indicates whether the user operation was successful.
pub success: bool,
/// The reason for failure, if any.
pub reason: Bytes,
/// The logs generated by the user operation.
pub logs: Vec<Log>,
/// The transaction receipt of the user operation.
pub receipt: TransactionReceipt,
}

/// Represents the gas estimation for a user operation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserOperationGasEstimation {
/// The gas limit for the pre-verification.
pub pre_verification_gas: U256,
/// The gas limit for the verification.
pub verification_gas: U256,
/// The gas limit for the paymaster verification.
pub paymaster_verification_gas: U256,
/// The gas limit for the call.
pub call_gas_limit: U256,
}
6 changes: 5 additions & 1 deletion crates/rpc-types-eth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ mod work;
pub use work::Work;

/// This module provides implementations for EIP-4337.
pub mod eip4337;
pub mod erc4337;
pub use erc4337::{
PackedUserOperation, SendUserOperation, SendUserOperationResponse, UserOperation,
UserOperationGasEstimation, UserOperationReceipt,
};

pub mod simulate;

0 comments on commit 155914b

Please sign in to comment.