-
Notifications
You must be signed in to change notification settings - Fork 320
Add support to entrypoint v0.07 useroperations #2692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 11 commits
4cf76e5
a6e2c4e
b0d76f9
2a7b386
e161f56
8ff1f96
f0bfdc2
99c9dc5
d52bbab
c3f5cf4
be1e1b2
c6ca161
1a03408
808007c
cfcfca1
eaae31a
f097ccd
2b851a0
d8aee49
c2e4b16
fcc9da4
0934612
96e7b5c
4965128
fa4aa7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from safe_eth.eth.account_abstraction import UserOperation as UserOperationV6 | ||
| from safe_eth.eth.utils import fast_keccak | ||
| from safe_eth.safe.account_abstraction import SafeOperation as SafeOperationClass | ||
|
|
||
| from safe_transaction_service.account_abstraction.UserOperationV7 import UserOperationV7 | ||
|
||
|
|
||
|
|
||
| class SafeOperation(SafeOperationClass): | ||
| @classmethod | ||
| def from_user_operation(cls, user_operation: UserOperationV6 | UserOperationV7): | ||
| return cls( | ||
| user_operation.sender, | ||
| user_operation.nonce, | ||
| fast_keccak(user_operation.init_code), | ||
| fast_keccak(user_operation.call_data), | ||
| user_operation.call_gas_limit, | ||
| user_operation.verification_gas_limit, | ||
| user_operation.pre_verification_gas, | ||
| user_operation.max_fee_per_gas, | ||
| user_operation.max_priority_fee_per_gas, | ||
| fast_keccak(user_operation.paymaster_and_data), | ||
| int.from_bytes(user_operation.signature[:6], byteorder="big"), | ||
| int.from_bytes(user_operation.signature[6:12], byteorder="big"), | ||
| user_operation.entry_point, | ||
| user_operation.signature[12:], | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import dataclasses | ||
|
|
||
| from eth_abi import encode as abi_encode | ||
| from eth_typing import ChecksumAddress | ||
| from hexbytes import HexBytes | ||
| from safe_eth.eth.account_abstraction.user_operation import UserOperationMetadata | ||
| from safe_eth.eth.utils import fast_keccak | ||
|
|
||
|
|
||
| @dataclasses.dataclass(eq=True, frozen=True) | ||
| class UserOperationV7: | ||
| """ | ||
| 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 | ||
| 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 | ||
| factory: ChecksumAddress | None = None | ||
| factory_data: bytes | None = None | ||
| paymaster_verification_gas_limit: int | None = None | ||
| paymaster_post_op_gas_limit: int | None = None | ||
| paymaster: bytes | None = None | ||
| paymaster_data: bytes | None = None | ||
| metadata: UserOperationMetadata | None = None | ||
|
|
||
| @property | ||
| def init_code(self) -> bytes: | ||
| """ | ||
| Returns the raw init_code bytes (factory address + factory_data). | ||
| For v0.7, this is the concatenation of factory and factory_data. | ||
| """ | ||
| if self.factory is not None and self.factory_data is not None: | ||
| return HexBytes(self.factory) + self.factory_data | ||
| else: | ||
| return b"" | ||
|
|
||
| @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 | ||
| or not self.paymaster_verification_gas_limit | ||
| or not self.paymaster_post_op_gas_limit | ||
| or not self.paymaster_data | ||
| ): | ||
| 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(self.init_code) if self.init_code else fast_keccak(b"") | ||
| ) | ||
| 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], | ||
| ) | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,3 +21,5 @@ | |
| USER_OPERATION_EVENT_TOPIC = HexBytes( | ||
| "0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f" | ||
| ) | ||
| ENTRYPOINT_V6 = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" | ||
| ENTRYPOINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Generated by Django 5.2.7 on 2025-10-28 17:35 | ||
|
|
||
| import safe_eth.eth.django.models | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('account_abstraction', '0005_alter_safeoperation_module_address_and_more'), | ||
| ('history', '0095_remove_internaltx_history_internaltx_value_idx_and_more'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='useroperation', | ||
| name='factory', | ||
| field=safe_eth.eth.django.models.EthereumAddressBinaryField(blank=True, db_index=True, null=True), | ||
| ), | ||
| migrations.AddField( | ||
| model_name='useroperation', | ||
| name='factory_data', | ||
| field=models.BinaryField(blank=True, editable=True, null=True), | ||
| ), | ||
| migrations.AddField( | ||
| model_name='useroperation', | ||
| name='paymaster_post_op_gas_limit', | ||
| field=safe_eth.eth.django.models.Uint256Field(blank=True, null=True), | ||
| ), | ||
| migrations.AddField( | ||
| model_name='useroperation', | ||
| name='paymaster_verification_gas_limit', | ||
| field=safe_eth.eth.django.models.Uint256Field(blank=True, null=True), | ||
| ), | ||
| migrations.AddConstraint( | ||
| model_name='useroperation', | ||
| constraint=models.CheckConstraint(condition=models.Q(('factory__isnull', True), ('init_code__isnull', True), _connector='OR'), name='factory_or_init_code_not_both'), | ||
| ), | ||
| migrations.AddConstraint( | ||
| model_name='useroperation', | ||
| constraint=models.CheckConstraint(condition=models.Q(models.Q(('paymaster_data__isnull', True), ('paymaster_post_op_gas_limit__isnull', True), ('paymaster_verification_gas_limit__isnull', True)), ('paymaster__isnull', False), _connector='OR'), name='paymaster_required_with_paymaster_fields'), | ||
| ), | ||
| migrations.AddConstraint( | ||
| model_name='useroperation', | ||
| constraint=models.CheckConstraint(condition=models.Q(('init_code__isnull', True), models.Q(('paymaster_post_op_gas_limit__isnull', True), ('paymaster_verification_gas_limit__isnull', True)), _connector='OR'), name='v7_paymaster_gas_limits_not_with_init_code'), | ||
| ), | ||
| migrations.AddConstraint( | ||
| model_name='useroperation', | ||
| constraint=models.CheckConstraint(condition=models.Q(('factory_data__isnull', True), ('factory__isnull', False), _connector='OR'), name='factory_required_with_factory_data'), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,18 +7,26 @@ | |
| from eth_abi.packed import encode_packed | ||
| from hexbytes import HexBytes | ||
| from model_utils.models import TimeStampedModel | ||
| from safe_eth.eth.account_abstraction import UserOperation as UserOperationClass | ||
| from safe_eth.eth.account_abstraction import UserOperation as UserOperationV6 | ||
| from safe_eth.eth.account_abstraction import UserOperationMetadata | ||
| from safe_eth.eth.django.models import ( | ||
| EthereumAddressBinaryField, | ||
| HexV2Field, | ||
| Keccak256Field, | ||
| Uint256Field, | ||
| ) | ||
| from safe_eth.safe.account_abstraction import SafeOperation as SafeOperationClass | ||
| from safe_eth.eth.utils import fast_to_checksum_address | ||
| from safe_eth.safe.safe_signature import SafeSignatureType | ||
| from safe_eth.util.util import to_0x_hex_str | ||
|
|
||
| from safe_transaction_service.account_abstraction.constants import ( | ||
| ENTRYPOINT_V6, | ||
| ENTRYPOINT_V7, | ||
| ) | ||
| from safe_transaction_service.account_abstraction.SafeOperation import ( | ||
| SafeOperation as SafeOperationClass, | ||
| ) | ||
| from safe_transaction_service.account_abstraction.UserOperationV7 import UserOperationV7 | ||
| from safe_transaction_service.history import models as history_models | ||
| from safe_transaction_service.utils.constants import SIGNATURE_LENGTH | ||
|
|
||
|
|
@@ -52,10 +60,49 @@ class UserOperation(models.Model): | |
| signature = models.BinaryField(null=True, blank=True, editable=True) | ||
| entry_point = EthereumAddressBinaryField(db_index=True) | ||
|
|
||
| # useroperation v7 specific fields | ||
sherifahmed990 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| factory = EthereumAddressBinaryField( | ||
| db_index=True, null=True, blank=True, editable=True | ||
| ) | ||
| factory_data = models.BinaryField(null=True, blank=True, editable=True) | ||
| paymaster_verification_gas_limit = Uint256Field( | ||
| null=True, blank=True, editable=True | ||
| ) | ||
| paymaster_post_op_gas_limit = Uint256Field(null=True, blank=True, editable=True) | ||
|
|
||
| class Meta: | ||
| indexes = [ | ||
| Index(fields=["sender", "-nonce"]), | ||
| ] | ||
| constraints = [ | ||
| models.CheckConstraint( | ||
| condition=models.Q(factory__isnull=True) | ||
| | models.Q(init_code__isnull=True), | ||
| name="factory_or_init_code_not_both", | ||
| ), | ||
| models.CheckConstraint( | ||
| condition=models.Q( | ||
| paymaster_verification_gas_limit__isnull=True, | ||
| paymaster_post_op_gas_limit__isnull=True, | ||
| paymaster_data__isnull=True, | ||
| ) | ||
| | models.Q(paymaster__isnull=False), | ||
| name="paymaster_required_with_paymaster_fields", | ||
| ), | ||
| models.CheckConstraint( | ||
| condition=models.Q(init_code__isnull=True) | ||
| | models.Q( | ||
| paymaster_verification_gas_limit__isnull=True, | ||
| paymaster_post_op_gas_limit__isnull=True, | ||
| ), | ||
| name="v7_paymaster_gas_limits_not_with_init_code", | ||
| ), | ||
| models.CheckConstraint( | ||
| condition=models.Q(factory_data__isnull=True) | ||
| | models.Q(factory__isnull=False), | ||
| name="factory_required_with_factory_data", | ||
| ), | ||
| ] | ||
|
|
||
| def __str__(self) -> str: | ||
| return f"{to_0x_hex_str(HexBytes(self.hash))} UserOperation for sender={self.sender} with nonce={self.nonce}" | ||
|
|
@@ -65,7 +112,9 @@ def paymaster_and_data(self) -> HexBytes | None: | |
| if self.paymaster and self.paymaster_data: | ||
| return HexBytes(HexBytes(self.paymaster) + HexBytes(self.paymaster_data)) | ||
|
|
||
| def to_user_operation(self, add_tx_metadata: bool = False) -> UserOperationClass: | ||
| def to_user_operation( | ||
| self, add_tx_metadata: bool = False | ||
| ) -> UserOperationV6 | UserOperationV7: | ||
| """ | ||
| Returns a safe-eth-py UserOperation object | ||
|
|
||
|
|
@@ -86,23 +135,50 @@ def to_user_operation(self, add_tx_metadata: bool = False) -> UserOperationClass | |
| if not self.signature: | ||
| raise ValueError("Signature should not be empty") | ||
| signature = HexBytes(self.signature) | ||
|
|
||
| return UserOperationClass( | ||
| HexBytes(self.hash), | ||
| self.sender, | ||
| self.nonce, | ||
| HexBytes(self.init_code) if self.init_code else b"", | ||
| HexBytes(self.call_data) if self.call_data else b"", | ||
| self.call_gas_limit, | ||
| self.verification_gas_limit, | ||
| self.pre_verification_gas, | ||
| self.max_fee_per_gas, | ||
| self.max_priority_fee_per_gas, | ||
| self.paymaster_and_data if self.paymaster_and_data else b"", | ||
| signature, | ||
| self.entry_point, | ||
| user_operation_metadata, | ||
| ) | ||
| if self.entry_point.lower() == ENTRYPOINT_V6.lower(): | ||
| return UserOperationV6( | ||
| HexBytes(self.hash), | ||
| self.sender, | ||
| self.nonce, | ||
| HexBytes(self.init_code) if self.init_code else b"", | ||
| HexBytes(self.call_data) if self.call_data else b"", | ||
| self.call_gas_limit, | ||
| self.verification_gas_limit, | ||
| self.pre_verification_gas, | ||
| self.max_fee_per_gas, | ||
| self.max_priority_fee_per_gas, | ||
| self.paymaster_and_data if self.paymaster_and_data else b"", | ||
| signature, | ||
| self.entry_point, | ||
| user_operation_metadata, | ||
| ) | ||
| elif self.entry_point.lower() == ENTRYPOINT_V7.lower(): | ||
| return UserOperationV7( | ||
| HexBytes(self.hash), | ||
| self.sender, | ||
| self.nonce, | ||
| HexBytes(self.call_data) if self.call_data else HexBytes(b""), | ||
| self.call_gas_limit, | ||
| self.verification_gas_limit, | ||
| self.pre_verification_gas, | ||
| self.max_fee_per_gas, | ||
| self.max_priority_fee_per_gas, | ||
| signature, | ||
| self.entry_point, | ||
| fast_to_checksum_address(self.factory) if self.factory else None, | ||
| HexBytes(self.factory_data) if self.factory_data else b"", | ||
| self.paymaster_verification_gas_limit | ||
| if self.paymaster_verification_gas_limit | ||
| else None, | ||
| self.paymaster_post_op_gas_limit | ||
| if self.paymaster_post_op_gas_limit | ||
| else None, | ||
| fast_to_checksum_address(self.paymaster) if self.paymaster else None, | ||
| HexBytes(self.paymaster_data) if self.paymaster_data else None, | ||
| user_operation_metadata, | ||
| ) | ||
| else: | ||
| raise ValueError("Unsupported entrypoint") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do entrypoints have the same address for every network? Remember that we run tx service for a lot of different networks, and it needs to be compatible with them. There's no other way to tell appart v6 from v7? I would prefer to check a field that exists on v7 and not on v6, for example. Checking
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the 4337 tooling that i know of assumes the official entrypoints addresses. Because we are using the same class for both useroperation types, a useroperation without init/factory and without paymaster would be identical for both v6 and v7(except for the entrypoint address). |
||
|
|
||
| def to_safe_operation(self) -> SafeOperationClass: | ||
| """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Being picky:
So all the settings related to 4337 start the same.
Also hardcoding shouldn't be done, it needs to be defined using environment configuration variables as the other configuration (using
env.str)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok