t402 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- t402/__init__.py +48 -0
- t402/erc4337/__init__.py +188 -0
- t402/erc4337/accounts.py +434 -0
- t402/erc4337/bundlers.py +458 -0
- t402/erc4337/paymasters.py +532 -0
- t402/erc4337/types.py +315 -0
- {t402-1.2.0.dist-info → t402-1.3.0.dist-info}/METADATA +1 -1
- {t402-1.2.0.dist-info → t402-1.3.0.dist-info}/RECORD +9 -4
- {t402-1.2.0.dist-info → t402-1.3.0.dist-info}/WHEEL +0 -0
t402/__init__.py
CHANGED
|
@@ -64,6 +64,33 @@ from t402.paywall import (
|
|
|
64
64
|
get_paywall_template,
|
|
65
65
|
is_browser_request,
|
|
66
66
|
)
|
|
67
|
+
from t402.erc4337 import (
|
|
68
|
+
# Constants
|
|
69
|
+
ENTRYPOINT_V07_ADDRESS,
|
|
70
|
+
ENTRYPOINT_V06_ADDRESS,
|
|
71
|
+
SAFE_4337_ADDRESSES,
|
|
72
|
+
SUPPORTED_CHAINS as ERC4337_SUPPORTED_CHAINS,
|
|
73
|
+
# Types
|
|
74
|
+
UserOperation,
|
|
75
|
+
PackedUserOperation,
|
|
76
|
+
PaymasterData,
|
|
77
|
+
GasEstimate,
|
|
78
|
+
UserOperationReceipt,
|
|
79
|
+
# Bundlers
|
|
80
|
+
GenericBundlerClient,
|
|
81
|
+
PimlicoBundlerClient,
|
|
82
|
+
AlchemyBundlerClient,
|
|
83
|
+
create_bundler_client,
|
|
84
|
+
# Paymasters
|
|
85
|
+
PimlicoPaymaster,
|
|
86
|
+
BiconomyPaymaster,
|
|
87
|
+
StackupPaymaster,
|
|
88
|
+
create_paymaster,
|
|
89
|
+
# Accounts
|
|
90
|
+
SafeSmartAccount,
|
|
91
|
+
SafeAccountConfig,
|
|
92
|
+
create_smart_account,
|
|
93
|
+
)
|
|
67
94
|
|
|
68
95
|
def hello() -> str:
|
|
69
96
|
return "Hello from t402!"
|
|
@@ -132,4 +159,25 @@ __all__ = [
|
|
|
132
159
|
"get_paywall_html",
|
|
133
160
|
"get_paywall_template",
|
|
134
161
|
"is_browser_request",
|
|
162
|
+
# ERC-4337 Account Abstraction
|
|
163
|
+
"ENTRYPOINT_V07_ADDRESS",
|
|
164
|
+
"ENTRYPOINT_V06_ADDRESS",
|
|
165
|
+
"SAFE_4337_ADDRESSES",
|
|
166
|
+
"ERC4337_SUPPORTED_CHAINS",
|
|
167
|
+
"UserOperation",
|
|
168
|
+
"PackedUserOperation",
|
|
169
|
+
"PaymasterData",
|
|
170
|
+
"GasEstimate",
|
|
171
|
+
"UserOperationReceipt",
|
|
172
|
+
"GenericBundlerClient",
|
|
173
|
+
"PimlicoBundlerClient",
|
|
174
|
+
"AlchemyBundlerClient",
|
|
175
|
+
"create_bundler_client",
|
|
176
|
+
"PimlicoPaymaster",
|
|
177
|
+
"BiconomyPaymaster",
|
|
178
|
+
"StackupPaymaster",
|
|
179
|
+
"create_paymaster",
|
|
180
|
+
"SafeSmartAccount",
|
|
181
|
+
"SafeAccountConfig",
|
|
182
|
+
"create_smart_account",
|
|
135
183
|
]
|
t402/erc4337/__init__.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ERC-4337 Account Abstraction Module for T402
|
|
3
|
+
|
|
4
|
+
This module provides complete ERC-4337 v0.7 support including:
|
|
5
|
+
- UserOperation building and signing
|
|
6
|
+
- Bundler clients (generic, Pimlico, Alchemy)
|
|
7
|
+
- Paymaster integration (Pimlico, Biconomy, Stackup)
|
|
8
|
+
- Smart account implementations (Safe)
|
|
9
|
+
|
|
10
|
+
Example usage:
|
|
11
|
+
|
|
12
|
+
from t402.erc4337 import (
|
|
13
|
+
UserOperation,
|
|
14
|
+
create_bundler_client,
|
|
15
|
+
create_paymaster,
|
|
16
|
+
SafeSmartAccount,
|
|
17
|
+
SafeAccountConfig,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Create a Safe smart account
|
|
21
|
+
account = SafeSmartAccount(SafeAccountConfig(
|
|
22
|
+
owner_private_key="0x...",
|
|
23
|
+
chain_id=84532, # Base Sepolia
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
# Create a bundler client
|
|
27
|
+
bundler = create_bundler_client(
|
|
28
|
+
provider="pimlico",
|
|
29
|
+
api_key="your-api-key",
|
|
30
|
+
chain_id=84532,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Create a paymaster
|
|
34
|
+
paymaster = create_paymaster(
|
|
35
|
+
provider="pimlico",
|
|
36
|
+
api_key="your-api-key",
|
|
37
|
+
chain_id=84532,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Build and submit a UserOperation
|
|
41
|
+
user_op = UserOperation(
|
|
42
|
+
sender=account.get_address(),
|
|
43
|
+
call_data=account.encode_execute(
|
|
44
|
+
target="0x...",
|
|
45
|
+
value=0,
|
|
46
|
+
data=b"...",
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Get gas estimates and paymaster data
|
|
51
|
+
gas = bundler.estimate_user_operation_gas(user_op)
|
|
52
|
+
pm_data = paymaster.get_paymaster_data(user_op, 84532, ENTRYPOINT_V07_ADDRESS)
|
|
53
|
+
|
|
54
|
+
# Update user_op with estimates and sign
|
|
55
|
+
user_op.verification_gas_limit = gas.verification_gas_limit
|
|
56
|
+
user_op.call_gas_limit = gas.call_gas_limit
|
|
57
|
+
user_op.pre_verification_gas = gas.pre_verification_gas
|
|
58
|
+
user_op.paymaster_and_data = pm_data.to_bytes()
|
|
59
|
+
user_op.signature = account.sign_user_op_hash(user_op_hash)
|
|
60
|
+
|
|
61
|
+
# Submit
|
|
62
|
+
user_op_hash = bundler.send_user_operation(user_op)
|
|
63
|
+
receipt = bundler.wait_for_receipt(user_op_hash)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Types
|
|
67
|
+
from .types import (
|
|
68
|
+
# Constants
|
|
69
|
+
ENTRYPOINT_V07_ADDRESS,
|
|
70
|
+
ENTRYPOINT_V06_ADDRESS,
|
|
71
|
+
SAFE_4337_ADDRESSES,
|
|
72
|
+
SUPPORTED_CHAINS,
|
|
73
|
+
ALCHEMY_NETWORKS,
|
|
74
|
+
PIMLICO_NETWORKS,
|
|
75
|
+
DEFAULT_GAS_LIMITS,
|
|
76
|
+
BUNDLER_METHODS,
|
|
77
|
+
# Enums
|
|
78
|
+
PaymasterType,
|
|
79
|
+
# Dataclasses
|
|
80
|
+
UserOperation,
|
|
81
|
+
PackedUserOperation,
|
|
82
|
+
PaymasterData,
|
|
83
|
+
GasEstimate,
|
|
84
|
+
UserOperationReceipt,
|
|
85
|
+
BundlerConfig,
|
|
86
|
+
PaymasterConfig,
|
|
87
|
+
TokenQuote,
|
|
88
|
+
AssetChange,
|
|
89
|
+
SimulationResult,
|
|
90
|
+
# Functions
|
|
91
|
+
pack_account_gas_limits,
|
|
92
|
+
unpack_account_gas_limits,
|
|
93
|
+
pack_gas_fees,
|
|
94
|
+
unpack_gas_fees,
|
|
95
|
+
is_supported_chain,
|
|
96
|
+
get_alchemy_network,
|
|
97
|
+
get_pimlico_network,
|
|
98
|
+
get_dummy_signature,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Bundlers
|
|
102
|
+
from .bundlers import (
|
|
103
|
+
BundlerError,
|
|
104
|
+
GenericBundlerClient,
|
|
105
|
+
PimlicoBundlerClient,
|
|
106
|
+
PimlicoGasPrice,
|
|
107
|
+
AlchemyBundlerClient,
|
|
108
|
+
AlchemyPolicyConfig,
|
|
109
|
+
GasAndPaymasterResult,
|
|
110
|
+
create_bundler_client,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Paymasters
|
|
114
|
+
from .paymasters import (
|
|
115
|
+
PaymasterError,
|
|
116
|
+
PaymasterClient,
|
|
117
|
+
PimlicoPaymaster,
|
|
118
|
+
BiconomyPaymaster,
|
|
119
|
+
StackupPaymaster,
|
|
120
|
+
UnifiedPaymaster,
|
|
121
|
+
create_paymaster,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Accounts
|
|
125
|
+
from .accounts import (
|
|
126
|
+
SmartAccountError,
|
|
127
|
+
SmartAccountSigner,
|
|
128
|
+
SafeSmartAccount,
|
|
129
|
+
SafeAccountConfig,
|
|
130
|
+
create_smart_account,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
# Constants
|
|
135
|
+
"ENTRYPOINT_V07_ADDRESS",
|
|
136
|
+
"ENTRYPOINT_V06_ADDRESS",
|
|
137
|
+
"SAFE_4337_ADDRESSES",
|
|
138
|
+
"SUPPORTED_CHAINS",
|
|
139
|
+
"ALCHEMY_NETWORKS",
|
|
140
|
+
"PIMLICO_NETWORKS",
|
|
141
|
+
"DEFAULT_GAS_LIMITS",
|
|
142
|
+
"BUNDLER_METHODS",
|
|
143
|
+
# Enums
|
|
144
|
+
"PaymasterType",
|
|
145
|
+
# Types
|
|
146
|
+
"UserOperation",
|
|
147
|
+
"PackedUserOperation",
|
|
148
|
+
"PaymasterData",
|
|
149
|
+
"GasEstimate",
|
|
150
|
+
"UserOperationReceipt",
|
|
151
|
+
"BundlerConfig",
|
|
152
|
+
"PaymasterConfig",
|
|
153
|
+
"TokenQuote",
|
|
154
|
+
"AssetChange",
|
|
155
|
+
"SimulationResult",
|
|
156
|
+
# Functions
|
|
157
|
+
"pack_account_gas_limits",
|
|
158
|
+
"unpack_account_gas_limits",
|
|
159
|
+
"pack_gas_fees",
|
|
160
|
+
"unpack_gas_fees",
|
|
161
|
+
"is_supported_chain",
|
|
162
|
+
"get_alchemy_network",
|
|
163
|
+
"get_pimlico_network",
|
|
164
|
+
"get_dummy_signature",
|
|
165
|
+
# Bundlers
|
|
166
|
+
"BundlerError",
|
|
167
|
+
"GenericBundlerClient",
|
|
168
|
+
"PimlicoBundlerClient",
|
|
169
|
+
"PimlicoGasPrice",
|
|
170
|
+
"AlchemyBundlerClient",
|
|
171
|
+
"AlchemyPolicyConfig",
|
|
172
|
+
"GasAndPaymasterResult",
|
|
173
|
+
"create_bundler_client",
|
|
174
|
+
# Paymasters
|
|
175
|
+
"PaymasterError",
|
|
176
|
+
"PaymasterClient",
|
|
177
|
+
"PimlicoPaymaster",
|
|
178
|
+
"BiconomyPaymaster",
|
|
179
|
+
"StackupPaymaster",
|
|
180
|
+
"UnifiedPaymaster",
|
|
181
|
+
"create_paymaster",
|
|
182
|
+
# Accounts
|
|
183
|
+
"SmartAccountError",
|
|
184
|
+
"SmartAccountSigner",
|
|
185
|
+
"SafeSmartAccount",
|
|
186
|
+
"SafeAccountConfig",
|
|
187
|
+
"create_smart_account",
|
|
188
|
+
]
|
t402/erc4337/accounts.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ERC-4337 Smart Account Implementations for T402
|
|
3
|
+
|
|
4
|
+
This module provides smart account implementations for ERC-4337,
|
|
5
|
+
including Safe smart account with 4337 module support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional, List, Tuple
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from eth_account import Account
|
|
12
|
+
from eth_account.messages import encode_defunct
|
|
13
|
+
from eth_utils import keccak
|
|
14
|
+
|
|
15
|
+
from .types import (
|
|
16
|
+
ENTRYPOINT_V07_ADDRESS,
|
|
17
|
+
SAFE_4337_ADDRESSES,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SmartAccountError(Exception):
|
|
22
|
+
"""Error from smart account operations."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SmartAccountSigner(ABC):
|
|
27
|
+
"""Abstract interface for smart account signers."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def get_address(self) -> str:
|
|
31
|
+
"""Get the smart account address."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def sign_user_op_hash(self, user_op_hash: bytes) -> bytes:
|
|
36
|
+
"""Sign a UserOperation hash."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def get_init_code(self) -> bytes:
|
|
41
|
+
"""Get the account's init code for deployment."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def is_deployed(self) -> bool:
|
|
46
|
+
"""Check if the account is deployed."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def encode_execute(self, target: str, value: int, data: bytes) -> bytes:
|
|
51
|
+
"""Encode a call to the account's execute function."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def encode_execute_batch(
|
|
56
|
+
self,
|
|
57
|
+
targets: List[str],
|
|
58
|
+
values: List[int],
|
|
59
|
+
datas: List[bytes]
|
|
60
|
+
) -> bytes:
|
|
61
|
+
"""Encode a batch call to the account's executeBatch function."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class SafeAccountConfig:
|
|
67
|
+
"""Configuration for Safe smart account."""
|
|
68
|
+
owner_private_key: str
|
|
69
|
+
chain_id: int
|
|
70
|
+
salt: int = 0
|
|
71
|
+
entry_point: str = ENTRYPOINT_V07_ADDRESS
|
|
72
|
+
threshold: int = 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SafeSmartAccount(SmartAccountSigner):
|
|
76
|
+
"""Safe smart account implementation for ERC-4337."""
|
|
77
|
+
|
|
78
|
+
# MultiSend library address (v1.3.0)
|
|
79
|
+
MULTI_SEND_ADDRESS = "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526"
|
|
80
|
+
|
|
81
|
+
def __init__(self, config: SafeAccountConfig):
|
|
82
|
+
self.owner_account = Account.from_key(config.owner_private_key)
|
|
83
|
+
self.owner_address = self.owner_account.address
|
|
84
|
+
self.chain_id = config.chain_id
|
|
85
|
+
self.salt = config.salt
|
|
86
|
+
self.entry_point = config.entry_point
|
|
87
|
+
self.threshold = config.threshold
|
|
88
|
+
|
|
89
|
+
self._cached_address: Optional[str] = None
|
|
90
|
+
self._cached_init_code: Optional[bytes] = None
|
|
91
|
+
|
|
92
|
+
def get_address(self) -> str:
|
|
93
|
+
"""Get the counterfactual Safe address."""
|
|
94
|
+
if self._cached_address:
|
|
95
|
+
return self._cached_address
|
|
96
|
+
|
|
97
|
+
# Calculate counterfactual address via CREATE2
|
|
98
|
+
init_code = self.get_init_code()
|
|
99
|
+
|
|
100
|
+
factory_address = bytes.fromhex(SAFE_4337_ADDRESSES["proxy_factory"][2:])
|
|
101
|
+
salt_hash = self._calculate_salt()
|
|
102
|
+
proxy_init_code = self._get_proxy_creation_code()
|
|
103
|
+
init_code_hash = keccak(proxy_init_code)
|
|
104
|
+
|
|
105
|
+
# CREATE2 address: keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:]
|
|
106
|
+
data = bytes([0xff]) + factory_address + salt_hash + init_code_hash
|
|
107
|
+
address_hash = keccak(data)
|
|
108
|
+
|
|
109
|
+
self._cached_address = "0x" + address_hash[12:].hex()
|
|
110
|
+
return self._cached_address
|
|
111
|
+
|
|
112
|
+
def sign_user_op_hash(self, user_op_hash: bytes) -> bytes:
|
|
113
|
+
"""Sign a UserOperation hash."""
|
|
114
|
+
# For Safe 4337, we sign with Ethereum signed message prefix
|
|
115
|
+
message_hash = self._get_safe_user_op_hash(user_op_hash)
|
|
116
|
+
|
|
117
|
+
# Sign with owner key
|
|
118
|
+
signed = self.owner_account.sign_message(
|
|
119
|
+
encode_defunct(primitive=message_hash)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return signed.signature
|
|
123
|
+
|
|
124
|
+
def get_init_code(self) -> bytes:
|
|
125
|
+
"""Get the init code for deploying the Safe."""
|
|
126
|
+
if self._cached_init_code:
|
|
127
|
+
return self._cached_init_code
|
|
128
|
+
|
|
129
|
+
factory_address = bytes.fromhex(SAFE_4337_ADDRESSES["proxy_factory"][2:])
|
|
130
|
+
|
|
131
|
+
# Build the initializer data for Safe setup
|
|
132
|
+
initializer = self._build_initializer()
|
|
133
|
+
|
|
134
|
+
# Encode createProxyWithNonce call
|
|
135
|
+
# Function selector: 0x1688f0b9
|
|
136
|
+
selector = bytes.fromhex("1688f0b9")
|
|
137
|
+
|
|
138
|
+
# ABI encode the parameters
|
|
139
|
+
singleton = bytes.fromhex(SAFE_4337_ADDRESSES["singleton"][2:])
|
|
140
|
+
encoded = self._encode_create_proxy_with_nonce(singleton, initializer, self.salt)
|
|
141
|
+
|
|
142
|
+
self._cached_init_code = factory_address + selector + encoded
|
|
143
|
+
return self._cached_init_code
|
|
144
|
+
|
|
145
|
+
def is_deployed(self) -> bool:
|
|
146
|
+
"""Check if the account is deployed."""
|
|
147
|
+
# This would require an RPC call to check code at address
|
|
148
|
+
# For now, return False (caller should check via eth_getCode)
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def encode_execute(self, target: str, value: int, data: bytes) -> bytes:
|
|
152
|
+
"""Encode a call to Safe's executeUserOp function."""
|
|
153
|
+
# Function: executeUserOp(address to, uint256 value, bytes data, uint8 operation)
|
|
154
|
+
# Selector: 0x541d63c8
|
|
155
|
+
selector = bytes.fromhex("541d63c8")
|
|
156
|
+
|
|
157
|
+
# Operation 0 = CALL
|
|
158
|
+
operation = 0
|
|
159
|
+
|
|
160
|
+
encoded = self._encode_execute_user_op(target, value, data, operation)
|
|
161
|
+
|
|
162
|
+
return selector + encoded
|
|
163
|
+
|
|
164
|
+
def encode_execute_batch(
|
|
165
|
+
self,
|
|
166
|
+
targets: List[str],
|
|
167
|
+
values: List[int],
|
|
168
|
+
datas: List[bytes]
|
|
169
|
+
) -> bytes:
|
|
170
|
+
"""Encode a batch call using multiSend."""
|
|
171
|
+
if len(targets) != len(values) or len(targets) != len(datas):
|
|
172
|
+
raise SmartAccountError("targets, values, and datas must have same length")
|
|
173
|
+
|
|
174
|
+
# Build multiSend data
|
|
175
|
+
multi_send_data = b""
|
|
176
|
+
for target, value, data in zip(targets, values, datas):
|
|
177
|
+
tx_data = self._encode_multi_send_tx(target, value, data)
|
|
178
|
+
multi_send_data += tx_data
|
|
179
|
+
|
|
180
|
+
# Encode multiSend(bytes transactions)
|
|
181
|
+
# Selector: 0x8d80ff0a
|
|
182
|
+
multi_send_selector = bytes.fromhex("8d80ff0a")
|
|
183
|
+
multi_send_call = self._encode_bytes(multi_send_data)
|
|
184
|
+
multi_send_calldata = multi_send_selector + multi_send_call
|
|
185
|
+
|
|
186
|
+
# Execute via Safe 4337 module with DELEGATECALL (operation = 1)
|
|
187
|
+
selector = bytes.fromhex("541d63c8") # executeUserOp
|
|
188
|
+
operation = 1 # DELEGATECALL
|
|
189
|
+
|
|
190
|
+
encoded = self._encode_execute_user_op(
|
|
191
|
+
self.MULTI_SEND_ADDRESS,
|
|
192
|
+
0,
|
|
193
|
+
multi_send_calldata,
|
|
194
|
+
operation
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return selector + encoded
|
|
198
|
+
|
|
199
|
+
def _build_initializer(self) -> bytes:
|
|
200
|
+
"""Build the Safe setup initializer data."""
|
|
201
|
+
# Safe.setup(
|
|
202
|
+
# address[] _owners,
|
|
203
|
+
# uint256 _threshold,
|
|
204
|
+
# address to,
|
|
205
|
+
# bytes data,
|
|
206
|
+
# address fallbackHandler,
|
|
207
|
+
# address paymentToken,
|
|
208
|
+
# uint256 payment,
|
|
209
|
+
# address paymentReceiver
|
|
210
|
+
# )
|
|
211
|
+
# Selector: 0xb63e800d
|
|
212
|
+
|
|
213
|
+
selector = bytes.fromhex("b63e800d")
|
|
214
|
+
|
|
215
|
+
owners = [self.owner_address]
|
|
216
|
+
threshold = self.threshold
|
|
217
|
+
|
|
218
|
+
# to = AddModulesLib to enable 4337 module
|
|
219
|
+
to_address = SAFE_4337_ADDRESSES["add_modules_lib"]
|
|
220
|
+
|
|
221
|
+
# data = enableModules([Safe4337Module])
|
|
222
|
+
module_setup_data = self._encode_enable_modules([SAFE_4337_ADDRESSES["module"]])
|
|
223
|
+
|
|
224
|
+
fallback_handler = SAFE_4337_ADDRESSES["fallback_handler"]
|
|
225
|
+
payment_token = "0x" + "00" * 20
|
|
226
|
+
payment = 0
|
|
227
|
+
payment_receiver = "0x" + "00" * 20
|
|
228
|
+
|
|
229
|
+
encoded = self._encode_setup(
|
|
230
|
+
owners,
|
|
231
|
+
threshold,
|
|
232
|
+
to_address,
|
|
233
|
+
module_setup_data,
|
|
234
|
+
fallback_handler,
|
|
235
|
+
payment_token,
|
|
236
|
+
payment,
|
|
237
|
+
payment_receiver
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return selector + encoded
|
|
241
|
+
|
|
242
|
+
def _calculate_salt(self) -> bytes:
|
|
243
|
+
"""Calculate the CREATE2 salt."""
|
|
244
|
+
# Salt = keccak256(keccak256(initializer) ++ saltNonce)
|
|
245
|
+
initializer = self._build_initializer()
|
|
246
|
+
init_hash = keccak(initializer)
|
|
247
|
+
|
|
248
|
+
salt_bytes = self.salt.to_bytes(32, 'big')
|
|
249
|
+
salt_data = init_hash + salt_bytes
|
|
250
|
+
|
|
251
|
+
return keccak(salt_data)
|
|
252
|
+
|
|
253
|
+
def _get_proxy_creation_code(self) -> bytes:
|
|
254
|
+
"""Get the proxy creation code."""
|
|
255
|
+
# This is simplified - actual implementation would use the real Safe proxy bytecode
|
|
256
|
+
singleton = bytes.fromhex(SAFE_4337_ADDRESSES["singleton"][2:])
|
|
257
|
+
|
|
258
|
+
# Simplified proxy creation code
|
|
259
|
+
# In production, this should match the actual Safe proxy bytecode
|
|
260
|
+
code = bytes([0x60, 0x20]) # PUSH1 0x20
|
|
261
|
+
code += singleton
|
|
262
|
+
|
|
263
|
+
return code
|
|
264
|
+
|
|
265
|
+
def _get_safe_user_op_hash(self, user_op_hash: bytes) -> bytes:
|
|
266
|
+
"""Create the Safe-specific user op hash for signing."""
|
|
267
|
+
# For EOA owners, we just return the userOpHash directly
|
|
268
|
+
# The Safe module verifies using ecrecover
|
|
269
|
+
return user_op_hash
|
|
270
|
+
|
|
271
|
+
def _encode_create_proxy_with_nonce(
|
|
272
|
+
self,
|
|
273
|
+
singleton: bytes,
|
|
274
|
+
initializer: bytes,
|
|
275
|
+
salt_nonce: int
|
|
276
|
+
) -> bytes:
|
|
277
|
+
"""ABI encode createProxyWithNonce parameters."""
|
|
278
|
+
# (address, bytes, uint256)
|
|
279
|
+
result = b""
|
|
280
|
+
|
|
281
|
+
# singleton address (padded to 32 bytes)
|
|
282
|
+
result += singleton.rjust(32, b"\x00")
|
|
283
|
+
|
|
284
|
+
# offset to initializer bytes (96)
|
|
285
|
+
result += (96).to_bytes(32, 'big')
|
|
286
|
+
|
|
287
|
+
# saltNonce
|
|
288
|
+
result += salt_nonce.to_bytes(32, 'big')
|
|
289
|
+
|
|
290
|
+
# initializer bytes
|
|
291
|
+
result += self._encode_bytes(initializer)
|
|
292
|
+
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
def _encode_execute_user_op(
|
|
296
|
+
self,
|
|
297
|
+
to: str,
|
|
298
|
+
value: int,
|
|
299
|
+
data: bytes,
|
|
300
|
+
operation: int
|
|
301
|
+
) -> bytes:
|
|
302
|
+
"""ABI encode executeUserOp parameters."""
|
|
303
|
+
# (address, uint256, bytes, uint8)
|
|
304
|
+
result = b""
|
|
305
|
+
|
|
306
|
+
# to address
|
|
307
|
+
to_bytes = bytes.fromhex(to[2:]) if to.startswith("0x") else bytes.fromhex(to)
|
|
308
|
+
result += to_bytes.rjust(32, b"\x00")
|
|
309
|
+
|
|
310
|
+
# value
|
|
311
|
+
result += value.to_bytes(32, 'big')
|
|
312
|
+
|
|
313
|
+
# offset to data bytes (128)
|
|
314
|
+
result += (128).to_bytes(32, 'big')
|
|
315
|
+
|
|
316
|
+
# operation
|
|
317
|
+
result += operation.to_bytes(32, 'big')
|
|
318
|
+
|
|
319
|
+
# data bytes
|
|
320
|
+
result += self._encode_bytes(data)
|
|
321
|
+
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
def _encode_bytes(self, data: bytes) -> bytes:
|
|
325
|
+
"""ABI encode bytes type."""
|
|
326
|
+
# length + padded data
|
|
327
|
+
length = len(data)
|
|
328
|
+
padded_length = ((length + 31) // 32) * 32
|
|
329
|
+
|
|
330
|
+
result = length.to_bytes(32, 'big')
|
|
331
|
+
result += data.ljust(padded_length, b"\x00")
|
|
332
|
+
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
def _encode_setup(
|
|
336
|
+
self,
|
|
337
|
+
owners: List[str],
|
|
338
|
+
threshold: int,
|
|
339
|
+
to: str,
|
|
340
|
+
data: bytes,
|
|
341
|
+
fallback_handler: str,
|
|
342
|
+
payment_token: str,
|
|
343
|
+
payment: int,
|
|
344
|
+
payment_receiver: str
|
|
345
|
+
) -> bytes:
|
|
346
|
+
"""ABI encode Safe.setup parameters."""
|
|
347
|
+
result = b""
|
|
348
|
+
|
|
349
|
+
# Calculate offsets
|
|
350
|
+
owners_encoded_len = 32 + 32 * len(owners) # length + addresses
|
|
351
|
+
data_offset = 256 + owners_encoded_len
|
|
352
|
+
|
|
353
|
+
# offset to owners array (256)
|
|
354
|
+
result += (256).to_bytes(32, 'big')
|
|
355
|
+
|
|
356
|
+
# threshold
|
|
357
|
+
result += threshold.to_bytes(32, 'big')
|
|
358
|
+
|
|
359
|
+
# to
|
|
360
|
+
to_bytes = bytes.fromhex(to[2:]) if to.startswith("0x") else bytes.fromhex(to)
|
|
361
|
+
result += to_bytes.rjust(32, b"\x00")
|
|
362
|
+
|
|
363
|
+
# data offset
|
|
364
|
+
result += data_offset.to_bytes(32, 'big')
|
|
365
|
+
|
|
366
|
+
# fallbackHandler
|
|
367
|
+
fh_bytes = bytes.fromhex(fallback_handler[2:]) if fallback_handler.startswith("0x") else bytes.fromhex(fallback_handler)
|
|
368
|
+
result += fh_bytes.rjust(32, b"\x00")
|
|
369
|
+
|
|
370
|
+
# paymentToken
|
|
371
|
+
pt_bytes = bytes.fromhex(payment_token[2:]) if payment_token.startswith("0x") else bytes.fromhex(payment_token)
|
|
372
|
+
result += pt_bytes.rjust(32, b"\x00")
|
|
373
|
+
|
|
374
|
+
# payment
|
|
375
|
+
result += payment.to_bytes(32, 'big')
|
|
376
|
+
|
|
377
|
+
# paymentReceiver
|
|
378
|
+
pr_bytes = bytes.fromhex(payment_receiver[2:]) if payment_receiver.startswith("0x") else bytes.fromhex(payment_receiver)
|
|
379
|
+
result += pr_bytes.rjust(32, b"\x00")
|
|
380
|
+
|
|
381
|
+
# owners array
|
|
382
|
+
result += len(owners).to_bytes(32, 'big')
|
|
383
|
+
for owner in owners:
|
|
384
|
+
owner_bytes = bytes.fromhex(owner[2:]) if owner.startswith("0x") else bytes.fromhex(owner)
|
|
385
|
+
result += owner_bytes.rjust(32, b"\x00")
|
|
386
|
+
|
|
387
|
+
# data bytes
|
|
388
|
+
result += self._encode_bytes(data)
|
|
389
|
+
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
def _encode_enable_modules(self, modules: List[str]) -> bytes:
|
|
393
|
+
"""ABI encode enableModules call."""
|
|
394
|
+
# enableModules(address[])
|
|
395
|
+
# Selector: 0xa3f4df7e
|
|
396
|
+
selector = bytes.fromhex("a3f4df7e")
|
|
397
|
+
|
|
398
|
+
# offset to array (32)
|
|
399
|
+
encoded = (32).to_bytes(32, 'big')
|
|
400
|
+
|
|
401
|
+
# array length
|
|
402
|
+
encoded += len(modules).to_bytes(32, 'big')
|
|
403
|
+
|
|
404
|
+
# array elements
|
|
405
|
+
for module in modules:
|
|
406
|
+
module_bytes = bytes.fromhex(module[2:]) if module.startswith("0x") else bytes.fromhex(module)
|
|
407
|
+
encoded += module_bytes.rjust(32, b"\x00")
|
|
408
|
+
|
|
409
|
+
return selector + encoded
|
|
410
|
+
|
|
411
|
+
def _encode_multi_send_tx(self, to: str, value: int, data: bytes) -> bytes:
|
|
412
|
+
"""Encode a single transaction for multiSend."""
|
|
413
|
+
# operation (1) + to (20) + value (32) + dataLength (32) + data
|
|
414
|
+
result = bytes([0]) # CALL operation
|
|
415
|
+
|
|
416
|
+
to_bytes = bytes.fromhex(to[2:]) if to.startswith("0x") else bytes.fromhex(to)
|
|
417
|
+
result += to_bytes
|
|
418
|
+
|
|
419
|
+
result += value.to_bytes(32, 'big')
|
|
420
|
+
result += len(data).to_bytes(32, 'big')
|
|
421
|
+
result += data
|
|
422
|
+
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def create_smart_account(
|
|
427
|
+
account_type: str,
|
|
428
|
+
config: SafeAccountConfig
|
|
429
|
+
) -> SmartAccountSigner:
|
|
430
|
+
"""Factory function to create a smart account."""
|
|
431
|
+
if account_type == "safe":
|
|
432
|
+
return SafeSmartAccount(config)
|
|
433
|
+
else:
|
|
434
|
+
raise SmartAccountError(f"Unknown smart account type: {account_type}")
|