Skip to content

Commit

Permalink
4337: Support 0.7 Entrypoint (#948)
Browse files Browse the repository at this point in the history
- Closes #785
  • Loading branch information
Uxio0 authored May 7, 2024
1 parent 5a9a1b4 commit 25e3371
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 27 deletions.
170 changes: 149 additions & 21 deletions gnosis/eth/account_abstraction/user_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class UserOperation:
"""
EIP4337 UserOperation for Entrypoint v0.6
https://github.com/eth-infinitism/account-abstraction/blob/v0.6.0
https://github.com/eth-infinitism/account-abstraction/blob/v0.6.0/contracts/interfaces/UserOperation.sol
"""

user_operation_hash: bytes
Expand All @@ -44,27 +44,65 @@ def from_bundler_response(
cls,
user_operation_hash: Union[HexStr, bytes],
user_operation_response: Dict[str, Any],
) -> "UserOperation":
return cls(
HexBytes(user_operation_hash),
ChecksumAddress(user_operation_response["userOperation"]["sender"]),
int(user_operation_response["userOperation"]["nonce"], 16),
HexBytes(user_operation_response["userOperation"]["initCode"]),
HexBytes(user_operation_response["userOperation"]["callData"]),
int(user_operation_response["userOperation"]["callGasLimit"], 16),
int(user_operation_response["userOperation"]["verificationGasLimit"], 16),
int(user_operation_response["userOperation"]["preVerificationGas"], 16),
int(user_operation_response["userOperation"]["maxFeePerGas"], 16),
int(user_operation_response["userOperation"]["maxPriorityFeePerGas"], 16),
HexBytes(user_operation_response["userOperation"]["paymasterAndData"]),
HexBytes(user_operation_response["userOperation"]["signature"]),
ChecksumAddress(user_operation_response["entryPoint"]),
metadata=UserOperationMetadata(
HexBytes(user_operation_response["transactionHash"]),
HexBytes(user_operation_response["blockHash"]),
int(user_operation_response["blockNumber"], 16),
),
) -> Union["UserOperation", "UserOperationV07"]:
user_operation = user_operation_response["userOperation"]
metadata = UserOperationMetadata(
HexBytes(user_operation_response["transactionHash"]),
HexBytes(user_operation_response["blockHash"]),
int(user_operation_response["blockNumber"], 16),
)
if "initCode" in user_operation:
return cls(
HexBytes(user_operation_hash),
ChecksumAddress(user_operation["sender"]),
int(user_operation["nonce"], 16),
HexBytes(user_operation["initCode"]),
HexBytes(user_operation["callData"]),
int(user_operation["callGasLimit"], 16),
int(user_operation["verificationGasLimit"], 16),
int(user_operation["preVerificationGas"], 16),
int(user_operation["maxFeePerGas"], 16),
int(user_operation["maxPriorityFeePerGas"], 16),
HexBytes(user_operation["paymasterAndData"]),
HexBytes(user_operation["signature"]),
ChecksumAddress(user_operation_response["entryPoint"]),
metadata=metadata,
)
else:
if paymaster := user_operation.get("paymaster"):
# Paymaster parameters are optional
paymaster_verification_gas_limit = int(
user_operation["paymasterVerificationGasLimit"], 16
)
paymaster_post_op_gas_limit = int(
user_operation["paymasterPostOpGasLimit"], 16
)
paymaster_data = HexBytes(user_operation["paymasterData"])
else:
paymaster_verification_gas_limit = None
paymaster_post_op_gas_limit = None
paymaster_data = None

return UserOperationV07(
HexBytes(user_operation_hash),
ChecksumAddress(user_operation["sender"]),
int(user_operation["nonce"], 16),
ChecksumAddress(user_operation["factory"]),
HexBytes(user_operation["factoryData"]),
HexBytes(user_operation["callData"]),
int(user_operation["callGasLimit"], 16),
int(user_operation["verificationGasLimit"], 16),
int(user_operation["preVerificationGas"], 16),
int(user_operation["maxPriorityFeePerGas"], 16),
int(user_operation["maxFeePerGas"], 16),
HexBytes(user_operation["signature"]),
ChecksumAddress(user_operation_response["entryPoint"]),
paymaster_verification_gas_limit,
paymaster_post_op_gas_limit,
paymaster,
paymaster_data,
metadata=metadata,
)

def __str__(self):
return f"User Operation sender={self.sender} nonce={self.nonce} hash={self.user_operation_hash.hex()}"
Expand Down Expand Up @@ -116,3 +154,93 @@ def calculate_user_operation_hash(self, chain_id: int) -> bytes:
[fast_keccak(user_operation_encoded), self.entry_point, chain_id],
)
)


