eth-prototype 1.5.2__tar.gz → 1.5.3__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.
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/PKG-INFO +1 -1
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/eth_prototype.egg-info/PKG-INFO +1 -1
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/eth_prototype.egg-info/SOURCES.txt +1 -0
- eth_prototype-1.5.3/src/ethproto/aa_bundler.py +432 -0
- eth_prototype-1.5.3/src/ethproto/aa_types.py +370 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/w3wrappers.py +3 -2
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/cassettes/test_aa_bundler/test_build_user_operation_execute_user_op.yaml +3 -2
- eth_prototype-1.5.3/tests/test_aa_bundler.py +960 -0
- eth_prototype-1.5.2/src/ethproto/aa_bundler.py +0 -653
- eth_prototype-1.5.2/tests/test_aa_bundler.py +0 -389
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.coveragerc +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.github/workflows/publish.yaml +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.github/workflows/test.yaml +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.gitignore +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.isort.cfg +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.pre-commit-config.yaml +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/.readthedocs.yml +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/AUTHORS.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/CHANGELOG.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/LICENSE.txt +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/README.md +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/Makefile +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/_static/.gitignore +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/authors.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/changelog.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/conf.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/index.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/license.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/readme.rst +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/docs/requirements.txt +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/pyproject.toml +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/setup.cfg +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/setup.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/eth_prototype.egg-info/dependency_links.txt +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/eth_prototype.egg-info/not-zip-safe +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/eth_prototype.egg-info/requires.txt +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/eth_prototype.egg-info/top_level.txt +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/__init__.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/build_artifacts.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/contracts.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/defender_relay.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/test_utils/__init__.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/test_utils/factories.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/test_utils/hardhat.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/test_utils/vcr_utils.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/wadray.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/src/ethproto/wrappers.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/__init__.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/cassettes/test_aa_bundler/test_build_user_operation.yaml +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/conftest.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/README.md +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/artifacts2/TestCurrency.sol/TestCurrency.json +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/Count.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/Counter.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/CounterUpgradeable.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/CounterUpgradeableWithLibrary.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/CounterWithLibrary.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/Datatypes.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/EventLauncher.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/TestCurrency.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/TestCurrencyUUPS.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/contracts/TestNFT.sol +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/hardhat.config.js +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/package-lock.json +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/package.json +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/verifiable-binaries/@anotherOrg/aPkg/1.0.2/build/contracts/TestCurrency.sol/TestCurrency.json +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/verifiable-binaries/@org/pkg/0.2.1/build/contracts/TestCurrency.json +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/hardhat-project/verifiable-binaries/@org/pkg/0.3.0/build/contracts/TestCurrency.json +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/test_build_artifacts.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/test_contracts.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/test_defender.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/test_time_control.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/test_w3.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tests/test_wadray.py +0 -0
- {eth_prototype-1.5.2 → eth_prototype-1.5.3}/tox.ini +0 -0
|
@@ -30,6 +30,7 @@ src/eth_prototype.egg-info/requires.txt
|
|
|
30
30
|
src/eth_prototype.egg-info/top_level.txt
|
|
31
31
|
src/ethproto/__init__.py
|
|
32
32
|
src/ethproto/aa_bundler.py
|
|
33
|
+
src/ethproto/aa_types.py
|
|
33
34
|
src/ethproto/build_artifacts.py
|
|
34
35
|
src/ethproto/contracts.py
|
|
35
36
|
src/ethproto/defender_relay.py
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from threading import local
|
|
7
|
+
from typing import ClassVar, Optional
|
|
8
|
+
|
|
9
|
+
from environs import Env
|
|
10
|
+
from eth_account import Account
|
|
11
|
+
from eth_typing import ChecksumAddress, HexAddress
|
|
12
|
+
from hexbytes import HexBytes
|
|
13
|
+
from requests import HTTPError
|
|
14
|
+
from web3 import Web3
|
|
15
|
+
from web3.constants import ADDRESS_ZERO
|
|
16
|
+
from web3.types import StateOverride
|
|
17
|
+
|
|
18
|
+
from .aa_types import PackedUserOperation # noqa: F401
|
|
19
|
+
from .aa_types import pack_two # noqa: F401
|
|
20
|
+
from .aa_types import ( # Import everything for backwards compatibility; Exceptions; Constants; Dataclasses; Functions
|
|
21
|
+
DUMMY_SIGNATURE,
|
|
22
|
+
GET_NONCE_ABI,
|
|
23
|
+
AlchemyGasAndPaymasterAndData,
|
|
24
|
+
BundlerError,
|
|
25
|
+
BundlerRevertError,
|
|
26
|
+
GasPrice,
|
|
27
|
+
NonceError,
|
|
28
|
+
PaymasterAndData,
|
|
29
|
+
Tx,
|
|
30
|
+
UserOperation,
|
|
31
|
+
UserOpEstimation,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
env = Env()
|
|
35
|
+
|
|
36
|
+
AA_BUNDLER_URL = env.str("AA_BUNDLER_URL", env.str("WEB3_PROVIDER_URI", None))
|
|
37
|
+
AA_BUNDLER_SENDER = env.str("AA_BUNDLER_SENDER", None)
|
|
38
|
+
AA_BUNDLER_ENTRYPOINT = env.str("AA_BUNDLER_ENTRYPOINT", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
|
|
39
|
+
AA_BUNDLER_EXECUTOR_PK = env.str("AA_BUNDLER_EXECUTOR_PK", None)
|
|
40
|
+
AA_BUNDLER_PROVIDER = env.str("AA_BUNDLER_PROVIDER", "generic")
|
|
41
|
+
AA_BUNDLER_GAS_LIMIT_FACTOR = env.float("AA_BUNDLER_GAS_LIMIT_FACTOR", 1)
|
|
42
|
+
AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR", 1)
|
|
43
|
+
AA_BUNDLER_BASE_GAS_PRICE_FACTOR = env.float("AA_BUNDLER_BASE_GAS_PRICE_FACTOR", 1)
|
|
44
|
+
AA_BUNDLER_VERIFICATION_GAS_FACTOR = env.float("AA_BUNDLER_VERIFICATION_GAS_FACTOR", 1)
|
|
45
|
+
AA_BUNDLER_MAX_FEE_PER_GAS = env.int("AA_BUNDLER_MAX_FEE_PER_GAS", 200000000000) # 200 gwei
|
|
46
|
+
|
|
47
|
+
AA_BUNDLER_STATE_OVERRIDES = env.json("AA_BUNDLER_STATE_OVERRIDES", default={})
|
|
48
|
+
|
|
49
|
+
NonceMode = Enum(
|
|
50
|
+
"NonceMode",
|
|
51
|
+
[
|
|
52
|
+
"RANDOM_KEY", # first time initializes a random key and increments nonce locally with calling the blockchain
|
|
53
|
+
"RANDOM_KEY_EVERYTIME", # initializes a random key every time and increments nonce locally
|
|
54
|
+
"FIXED_KEY_LOCAL_NONCE", # uses a fixed key, keeps nonce locally and fetches the nonce when receiving
|
|
55
|
+
# 'AA25 invalid account nonce'
|
|
56
|
+
"FIXED_KEY_FETCH_ALWAYS", # uses a fixed key, always fetches unless received as parameter
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
AA_BUNDLER_NONCE_MODE = env.enum("AA_BUNDLER_NONCE_MODE", default="FIXED_KEY_LOCAL_NONCE", enum=NonceMode)
|
|
61
|
+
AA_BUNDLER_NONCE_KEY = env.int("AA_BUNDLER_NONCE_KEY", 0)
|
|
62
|
+
AA_BUNDLER_ALCHEMY_GAS_POLICY_ID = env.str("AA_BUNDLER_ALCHEMY_GAS_POLICY_ID", None)
|
|
63
|
+
AA_BUNDLER_USE_EXECUTE_USER_OP = env.bool("AA_BUNDLER_USE_EXECUTE_USER_OP", False)
|
|
64
|
+
|
|
65
|
+
NONCE_CACHE = defaultdict(lambda: 0)
|
|
66
|
+
RANDOM_NONCE_KEY = local()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _to_uint(x):
|
|
70
|
+
if isinstance(x, str):
|
|
71
|
+
return int(x, 16)
|
|
72
|
+
elif isinstance(x, int):
|
|
73
|
+
return x
|
|
74
|
+
raise RuntimeError(f"Invalid int value {x}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def make_nonce(nonce_key, nonce):
|
|
78
|
+
nonce_key = _to_uint(nonce_key)
|
|
79
|
+
nonce = _to_uint(nonce)
|
|
80
|
+
return (nonce_key << 64) | nonce
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fetch_nonce(w3, account, entrypoint, nonce_key):
|
|
84
|
+
ep = w3.eth.contract(abi=GET_NONCE_ABI, address=entrypoint)
|
|
85
|
+
return ep.functions.getNonce(account, nonce_key).call()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_random_nonce_key(force=False):
|
|
89
|
+
if force or getattr(RANDOM_NONCE_KEY, "key", None) is None:
|
|
90
|
+
RANDOM_NONCE_KEY.key = random.randint(1, 2**192 - 1)
|
|
91
|
+
return RANDOM_NONCE_KEY.key
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def consume_nonce(nonce_key, nonce):
|
|
95
|
+
NONCE_CACHE[nonce_key] = max(NONCE_CACHE[nonce_key], nonce + 1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def is_nonce_error(resp):
|
|
99
|
+
"""Check if a bundler response contains an AA25 nonce error."""
|
|
100
|
+
return "error" in resp and "AA25" in resp["error"]["message"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_sender(tx):
|
|
104
|
+
if tx.from_ == ADDRESS_ZERO:
|
|
105
|
+
if AA_BUNDLER_SENDER is None:
|
|
106
|
+
raise RuntimeError("Must define AA_BUNDLER_SENDER or send 'from' in the TX")
|
|
107
|
+
return AA_BUNDLER_SENDER
|
|
108
|
+
else:
|
|
109
|
+
return tx.from_
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Bundler:
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
w3: Web3,
|
|
116
|
+
bundler_url: str = AA_BUNDLER_URL,
|
|
117
|
+
bundler_type: str = AA_BUNDLER_PROVIDER,
|
|
118
|
+
entrypoint: HexAddress = AA_BUNDLER_ENTRYPOINT,
|
|
119
|
+
nonce_mode: NonceMode = AA_BUNDLER_NONCE_MODE,
|
|
120
|
+
fixed_nonce_key: int = AA_BUNDLER_NONCE_KEY,
|
|
121
|
+
verification_gas_factor: float = AA_BUNDLER_VERIFICATION_GAS_FACTOR,
|
|
122
|
+
gas_limit_factor: float = AA_BUNDLER_GAS_LIMIT_FACTOR,
|
|
123
|
+
priority_gas_price_factor: float = AA_BUNDLER_PRIORITY_GAS_PRICE_FACTOR,
|
|
124
|
+
base_gas_price_factor: float = AA_BUNDLER_BASE_GAS_PRICE_FACTOR,
|
|
125
|
+
max_fee_per_gas: int = AA_BUNDLER_MAX_FEE_PER_GAS,
|
|
126
|
+
executor_pk: HexBytes = AA_BUNDLER_EXECUTOR_PK,
|
|
127
|
+
overrides: StateOverride = AA_BUNDLER_STATE_OVERRIDES,
|
|
128
|
+
use_execute_user_op: bool = AA_BUNDLER_USE_EXECUTE_USER_OP,
|
|
129
|
+
):
|
|
130
|
+
self.w3 = w3
|
|
131
|
+
self.bundler_w3 = Web3(Web3.HTTPProvider(bundler_url), middleware=[]) if bundler_url else w3
|
|
132
|
+
self.bundler_type = bundler_type
|
|
133
|
+
self.entrypoint = entrypoint
|
|
134
|
+
self.nonce_mode = nonce_mode
|
|
135
|
+
self.fixed_nonce_key = fixed_nonce_key
|
|
136
|
+
self.verification_gas_factor = verification_gas_factor
|
|
137
|
+
self.gas_limit_factor = gas_limit_factor
|
|
138
|
+
self.priority_gas_price_factor = priority_gas_price_factor
|
|
139
|
+
self.base_gas_price_factor = base_gas_price_factor
|
|
140
|
+
self.account = Account.from_key(executor_pk) if executor_pk else None
|
|
141
|
+
self.max_fee_per_gas = max_fee_per_gas
|
|
142
|
+
|
|
143
|
+
# stateOverrideSet mapping to use when calling eth_estimateUserOperationGas
|
|
144
|
+
# https://docs.alchemy.com/reference/eth-estimateuseroperationgas
|
|
145
|
+
self.overrides = overrides
|
|
146
|
+
self.use_execute_user_op = use_execute_user_op
|
|
147
|
+
|
|
148
|
+
strategy_class = GasEstimationStrategy._strategies.get(bundler_type)
|
|
149
|
+
if strategy_class is None:
|
|
150
|
+
raise BundlerError(f"Unknown bundler_type: {bundler_type}")
|
|
151
|
+
self.gas_strategy = strategy_class(self)
|
|
152
|
+
|
|
153
|
+
def __str__(self):
|
|
154
|
+
return (
|
|
155
|
+
f"Bundler(type={self.bundler_type}, entrypoint={self.entrypoint}, nonce_mode={self.nonce_mode}, "
|
|
156
|
+
f"fixed_nonce_key={self.fixed_nonce_key}, verification_gas_factor={self.verification_gas_factor}, "
|
|
157
|
+
f"gas_limit_factor={self.gas_limit_factor}, priority_gas_price_factor={self.priority_gas_price_factor}, "
|
|
158
|
+
f"base_gas_price_factor={self.base_gas_price_factor}, max_fee_per_gas={self.max_fee_per_gas}), "
|
|
159
|
+
f"use_execute_user_op={self.use_execute_user_op}, signer={self.account.address if self.account else None}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def execute_user_op_context(self) -> ChecksumAddress:
|
|
164
|
+
if self.use_execute_user_op and self.account:
|
|
165
|
+
return self.account.address
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def get_nonce_and_key(self, tx: Tx, fetch=False):
|
|
169
|
+
nonce_key = tx.nonce_key
|
|
170
|
+
nonce = tx.nonce
|
|
171
|
+
|
|
172
|
+
if nonce_key is None:
|
|
173
|
+
if self.nonce_mode == NonceMode.RANDOM_KEY:
|
|
174
|
+
nonce_key = get_random_nonce_key()
|
|
175
|
+
elif self.nonce_mode == NonceMode.RANDOM_KEY_EVERYTIME:
|
|
176
|
+
nonce_key = get_random_nonce_key(force=True)
|
|
177
|
+
else:
|
|
178
|
+
nonce_key = self.fixed_nonce_key
|
|
179
|
+
|
|
180
|
+
if nonce is None:
|
|
181
|
+
if fetch or self.nonce_mode == NonceMode.FIXED_KEY_FETCH_ALWAYS:
|
|
182
|
+
nonce = fetch_nonce(self.w3, get_sender(tx), self.entrypoint, nonce_key)
|
|
183
|
+
else:
|
|
184
|
+
nonce = NONCE_CACHE[nonce_key]
|
|
185
|
+
return nonce_key, nonce
|
|
186
|
+
|
|
187
|
+
def build_user_operation(self, tx: Tx, enable_cap=True) -> UserOperation:
|
|
188
|
+
nonce_key, nonce = self.get_nonce_and_key(tx)
|
|
189
|
+
consume_nonce(nonce_key, nonce)
|
|
190
|
+
|
|
191
|
+
user_operation = UserOperation.from_tx(
|
|
192
|
+
tx, make_nonce(nonce_key, nonce), execute_user_op_context=self.execute_user_op_context
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
estimation = self.gas_strategy.estimate_gas_limits(user_operation)
|
|
196
|
+
user_operation = user_operation.add_estimation(estimation)
|
|
197
|
+
|
|
198
|
+
gas_price = self.gas_strategy.estimate_gas_price(user_operation)
|
|
199
|
+
if enable_cap:
|
|
200
|
+
gas_price = replace(
|
|
201
|
+
gas_price,
|
|
202
|
+
max_fee_per_gas=min(gas_price.max_fee_per_gas, self.max_fee_per_gas),
|
|
203
|
+
)
|
|
204
|
+
user_operation = user_operation.add_gas_price(gas_price)
|
|
205
|
+
|
|
206
|
+
paymaster_and_data = self.gas_strategy.estimate_paymaster(user_operation)
|
|
207
|
+
if paymaster_and_data is not None:
|
|
208
|
+
user_operation = user_operation.add_paymaster_and_data(paymaster_and_data)
|
|
209
|
+
|
|
210
|
+
return user_operation
|
|
211
|
+
|
|
212
|
+
def send_transaction(self, tx: Tx):
|
|
213
|
+
user_operation = self.build_user_operation(tx).sign(self.account.key, tx.chain_id, self.entrypoint)
|
|
214
|
+
return self.send_user_operation(user_operation)
|
|
215
|
+
|
|
216
|
+
def send_user_operation(self, user_operation: UserOperation):
|
|
217
|
+
resp = self.bundler_w3.provider.make_request(
|
|
218
|
+
"eth_sendUserOperation", [user_operation.as_dict(), self.entrypoint]
|
|
219
|
+
)
|
|
220
|
+
if "error" in resp:
|
|
221
|
+
if is_nonce_error(resp):
|
|
222
|
+
raise NonceError(resp["error"]["message"], userop=user_operation, response=resp)
|
|
223
|
+
raise BundlerRevertError(resp["error"]["message"], userop=user_operation, response=resp)
|
|
224
|
+
return {"userOpHash": resp["result"]}
|
|
225
|
+
|
|
226
|
+
def get_user_operation(self, user_op_hash):
|
|
227
|
+
resp = self.bundler_w3.provider.make_request("eth_getUserOperationByHash", [user_op_hash])
|
|
228
|
+
if "error" in resp:
|
|
229
|
+
raise BundlerRevertError(resp["error"]["message"], response=resp)
|
|
230
|
+
return resp["result"]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class GasEstimationStrategy(ABC):
|
|
234
|
+
_strategies: ClassVar[dict] = {}
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def register(cls, name: str):
|
|
238
|
+
def decorator(strategy_cls):
|
|
239
|
+
cls._strategies[name] = strategy_cls
|
|
240
|
+
return strategy_cls
|
|
241
|
+
|
|
242
|
+
return decorator
|
|
243
|
+
|
|
244
|
+
def __init__(self, bundler: "Bundler", **kwargs):
|
|
245
|
+
self.bundler = bundler
|
|
246
|
+
|
|
247
|
+
def _estimate_user_operation_gas(self, user_operation: UserOperation) -> UserOpEstimation:
|
|
248
|
+
resp = self.bundler.bundler_w3.provider.make_request(
|
|
249
|
+
"eth_estimateUserOperationGas",
|
|
250
|
+
[user_operation.as_dict(), self.bundler.entrypoint, self.bundler.overrides],
|
|
251
|
+
)
|
|
252
|
+
if "error" in resp:
|
|
253
|
+
raise BundlerRevertError(resp["error"]["message"], user_operation, resp)
|
|
254
|
+
|
|
255
|
+
paymaster_verification_gas_limit = resp["result"].get("paymasterVerificationGasLimit", "0x00")
|
|
256
|
+
return UserOpEstimation(
|
|
257
|
+
pre_verification_gas=int(resp["result"].get("preVerificationGas", "0x00"), 16),
|
|
258
|
+
verification_gas_limit=int(
|
|
259
|
+
int(resp["result"].get("verificationGasLimit", "0x00"), 16)
|
|
260
|
+
* self.bundler.verification_gas_factor
|
|
261
|
+
),
|
|
262
|
+
call_gas_limit=int(
|
|
263
|
+
int(resp["result"].get("callGasLimit", "0x00"), 16) * self.bundler.gas_limit_factor
|
|
264
|
+
),
|
|
265
|
+
paymaster_verification_gas_limit=(
|
|
266
|
+
int(paymaster_verification_gas_limit, 16)
|
|
267
|
+
if paymaster_verification_gas_limit is not None
|
|
268
|
+
else 0
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _get_base_fee(self) -> int:
|
|
273
|
+
blk = self.bundler.w3.eth.get_block("latest")
|
|
274
|
+
return int(_to_uint(blk["baseFeePerGas"]) * self.bundler.base_gas_price_factor)
|
|
275
|
+
|
|
276
|
+
@abstractmethod
|
|
277
|
+
def estimate_gas_limits(self, user_operation: UserOperation) -> UserOpEstimation:
|
|
278
|
+
...
|
|
279
|
+
|
|
280
|
+
@abstractmethod
|
|
281
|
+
def estimate_gas_price(self, user_operation: UserOperation) -> GasPrice:
|
|
282
|
+
...
|
|
283
|
+
|
|
284
|
+
def estimate_paymaster(self, user_operation: UserOperation) -> Optional[PaymasterAndData]:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@GasEstimationStrategy.register("generic")
|
|
289
|
+
class GenericGasStrategy(GasEstimationStrategy):
|
|
290
|
+
def estimate_gas_limits(self, user_operation: UserOperation) -> UserOpEstimation:
|
|
291
|
+
return self._estimate_user_operation_gas(user_operation)
|
|
292
|
+
|
|
293
|
+
def estimate_gas_price(self, user_operation: UserOperation) -> GasPrice:
|
|
294
|
+
base_fee = self._get_base_fee()
|
|
295
|
+
priority_fee = self.bundler.w3.eth.max_priority_fee
|
|
296
|
+
max_priority_fee_per_gas = int(priority_fee * self.bundler.priority_gas_price_factor)
|
|
297
|
+
max_fee_per_gas = max_priority_fee_per_gas + base_fee
|
|
298
|
+
return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@GasEstimationStrategy.register("pimlico")
|
|
302
|
+
class PimlicoGasStrategy(GasEstimationStrategy):
|
|
303
|
+
def estimate_gas_limits(self, user_operation: UserOperation) -> UserOpEstimation:
|
|
304
|
+
return self._estimate_user_operation_gas(user_operation)
|
|
305
|
+
|
|
306
|
+
def estimate_gas_price(self, user_operation: UserOperation) -> GasPrice:
|
|
307
|
+
resp = self.bundler.bundler_w3.provider.make_request("pimlico_getUserOperationGasPrice", [])
|
|
308
|
+
if "error" in resp:
|
|
309
|
+
raise BundlerRevertError(resp["error"]["message"], response=resp)
|
|
310
|
+
# {
|
|
311
|
+
# "jsonrpc": "2.0",
|
|
312
|
+
# "id": 1,
|
|
313
|
+
# "result": {
|
|
314
|
+
# "slow": {
|
|
315
|
+
# "maxFeePerGas": "0x829b42b5",
|
|
316
|
+
# "maxPriorityFeePerGas": "0x829b42b5"
|
|
317
|
+
# },
|
|
318
|
+
# "standard": {
|
|
319
|
+
# "maxFeePerGas": "0x88d36a75",
|
|
320
|
+
# "maxPriorityFeePerGas": "0x88d36a75"
|
|
321
|
+
# },
|
|
322
|
+
# "fast": {
|
|
323
|
+
# "maxFeePerGas": "0x8f0b9234",
|
|
324
|
+
# "maxPriorityFeePerGas": "0x8f0b9234"
|
|
325
|
+
# }
|
|
326
|
+
# }
|
|
327
|
+
# }
|
|
328
|
+
priority_fee = int(resp["result"]["standard"]["maxPriorityFeePerGas"], 16)
|
|
329
|
+
total_fee = int(resp["result"]["standard"]["maxFeePerGas"], 16)
|
|
330
|
+
base_fee = total_fee - priority_fee
|
|
331
|
+
max_priority_fee_per_gas = int(priority_fee * self.bundler.priority_gas_price_factor)
|
|
332
|
+
max_fee_per_gas = max_priority_fee_per_gas + base_fee
|
|
333
|
+
return GasPrice(max_priority_fee_per_gas=max_priority_fee_per_gas, max_fee_per_gas=max_fee_per_gas)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@GasEstimationStrategy.register("alchemy")
|
|
337
|
+
class AlchemyGasStrategy(GasEstimationStrategy):
|
|
338
|
+
def __init__(self, bundler: "Bundler", **kwargs):
|
|
339
|
+
super().__init__(bundler)
|
|
340
|
+
gas_policy_id = kwargs.pop("gas_policy_id", AA_BUNDLER_ALCHEMY_GAS_POLICY_ID)
|
|
341
|
+
if gas_policy_id is None:
|
|
342
|
+
raise BundlerError("Must provide alchemy_gas_policy_id when using alchemy bundler_type")
|
|
343
|
+
self._gas_policy_id = gas_policy_id
|
|
344
|
+
self._cached_result: Optional[AlchemyGasAndPaymasterAndData] = None
|
|
345
|
+
|
|
346
|
+
def _get_estimation(self, user_operation: UserOperation) -> AlchemyGasAndPaymasterAndData:
|
|
347
|
+
if self._cached_result is None:
|
|
348
|
+
self._cached_result = self._alchemy_estimation(user_operation)
|
|
349
|
+
return self._cached_result
|
|
350
|
+
|
|
351
|
+
def _alchemy_estimation(self, user_operation: UserOperation) -> AlchemyGasAndPaymasterAndData:
|
|
352
|
+
try:
|
|
353
|
+
resp = self.bundler.bundler_w3.provider.make_request(
|
|
354
|
+
"alchemy_requestGasAndPaymasterAndData",
|
|
355
|
+
[
|
|
356
|
+
{
|
|
357
|
+
"policyId": self._gas_policy_id,
|
|
358
|
+
"entryPoint": self.bundler.entrypoint,
|
|
359
|
+
"dummySignature": DUMMY_SIGNATURE,
|
|
360
|
+
"userOperation": user_operation.as_reduced_dict(),
|
|
361
|
+
"overrides": {
|
|
362
|
+
"maxFeePerGas": {"multiplier": self.bundler.base_gas_price_factor},
|
|
363
|
+
"maxPriorityFeePerGas": {"multiplier": self.bundler.priority_gas_price_factor},
|
|
364
|
+
"callGasLimit": {"multiplier": self.bundler.gas_limit_factor},
|
|
365
|
+
"verificationGasLimit": {"multiplier": self.bundler.verification_gas_factor},
|
|
366
|
+
},
|
|
367
|
+
# Alchemy seems to be ignoring this, even though it's documented
|
|
368
|
+
"stateOverrideSet": self.bundler.overrides,
|
|
369
|
+
}
|
|
370
|
+
],
|
|
371
|
+
)
|
|
372
|
+
except HTTPError as e:
|
|
373
|
+
raise BundlerRevertError(
|
|
374
|
+
f"HTTP error while requesting gas and paymaster data: {str(e)}",
|
|
375
|
+
userop=user_operation,
|
|
376
|
+
response=e.response.text,
|
|
377
|
+
) from e
|
|
378
|
+
|
|
379
|
+
if "error" in resp:
|
|
380
|
+
raise BundlerRevertError(resp["error"]["message"], userop=user_operation, response=resp)
|
|
381
|
+
|
|
382
|
+
# {
|
|
383
|
+
# "callGasLimit": "0x3dab",
|
|
384
|
+
# "paymasterVerificationGasLimit": "0x9afa",
|
|
385
|
+
# "paymasterPostOpGasLimit": "0x0",
|
|
386
|
+
# "verificationGasLimit": "0xac33",
|
|
387
|
+
# "maxPriorityFeePerGas": "0x7aef40a00",
|
|
388
|
+
# "paymaster": "0x2cc0c7981D846b9F2a16276556f6e8cb52BfB633",
|
|
389
|
+
# "maxFeePerGas": "0xaf9fe62e48",
|
|
390
|
+
# "paymasterData": "0xabcd...",
|
|
391
|
+
# "preVerificationGas": "0xb8ec"
|
|
392
|
+
# }
|
|
393
|
+
|
|
394
|
+
estimation = UserOpEstimation(
|
|
395
|
+
pre_verification_gas=int(resp["result"]["preVerificationGas"], 16),
|
|
396
|
+
verification_gas_limit=int(resp["result"]["verificationGasLimit"], 16),
|
|
397
|
+
call_gas_limit=int(resp["result"]["callGasLimit"], 16),
|
|
398
|
+
paymaster_verification_gas_limit=int(resp["result"]["paymasterVerificationGasLimit"], 16),
|
|
399
|
+
)
|
|
400
|
+
gas_price = GasPrice(
|
|
401
|
+
max_priority_fee_per_gas=int(resp["result"]["maxPriorityFeePerGas"], 16),
|
|
402
|
+
max_fee_per_gas=int(resp["result"]["maxFeePerGas"], 16),
|
|
403
|
+
)
|
|
404
|
+
paymaster_and_data = PaymasterAndData(
|
|
405
|
+
paymaster=resp["result"]["paymaster"],
|
|
406
|
+
paymaster_data=HexBytes(resp["result"]["paymasterData"]),
|
|
407
|
+
paymaster_verification_gas_limit=int(resp["result"]["paymasterVerificationGasLimit"], 16),
|
|
408
|
+
paymaster_post_op_gas_limit=int(resp["result"]["paymasterPostOpGasLimit"], 16),
|
|
409
|
+
)
|
|
410
|
+
return AlchemyGasAndPaymasterAndData(
|
|
411
|
+
estimation=estimation,
|
|
412
|
+
gas_price=gas_price,
|
|
413
|
+
paymaster_and_data=paymaster_and_data,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def estimate_gas_limits(self, user_operation: UserOperation) -> UserOpEstimation:
|
|
417
|
+
return self._get_estimation(user_operation).estimation
|
|
418
|
+
|
|
419
|
+
def estimate_gas_price(self, user_operation: UserOperation) -> GasPrice:
|
|
420
|
+
return self._get_estimation(user_operation).gas_price
|
|
421
|
+
|
|
422
|
+
def estimate_paymaster(self, user_operation: UserOperation) -> PaymasterAndData:
|
|
423
|
+
return self._get_estimation(user_operation).paymaster_and_data
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@GasEstimationStrategy.register("zeroprice")
|
|
427
|
+
class ZeroPriceGasStrategy(GasEstimationStrategy):
|
|
428
|
+
def estimate_gas_limits(self, user_operation: UserOperation) -> UserOpEstimation:
|
|
429
|
+
return self._estimate_user_operation_gas(user_operation)
|
|
430
|
+
|
|
431
|
+
def estimate_gas_price(self, user_operation: UserOperation) -> GasPrice:
|
|
432
|
+
return GasPrice(max_priority_fee_per_gas=0, max_fee_per_gas=0)
|