t402 1.2.0__tar.gz → 1.3.0__tar.gz

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.
Files changed (50) hide show
  1. {t402-1.2.0 → t402-1.3.0}/PKG-INFO +1 -1
  2. {t402-1.2.0 → t402-1.3.0}/pyproject.toml +1 -1
  3. {t402-1.2.0 → t402-1.3.0}/src/t402/__init__.py +48 -0
  4. t402-1.3.0/src/t402/erc4337/__init__.py +188 -0
  5. t402-1.3.0/src/t402/erc4337/accounts.py +434 -0
  6. t402-1.3.0/src/t402/erc4337/bundlers.py +458 -0
  7. t402-1.3.0/src/t402/erc4337/paymasters.py +532 -0
  8. t402-1.3.0/src/t402/erc4337/types.py +315 -0
  9. t402-1.3.0/tests/test_tron.py +440 -0
  10. {t402-1.2.0 → t402-1.3.0}/.gitignore +0 -0
  11. {t402-1.2.0 → t402-1.3.0}/.python-version +0 -0
  12. {t402-1.2.0 → t402-1.3.0}/README.md +0 -0
  13. {t402-1.2.0 → t402-1.3.0}/src/t402/chains.py +0 -0
  14. {t402-1.2.0 → t402-1.3.0}/src/t402/clients/__init__.py +0 -0
  15. {t402-1.2.0 → t402-1.3.0}/src/t402/clients/base.py +0 -0
  16. {t402-1.2.0 → t402-1.3.0}/src/t402/clients/httpx.py +0 -0
  17. {t402-1.2.0 → t402-1.3.0}/src/t402/clients/requests.py +0 -0
  18. {t402-1.2.0 → t402-1.3.0}/src/t402/common.py +0 -0
  19. {t402-1.2.0 → t402-1.3.0}/src/t402/encoding.py +0 -0
  20. {t402-1.2.0 → t402-1.3.0}/src/t402/evm_paywall_template.py +0 -0
  21. {t402-1.2.0 → t402-1.3.0}/src/t402/exact.py +0 -0
  22. {t402-1.2.0 → t402-1.3.0}/src/t402/facilitator.py +0 -0
  23. {t402-1.2.0 → t402-1.3.0}/src/t402/fastapi/__init__.py +0 -0
  24. {t402-1.2.0 → t402-1.3.0}/src/t402/fastapi/middleware.py +0 -0
  25. {t402-1.2.0 → t402-1.3.0}/src/t402/flask/__init__.py +0 -0
  26. {t402-1.2.0 → t402-1.3.0}/src/t402/flask/middleware.py +0 -0
  27. {t402-1.2.0 → t402-1.3.0}/src/t402/networks.py +0 -0
  28. {t402-1.2.0 → t402-1.3.0}/src/t402/path.py +0 -0
  29. {t402-1.2.0 → t402-1.3.0}/src/t402/paywall.py +0 -0
  30. {t402-1.2.0 → t402-1.3.0}/src/t402/py.typed +0 -0
  31. {t402-1.2.0 → t402-1.3.0}/src/t402/svm_paywall_template.py +0 -0
  32. {t402-1.2.0 → t402-1.3.0}/src/t402/ton.py +0 -0
  33. {t402-1.2.0 → t402-1.3.0}/src/t402/ton_paywall_template.py +0 -0
  34. {t402-1.2.0 → t402-1.3.0}/src/t402/tron.py +0 -0
  35. {t402-1.2.0 → t402-1.3.0}/src/t402/types.py +0 -0
  36. {t402-1.2.0 → t402-1.3.0}/tests/clients/__init__.py +0 -0
  37. {t402-1.2.0 → t402-1.3.0}/tests/clients/test_base.py +0 -0
  38. {t402-1.2.0 → t402-1.3.0}/tests/clients/test_httpx.py +0 -0
  39. {t402-1.2.0 → t402-1.3.0}/tests/clients/test_requests.py +0 -0
  40. {t402-1.2.0 → t402-1.3.0}/tests/fastapi_tests/__init__.py +0 -0
  41. {t402-1.2.0 → t402-1.3.0}/tests/fastapi_tests/test_middleware.py +0 -0
  42. {t402-1.2.0 → t402-1.3.0}/tests/flask_tests/__init__.py +0 -0
  43. {t402-1.2.0 → t402-1.3.0}/tests/flask_tests/test_middleware.py +0 -0
  44. {t402-1.2.0 → t402-1.3.0}/tests/test_common.py +0 -0
  45. {t402-1.2.0 → t402-1.3.0}/tests/test_encoding.py +0 -0
  46. {t402-1.2.0 → t402-1.3.0}/tests/test_exact.py +0 -0
  47. {t402-1.2.0 → t402-1.3.0}/tests/test_paywall.py +0 -0
  48. {t402-1.2.0 → t402-1.3.0}/tests/test_ton.py +0 -0
  49. {t402-1.2.0 → t402-1.3.0}/tests/test_types.py +0 -0
  50. {t402-1.2.0 → t402-1.3.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t402
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: t402: An internet native payments protocol
5
5
  Author-email: T402 Team <dev@t402.io>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "t402"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "t402: An internet native payments protocol"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -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
  ]
@@ -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
+ ]
@@ -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}")