@dataclasses.dataclass(eq=True, frozen=True)
class UserOperationV07:
"""
EIP4337 UserOperation for Entrypoint v0.7
https://github.com/eth-infinitism/account-abstraction/blob/v0.7.0/contracts/interfaces/PackedUserOperation.sol
"""

user_operation_hash: bytes
sender: ChecksumAddress
nonce: int
factory: ChecksumAddress
factory_data: bytes
call_data: bytes
call_gas_limit: int
verification_gas_limit: int
pre_verification_gas: int
max_priority_fee_per_gas: int
max_fee_per_gas: int
signature: bytes
entry_point: ChecksumAddress
paymaster_verification_gas_limit: Optional[int] = None
paymaster_post_op_gas_limit: Optional[int] = None
paymaster: Optional[bytes] = None
paymaster_data: Optional[bytes] = None
metadata: Optional[UserOperationMetadata] = None

@property
def account_gas_limits(self) -> bytes:
"""
:return:Account Gas Limits is a `bytes32` in Solidity, first `bytes16` `verification_gas_limit` and then `call_gas_limit`
"""
return HexBytes(self.verification_gas_limit).rjust(16, b"\x00") + HexBytes(
self.call_gas_limit
).rjust(16, b"\x00")

@property
def gas_fees(self) -> bytes:
"""
:return: Gas Fees is a `bytes32` in Solidity, first `bytes16` `verification_gas_limit` and then `call_gas_limit`
"""
return HexBytes(self.max_priority_fee_per_gas).rjust(16, b"\x00") + HexBytes(
self.max_fee_per_gas
).rjust(16, b"\x00")

@property
def paymaster_and_data(self) -> bytes:
if not self.paymaster:
return b""
return (
HexBytes(self.paymaster).rjust(20, b"\x00")
+ HexBytes(self.paymaster_verification_gas_limit).rjust(16, b"\x00")
+ HexBytes(self.paymaster_post_op_gas_limit).rjust(16, b"\x00")
+ HexBytes(self.paymaster_data)
)

def calculate_user_operation_hash(self, chain_id: int) -> bytes:
hash_init_code = fast_keccak(HexBytes(self.factory) + self.factory_data)
hash_call_data = fast_keccak(self.call_data)
hash_paymaster_and_data = fast_keccak(self.paymaster_and_data)
user_operation_encoded = abi_encode(
[
"address",
"uint256",
"bytes32",
"bytes32",
"bytes32",
"uint256",
"bytes32",
"bytes32",
],
[
self.sender,
self.nonce,
hash_init_code,
hash_call_data,
self.account_gas_limits,
self.pre_verification_gas,
self.gas_fees,
hash_paymaster_and_data,
],
)
return fast_keccak(
abi_encode(
["bytes32", "address", "uint256"],
[fast_keccak(user_operation_encoded), self.entry_point, chain_id],
)
)
25 changes: 24 additions & 1 deletion gnosis/eth/tests/account_abstraction/test_e2e_bundler_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import pytest

from ...account_abstraction import BundlerClient, UserOperation, UserOperationReceipt
from ...account_abstraction.user_operation import UserOperationV07
from ..mocks.mock_bundler import (
safe_4337_chain_id_mock,
safe_4337_user_operation_hash_mock,
user_operation_mock,
user_operation_receipt_mock,
user_operation_v07_chain_id,
user_operation_v07_hash,
)


Expand All @@ -30,10 +34,29 @@ def test_get_user_operation_by_hash(self):
expected_user_operation = UserOperation.from_bundler_response(
user_operation_hash, user_operation_mock["result"]
)
user_operation = self.bundler.get_user_operation_by_hash(user_operation_hash)
self.assertEqual(
self.bundler.get_user_operation_by_hash(user_operation_hash),
user_operation,
expected_user_operation,
)
self.assertEqual(
user_operation.calculate_user_operation_hash(safe_4337_chain_id_mock).hex(),
user_operation_hash,
)

def test_get_user_operation_V07_by_hash(self):
"""
Test UserOperation v0.7
"""
user_operation_hash = user_operation_v07_hash.hex()
user_operation = self.bundler.get_user_operation_by_hash(user_operation_hash)
self.assertIsInstance(user_operation, UserOperationV07)
self.assertEqual(
user_operation.calculate_user_operation_hash(
user_operation_v07_chain_id
).hex(),
user_operation_hash,
)

