Skip to content
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
47 changes: 38 additions & 9 deletions src/ethproto/aa_bundler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from eth_abi.packed import encode_packed
from eth_account import Account
from eth_account.messages import encode_defunct
from eth_typing import HexAddress
from eth_typing import ChecksumAddress, HexAddress
from eth_utils import add_0x_prefix, function_signature_to_4byte_selector
from hexbytes import HexBytes
from requests import HTTPError
Expand Down Expand Up @@ -50,6 +50,7 @@
AA_BUNDLER_NONCE_KEY = env.int("AA_BUNDLER_NONCE_KEY", 0)
AA_BUNDLER_MAX_GETNONCE_RETRIES = env.int("AA_BUNDLER_MAX_GETNONCE_RETRIES", 3)
AA_BUNDLER_ALCHEMY_GAS_POLICY_ID = env.str("AA_BUNDLER_ALCHEMY_GAS_POLICY_ID", None)
AA_BUNDLER_USE_EXECUTE_USER_OP = env.bool("AA_BUNDLER_USE_EXECUTE_USER_OP", False)

GET_NONCE_ABI = [
{
Expand Down Expand Up @@ -148,6 +149,12 @@ class UserOperation:
EXECUTE_ARG_TYPES = ["address", "uint256", "bytes"]
EXECUTE_SELECTOR = function_signature_to_4byte_selector(f"execute({','.join(EXECUTE_ARG_TYPES)})")

EXECUTE_USEROP_ARG_TYPES = ["address", "address", "uint256", "bytes"]
EXECUTE_USEROP_SELECTOR = function_signature_to_4byte_selector(
# PackedUserOperation struct
"executeUserOp((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes),bytes32)"
)

sender: HexBytes
nonce: int
call_data: HexBytes
Expand All @@ -169,13 +176,24 @@ class UserOperation:
paymaster_post_op_gas_limit: int = 0

@classmethod
def from_tx(cls, tx: Tx, nonce):
def from_tx(cls, tx: Tx, nonce, execute_user_op_context=None):
if execute_user_op_context is not None:
call_data = add_0x_prefix(
(
cls.EXECUTE_USEROP_SELECTOR
+ encode(
cls.EXECUTE_USEROP_ARG_TYPES, [execute_user_op_context, tx.target, tx.value, tx.data]
)
).hex()
)
else:
call_data = add_0x_prefix(
(cls.EXECUTE_SELECTOR + encode(cls.EXECUTE_ARG_TYPES, tx.as_execute_args())).hex()
)
return cls(
sender=get_sender(tx),
nonce=nonce,
call_data=add_0x_prefix(
(cls.EXECUTE_SELECTOR + encode(cls.EXECUTE_ARG_TYPES, tx.as_execute_args())).hex()
),
call_data=call_data,
)

def as_reduced_dict(self):
Expand Down Expand Up @@ -386,6 +404,7 @@ def __init__(
executor_pk: HexBytes = AA_BUNDLER_EXECUTOR_PK,
overrides: StateOverride = AA_BUNDLER_STATE_OVERRIDES,
alchemy_gas_policy_id: str = AA_BUNDLER_ALCHEMY_GAS_POLICY_ID,
use_execute_user_op: bool = AA_BUNDLER_USE_EXECUTE_USER_OP,
):
self.w3 = w3
self.bundler = Web3(Web3.HTTPProvider(bundler_url), middleware=[]) if bundler_url else w3
Expand All @@ -397,7 +416,7 @@ def __init__(
self.gas_limit_factor = gas_limit_factor
self.priority_gas_price_factor = priority_gas_price_factor
self.base_gas_price_factor = base_gas_price_factor
self.executor_pk = executor_pk
self.account = Account.from_key(executor_pk) if executor_pk else None
self.max_fee_per_gas = max_fee_per_gas

# stateOverrideSet mapping to use when calling eth_estimateUserOperationGas
Expand All @@ -407,15 +426,23 @@ def __init__(
if alchemy_gas_policy_id is None and bundler_type == "alchemy":
raise BundlerError("Must provide alchemy_gas_policy_id when using alchemy bundler_type")
self.alchemy_gas_policy_id = alchemy_gas_policy_id
self.use_execute_user_op = use_execute_user_op

def __str__(self):
return (
f"Bundler(type={self.bundler_type}, entrypoint={self.entrypoint}, nonce_mode={self.nonce_mode}, "
f"fixed_nonce_key={self.fixed_nonce_key}, verification_gas_factor={self.verification_gas_factor}, "
f"gas_limit_factor={self.gas_limit_factor}, priority_gas_price_factor={self.priority_gas_price_factor}, "
f"base_gas_price_factor={self.base_gas_price_factor}, max_fee_per_gas={self.max_fee_per_gas})"
f"base_gas_price_factor={self.base_gas_price_factor}, max_fee_per_gas={self.max_fee_per_gas}), "
f"use_execute_user_op={self.use_execute_user_op}, signer={self.account.address if self.account else None}"
)

@property
def execute_user_op_context(self) -> ChecksumAddress:
if self.use_execute_user_op and self.account:
return self.account.address
return None

def get_nonce_and_key(self, tx: Tx, fetch=False):
nonce_key = tx.nonce_key
nonce = tx.nonce
Expand Down Expand Up @@ -567,7 +594,9 @@ def build_user_operation(self, tx: Tx, retry_nonce=None) -> UserOperation:
# Consume the nonce, even if the userop may fail later
consume_nonce(nonce_key, nonce)

user_operation = UserOperation.from_tx(tx, make_nonce(nonce_key, nonce))
user_operation = UserOperation.from_tx(
tx, make_nonce(nonce_key, nonce), execute_user_op_context=self.execute_user_op_context
)

if self.bundler_type == "alchemy":
estimation_and_paymaster = self.alchemy_estimation(user_operation)
Expand Down Expand Up @@ -603,7 +632,7 @@ def build_user_operation(self, tx: Tx, retry_nonce=None) -> UserOperation:

def send_transaction(self, tx: Tx, retry_nonce=None):
user_operation = self.build_user_operation(tx, retry_nonce).sign(
self.executor_pk, tx.chain_id, self.entrypoint
self.account.key, tx.chain_id, self.entrypoint
)

resp = self.bundler.provider.make_request(
Expand Down
12 changes: 9 additions & 3 deletions src/ethproto/test_utils/vcr_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ def json_rpc_matcher(r1, r2):
r1_body = json.loads(r1.body)
r2_body = json.loads(r2.body)

assert r1_body["jsonrpc"] == r2_body["jsonrpc"] == "2.0"
assert r1_body["method"] == r2_body["method"]
assert r1_body["params"] == r2_body["params"]
assert (
r1_body["jsonrpc"] == r2_body["jsonrpc"] == "2.0"
), f"JSON-RPC mismatch: {r1_body['jsonrpc']} != {r2_body['jsonrpc']}"
assert (
r1_body["method"] == r2_body["method"]
), f"Method mismatch: {r1_body['method']} != {r2_body['method']}"
assert (
r1_body["params"] == r2_body["params"]
), f"Param mismatch: {r1_body['params']} != {r2_body['params']}"
92 changes: 46 additions & 46 deletions tests/cassettes/test_aa_bundler/test_build_user_operation.yaml
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
interactions:
- request:
body: '{"jsonrpc": "2.0", "method": "alchemy_requestGasAndPaymasterAndData", "params":
[{"policyId": "d80ed67a-d8bc-4cd1-90ad-b50e0a58c93e", "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"dummySignature": "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c",
"userOperation": {"sender": "0xE8B412158c205B0F605e0FC09dCdA27d3F140FE9", "nonce":
"0xae85c374ae0606ed34d0ee009a9ca43a757a8a46a324510000000000000000", "callData":
"0xb61d27f60000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000007ace242f32208d836a2245df957c08547059bf45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000",
"signature": "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"},
"overrides": {"maxFeePerGas": {"multiplier": 1}, "maxPriorityFeePerGas": {"multiplier":
1}, "callGasLimit": {"multiplier": 1}, "verificationGasLimit": {"multiplier":
1}}, "stateOverrideSet": {}}], "id": 0}'
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '1328'
Content-Type:
- application/json
User-Agent:
- web3.py/7.14.0/web3.providers.rpc.rpc.HTTPProvider
method: POST
uri: https://polygon-mainnet.g.alchemy.com
response:
body:
string: '{"jsonrpc":"2.0","id":0,"result":{"callGasLimit":"0xd912","paymasterVerificationGasLimit":"0x9b37","paymasterPostOpGasLimit":"0x0","verificationGasLimit":"0xac76","maxPriorityFeePerGas":"0x7aef40a00","paymaster":"0x2cc0c7981D846b9F2a16276556f6e8cb52BfB633","maxFeePerGas":"0xbaad142eb6","paymasterData":"0x00000000000000006982f2961b02e25cb873537d0a2383b9665992667628a8d22f8223e7951b2f6d2aeae55e03af6a6ec9774cf4303980e7e3b2ecab9b65a8bcce8aabe4fd89b81c0facdb4f1b","preVerificationGas":"0xbd54"}}'
headers:
content-length:
- '493'
content-type:
- application/json
date:
- Wed, 04 Feb 2026 07:07:42 GMT
server:
- istio-envoy
x-alchemy-trace-id:
- c905c727e8b9bd9f1495b4112264ea51
- 2a016524-94c2-4e82-bdb5-7a4c7035b07e
x-envoy-upstream-service-time:
- '122'
status:
code: 200
message: OK
- request:
body: '{"jsonrpc": "2.0", "method": "alchemy_requestGasAndPaymasterAndData", "params":
[{"policyId": "d80ed67a-d8bc-4cd1-90ad-b50e0a58c93e", "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"dummySignature": "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c",
"userOperation": {"sender": "0xE8B412158c205B0F605e0FC09dCdA27d3F140FE9", "nonce":
"0xae85c374ae0606ed34d0ee009a9ca43a757a8a46a324510000000000000000", "callData":
"0xb61d27f60000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000007ace242f32208d836a2245df957c08547059bf45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000",
"signature": "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"},
"overrides": {"maxFeePerGas": {"multiplier": 1}, "maxPriorityFeePerGas": {"multiplier":
1}, "callGasLimit": {"multiplier": 1}, "verificationGasLimit": {"multiplier":
1}}, "stateOverrideSet": {}}], "id": 0}'
headers:
Accept:
- "*/*"
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- "1328"
Content-Type:
- application/json
User-Agent:
- web3.py/7.14.0/web3.providers.rpc.rpc.HTTPProvider
method: POST
uri: https://bundler.example.com
response:
body:
string: '{"jsonrpc":"2.0","id":0,"result":{"callGasLimit":"0xd912","paymasterVerificationGasLimit":"0x9b37","paymasterPostOpGasLimit":"0x0","verificationGasLimit":"0xac76","maxPriorityFeePerGas":"0x7aef40a00","paymaster":"0x2cc0c7981D846b9F2a16276556f6e8cb52BfB633","maxFeePerGas":"0xbaad142eb6","paymasterData":"0x00000000000000006982f2961b02e25cb873537d0a2383b9665992667628a8d22f8223e7951b2f6d2aeae55e03af6a6ec9774cf4303980e7e3b2ecab9b65a8bcce8aabe4fd89b81c0facdb4f1b","preVerificationGas":"0xbd54"}}'
headers:
content-length:
- "493"
content-type:
- application/json
date:
- Wed, 04 Feb 2026 07:07:42 GMT
server:
- istio-envoy
x-alchemy-trace-id:
- c905c727e8b9bd9f1495b4112264ea51
- 2a016524-94c2-4e82-bdb5-7a4c7035b07e
x-envoy-upstream-service-time:
- "122"
status:
code: 200
message: OK
version: 1
Loading
Loading