Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4cf76e5
Add EntryPoint v0.7 and v0.6 constants
sherifahmed990 Oct 30, 2025
a6e2c4e
Add v0.7 entrypoint and module addresses to default settings
sherifahmed990 Oct 30, 2025
b0d76f9
Add UserOperationV7 class for entrypoint v0.7 support
sherifahmed990 Oct 30, 2025
2a7b386
Add SafeOperation wrapper class to oveeride from_user_operation to ac…
sherifahmed990 Oct 30, 2025
e161f56
Add database migration for entrypoint v0.7 support
sherifahmed990 Oct 30, 2025
8ff1f96
Update class UserOperation to support both v0.6 and v0.7 useroperations
sherifahmed990 Oct 30, 2025
f0bfdc2
Update serializer classes to support v0.7 useroperation
sherifahmed990 Oct 30, 2025
99c9dc5
Add support for useroperation v7 to index_user_operation
sherifahmed990 Oct 30, 2025
d52bbab
Use the new SafeOperation wrapper class for tests
sherifahmed990 Oct 30, 2025
c3f5cf4
Override settings to only support v0.6 for test_process_aa_transactio…
sherifahmed990 Oct 30, 2025
be1e1b2
Merge branch 'safe-global:main' into entrypointv7
sherifahmed990 Oct 30, 2025
c6ca161
Merge branch 'safe-global:main' into entrypointv7
sherifahmed990 Dec 6, 2025
1a03408
Merge branch 'safe-global:main' into entrypointv7
sherifahmed990 Dec 11, 2025
808007c
Bump safe-eth-py from 7.14.0 to 7.17.0
sherifahmed990 Dec 6, 2025
cfcfca1
import ETHEREUM_4337_ENTRYPOINT_V6 &v7 from base.py
sherifahmed990 Dec 12, 2025
eaae31a
import SafeOperation and UserOperationV07 from safe-eth-py 7.17.0
sherifahmed990 Dec 12, 2025
f097ccd
update comment
sherifahmed990 Dec 12, 2025
2b851a0
fix: import user_operation_v07_hash_1 and user_operation_v07_mock_1 f…
sherifahmed990 Dec 12, 2025
d8aee49
fix: ETHEREUM_4337_ENTRYPOINT_V6 and ETHEREUM_4337_ENTRYPOINT_V7
sherifahmed990 Dec 12, 2025
c2e4b16
Merge branch 'main' into entrypointv7
sherifahmed990 Jan 5, 2026
fcc9da4
Merge branch 'safe-global:main' into entrypointv7
sherifahmed990 Jan 14, 2026
0934612
use parentheses instead of brackets for validated_data.get()
sherifahmed990 Jan 6, 2026
96e7b5c
Add paymaster and paymaster_data fields for v7 fields
sherifahmed990 Jan 14, 2026
4965128
fix validation and handling for some fields
sherifahmed990 Jan 16, 2026
fa4aa7a
Add tests for SafeOperationSerializer including valid_after and valid…
sherifahmed990 Jan 16, 2026
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
12 changes: 10 additions & 2 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,13 +540,21 @@
# Ethereum 4337 Bundler RPC
# ------------------------------------------------------------------------------
ETHEREUM_4337_BUNDLER_URL = env("ETHEREUM_4337_BUNDLER_URL", default=None)
ETHEREUM_4337_ENTRYPOINT_V6 = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
ETHEREUM_4337_ENTRYPOINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
ETHEREUM_4337_SUPPORTED_ENTRY_POINTS = env.list(
"ETHEREUM_4337_SUPPORTED_ENTRY_POINTS",
default=["0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"],
default=[ETHEREUM_4337_ENTRYPOINT_V6, ETHEREUM_4337_ENTRYPOINT_V7],
)
# Module addresses
ETHEREUM_4337_SAFE_MODULE_ADDRESS_V06 = "0xa581c4A4DB7175302464fF3C06380BC3270b4037"
ETHEREUM_4337_SAFE_MODULE_ADDRESS_V07 = "0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226"
ETHEREUM_4337_SUPPORTED_SAFE_MODULES = env.list(
"ETHEREUM_4337_SUPPORTED_SAFE_MODULES",
default=["0xa581c4A4DB7175302464fF3C06380BC3270b4037"],
default=[
ETHEREUM_4337_SAFE_MODULE_ADDRESS_V06, # v0.6 Safe4337Module
ETHEREUM_4337_SAFE_MODULE_ADDRESS_V07, # v0.7 Safe4337Module
],
)

# Tracing indexing configuration (not useful for L2 indexing)
Expand Down
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'),
),
]
109 changes: 90 additions & 19 deletions safe_transaction_service/account_abstraction/models.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import logging
from functools import cached_property

from django.conf import settings
from django.db import models
from django.db.models import Index

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.account_abstraction import UserOperationV07 as UserOperationV7
from safe_eth.eth.django.models import (
EthereumAddressBinaryField,
HexV2Field,
Keccak256Field,
Uint256Field,
)
from safe_eth.eth.utils import fast_to_checksum_address
from safe_eth.safe.account_abstraction import SafeOperation as SafeOperationClass
from safe_eth.safe.safe_signature import SafeSignatureType
from safe_eth.util.util import to_0x_hex_str
Expand Down Expand Up @@ -52,10 +55,49 @@ class UserOperation(models.Model):
signature = models.BinaryField(null=True, blank=True, editable=True)
entry_point = EthereumAddressBinaryField(db_index=True)

# UserOperation v7 specific fields, filled as `None` for <= v6
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}"
Expand All @@ -65,7 +107,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

Expand All @@ -86,23 +130,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() == settings.ETHEREUM_4337_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() == settings.ETHEREUM_4337_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")
Copy link
Member

Choose a reason for hiding this comment

The 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 entrypoint address is a good solution for your use case, but not for the service

Copy link
Author

Choose a reason for hiding this comment

The 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.
eth-infinitism/account-abstraction#372 (comment)
I think it is safe to only support 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).
Also v7, v8 and v9 have the same useroperation structure.
So i think using the entrypoint address is the only option with the current structure.
I added some model constrains as sanity checks to verify that some values should be none for each useroperation type, i can repeat these sanity checks here too.


def to_safe_operation(self) -> SafeOperationClass:
"""
Expand Down
Loading