def test_get_user_operation_receipt(self):
user_operation_hash = safe_4337_user_operation_hash_mock.hex()
Expand Down
23 changes: 19 additions & 4 deletions gnosis/eth/tests/account_abstraction/test_user_operation.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
from unittest import TestCase

from ...account_abstraction import UserOperation
from ...account_abstraction.user_operation import UserOperationV07
from ..mocks.mock_bundler import (
safe_4337_chain_id_mock,
safe_4337_user_operation_hash_mock,
user_operation_mock,
user_operation_v07_hash,
user_operation_v07_mock,
)


class TestUserOperation(TestCase):
def test_calculate_user_operation_hash(self):
user_operation_hash = safe_4337_user_operation_hash_mock.hex()
def test_calculate_user_operation_hash_V06(self):
user_operation_hash = safe_4337_user_operation_hash_mock
user_operation = UserOperation.from_bundler_response(
user_operation_hash, user_operation_mock["result"]
user_operation_hash.hex(), user_operation_mock["result"]
)
self.assertIsInstance(user_operation, UserOperation)
self.assertEqual(
user_operation.calculate_user_operation_hash(safe_4337_chain_id_mock),
safe_4337_user_operation_hash_mock,
user_operation_hash,
)

def test_calculate_user_operation_hash_V07(self):
user_operation_hash = user_operation_v07_hash
user_operation = UserOperation.from_bundler_response(
user_operation_hash.hex(), user_operation_v07_mock["result"]
)
self.assertIsInstance(user_operation, UserOperationV07)
self.assertEqual(
user_operation.calculate_user_operation_hash(safe_4337_chain_id_mock),
user_operation_hash,
)
31 changes: 30 additions & 1 deletion gnosis/eth/tests/mocks/mock_bundler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
},
}


user_operation_receipt_mock = {
"jsonrpc": "2.0",
"id": 1,
Expand Down Expand Up @@ -423,3 +422,33 @@
"transactionHash": "0xe2dc67ebcfc3215eaf3809931529ea9a4c657d1931f9b8e0ec5714d71d0a8387",
},
}


# v0.7.0 UserOperation
user_operation_v07_chain_id = 11155111
user_operation_v07_hash = HexBytes(
"0xc8e745161cb3523539bae0e5ed7fa7812dd812bf39030bb73378e792c1ee6576"
)
user_operation_v07_mock = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"userOperation": {
"sender": "0x1da329ef902379ff73554502d39926e464908425",
"nonce": "0x0",
"callData": "0x1af876fe00000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762000000000000000000000000c40156abfee908e2e3269da84fa9609bccddec60fc22c8ee4f29eb3d046f14f2d9b50ced813a1522cc702c418a4e1134b81ac3a002dfc8bae91519edc3944211c524092a1b599acf0d1f5e08e820d618e2f09c6e000000000000000000000000ca89cba4813d5b40aec6e57a30d0eeb500d6531b0000000000000000000000002dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47000000000000000000000000000000000000000000000000000000000000012000000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c22600000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000648d0dc49f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c226000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104541d63c8000000000000000000000000bb9ebb7b8ee75cdbf64e5ce124731a89c2bc4a070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a14481940000000000000000000000001da329ef902379ff73554502d39926e46490842568e9ec8f675d7d3e64886c7a33a4763060a61422da8e6efa875b0901be5176e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"callGasLimit": "0xc34cf",
"verificationGasLimit": "0xe2395",
"preVerificationGas": "0x1241e",
"maxPriorityFeePerGas": "0x47868c00",
"maxFeePerGas": "0x5dbefe60",
"factory": "0x4e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67",
"factoryData": "0x1688f0b9000000000000000000000000f9e93efbf37b588f9c79d509c48b87a26c3dfeb90000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000844fff40e1277cc7ff46804f631ff0ed4cca5603a534066323b522073e113ee4d86d34298100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e04d29724cd758a76bde450eb4e2be71bf21398d55795de887cab857b85cd49a447d013104f681b1a225de0d97e4e3dfc8587861f847f539a05425591d3167c9f2000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034226f726967696e223a22687474703a2f2f6c6f63616c686f73743a35313733222c2263726f73734f726967696e223a66616c7365000000000000000000000000",
},
"entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"blockNumber": "0x581c8f",
"blockHash": "0x36477d9ffb259cd2dd695c524ad32d6a92c9e11e0693191e1de30d358828fcf8",
"transactionHash": "0xe4c96ce576cbd9228c645e31d64a38a56e8c9b57b4e00882f6517fd73e7c183f",
},
}

0 comments on commit 25e3371

Please sign in to comment.