fastweb3-objects 0.1.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.
- fastweb3_objects-0.1.0.dist-info/METADATA +233 -0
- fastweb3_objects-0.1.0.dist-info/RECORD +22 -0
- fastweb3_objects-0.1.0.dist-info/WHEEL +5 -0
- fastweb3_objects-0.1.0.dist-info/licenses/LICENSE +21 -0
- fastweb3_objects-0.1.0.dist-info/top_level.txt +1 -0
- fw3_objects/__init__.py +12 -0
- fw3_objects/abi.py +511 -0
- fw3_objects/account.py +407 -0
- fw3_objects/cache/__init__.py +1 -0
- fw3_objects/cache/db.py +73 -0
- fw3_objects/cache/metadata.py +89 -0
- fw3_objects/cache/rpc.py +205 -0
- fw3_objects/chain.py +317 -0
- fw3_objects/contract.py +647 -0
- fw3_objects/errors.py +92 -0
- fw3_objects/events.py +379 -0
- fw3_objects/explorers/__init__.py +1 -0
- fw3_objects/explorers/blockscout.py +83 -0
- fw3_objects/explorers/etherscan.py +96 -0
- fw3_objects/explorers/lookup.py +267 -0
- fw3_objects/monitor.py +125 -0
- fw3_objects/transaction.py +348 -0
fw3_objects/contract.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import weakref
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fw3.deferred import deferred_response
|
|
7
|
+
|
|
8
|
+
from . import abi
|
|
9
|
+
from .account import Account
|
|
10
|
+
from .cache.metadata import AddressMetadataCache
|
|
11
|
+
from .chain import Chain
|
|
12
|
+
from .errors import ABINotFound, NoActiveChain
|
|
13
|
+
from .explorers.lookup import HIGH_PRIORITY, fetch_abi
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_abi(abi):
|
|
17
|
+
if isinstance(abi, (str, Path)):
|
|
18
|
+
path = Path(abi)
|
|
19
|
+
|
|
20
|
+
if not path.exists():
|
|
21
|
+
raise FileNotFoundError(f"ABI file not found: {path}")
|
|
22
|
+
|
|
23
|
+
with path.open() as f:
|
|
24
|
+
data = json.load(f)
|
|
25
|
+
|
|
26
|
+
if isinstance(data, list):
|
|
27
|
+
abi_list = data
|
|
28
|
+
elif isinstance(data, dict) and "abi" in data:
|
|
29
|
+
abi_list = data["abi"]
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError("Invalid ABI format: expected list or dict with 'abi' key")
|
|
32
|
+
|
|
33
|
+
elif isinstance(abi, list):
|
|
34
|
+
abi_list = abi
|
|
35
|
+
|
|
36
|
+
else:
|
|
37
|
+
raise TypeError("abi must be a sequence or a path to a JSON file")
|
|
38
|
+
|
|
39
|
+
if not all(isinstance(item, dict) for item in abi_list):
|
|
40
|
+
raise ValueError("ABI must be a sequence of dicts")
|
|
41
|
+
|
|
42
|
+
return abi_list
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _method_class(method_abi: dict) -> type["_ContractMethod"]:
|
|
46
|
+
mutability = method_abi.get("stateMutability")
|
|
47
|
+
|
|
48
|
+
if mutability is None:
|
|
49
|
+
if method_abi.get("constant", False):
|
|
50
|
+
mutability = "view"
|
|
51
|
+
elif method_abi.get("payable", False):
|
|
52
|
+
mutability = "payable"
|
|
53
|
+
else:
|
|
54
|
+
mutability = "nonpayable"
|
|
55
|
+
|
|
56
|
+
if mutability in ("view", "pure"):
|
|
57
|
+
return ContractCall
|
|
58
|
+
return ContractTx
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class _ContractState:
|
|
63
|
+
chain: Chain
|
|
64
|
+
abi_job: object | None = None
|
|
65
|
+
proxy_abi: list[dict] | None = None
|
|
66
|
+
implementation: str | None = None
|
|
67
|
+
implementation_contract: object | None = None
|
|
68
|
+
refresh_abi: bool | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_RESERVED_NAMES = {"abi", "address"}
|
|
72
|
+
_CONTRACT_STATE = weakref.WeakKeyDictionary()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _install_abi(contract: "Contract", abi_list: list[dict]) -> None:
|
|
76
|
+
state = _CONTRACT_STATE[contract]
|
|
77
|
+
|
|
78
|
+
contract.abi = abi_list
|
|
79
|
+
|
|
80
|
+
function_abis = [i for i in contract.abi if i.get("type", "function") == "function"]
|
|
81
|
+
functions = {}
|
|
82
|
+
|
|
83
|
+
for method_abi in function_abis:
|
|
84
|
+
name = method_abi["name"]
|
|
85
|
+
if name in _RESERVED_NAMES:
|
|
86
|
+
raise ValueError(f"Contract ABI may not define reserved attribute {name!r}")
|
|
87
|
+
functions.setdefault(name, []).append(method_abi)
|
|
88
|
+
|
|
89
|
+
for name, method_abis in functions.items():
|
|
90
|
+
if len(method_abis) == 1:
|
|
91
|
+
method_abi = method_abis[0]
|
|
92
|
+
cls = _method_class(method_abi)
|
|
93
|
+
method = cls(address=contract.address, method_abi=method_abi, chain=state.chain)
|
|
94
|
+
else:
|
|
95
|
+
method = OverloadedMethod(
|
|
96
|
+
address=contract.address, method_abis=method_abis, chain=state.chain
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
setattr(contract, name, method)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cache_abi_result(cache, chain_id: int, address: str, result) -> None:
|
|
103
|
+
abi_list, implementation = result
|
|
104
|
+
cache.set(chain_id, address, "abi", abi_list)
|
|
105
|
+
if implementation is not None:
|
|
106
|
+
cache.set(chain_id, address, "implementation", implementation)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _normalize_implementation(implementation, chain: Chain) -> str | None:
|
|
110
|
+
if implementation is None or implementation is False:
|
|
111
|
+
return None
|
|
112
|
+
return str(Account(implementation, chain=chain))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _start_implementation_lookup(contract: "Contract", refresh_abi: bool | None) -> None:
|
|
116
|
+
state = _CONTRACT_STATE[contract]
|
|
117
|
+
if state.implementation is None or state.implementation_contract is not None:
|
|
118
|
+
return
|
|
119
|
+
state.implementation_contract = Contract(
|
|
120
|
+
state.implementation,
|
|
121
|
+
chain=state.chain,
|
|
122
|
+
refresh_abi=refresh_abi,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _resolve_abi(contract: "Contract") -> None:
|
|
127
|
+
state = _CONTRACT_STATE[contract]
|
|
128
|
+
|
|
129
|
+
if state.proxy_abi is None:
|
|
130
|
+
if state.abi_job is None:
|
|
131
|
+
raise AttributeError("abi")
|
|
132
|
+
|
|
133
|
+
state.abi_job.bump_priority(HIGH_PRIORITY)
|
|
134
|
+
try:
|
|
135
|
+
result = state.abi_job.wait()
|
|
136
|
+
except ABINotFound:
|
|
137
|
+
if state.implementation is None:
|
|
138
|
+
raise
|
|
139
|
+
state.proxy_abi = []
|
|
140
|
+
else:
|
|
141
|
+
proxy_abi, implementation = result
|
|
142
|
+
state.proxy_abi = proxy_abi
|
|
143
|
+
if state.implementation is None:
|
|
144
|
+
state.implementation = implementation
|
|
145
|
+
finally:
|
|
146
|
+
state.abi_job = None
|
|
147
|
+
|
|
148
|
+
_start_implementation_lookup(contract, state.refresh_abi)
|
|
149
|
+
|
|
150
|
+
if state.implementation is None:
|
|
151
|
+
_install_abi(contract, state.proxy_abi)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
_start_implementation_lookup(contract, state.refresh_abi)
|
|
155
|
+
implementation_abi = state.implementation_contract.abi
|
|
156
|
+
_install_abi(contract, abi.overlay_abi(implementation_abi, state.proxy_abi))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Contract:
|
|
160
|
+
"""Contract instance bound to an address and chain."""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
address: Account | str,
|
|
165
|
+
abi: list | str | Path | None = None,
|
|
166
|
+
chain: Chain | int | None = None,
|
|
167
|
+
implementation: Account | str | bool | None = None,
|
|
168
|
+
refresh_abi: bool | None = None,
|
|
169
|
+
):
|
|
170
|
+
"""Create a contract bound to an address with optional ABI resolution.
|
|
171
|
+
|
|
172
|
+
Calls always execute against ``address``. The ABI used for method dispatch may
|
|
173
|
+
come from multiple sources depending on the inputs:
|
|
174
|
+
|
|
175
|
+
- If ``abi`` is provided, it is trusted as complete and no explorer lookup or
|
|
176
|
+
proxy handling is performed.
|
|
177
|
+
- If ``implementation`` is an address, the ABI is taken from that implementation
|
|
178
|
+
and overlaid with any proxy ABI found at ``address``.
|
|
179
|
+
- If ``implementation`` is ``False``, proxy handling is disabled.
|
|
180
|
+
- Otherwise, the ABI is loaded from cache or fetched from an explorer. If the
|
|
181
|
+
contract is identified as a proxy, the implementation ABI is used and overlaid
|
|
182
|
+
with the proxy ABI.
|
|
183
|
+
|
|
184
|
+
Explorer lookups are asynchronous. The constructor returns immediately, and the
|
|
185
|
+
ABI is installed on first access. Until then, the ``abi`` attribute may not be
|
|
186
|
+
present.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
address: Contract address to execute calls against.
|
|
190
|
+
abi: ABI list or path to a JSON ABI file.
|
|
191
|
+
chain: Chain or chain ID. Uses the active default chain if omitted.
|
|
192
|
+
implementation: Proxy override. Address forces an implementation,
|
|
193
|
+
``False`` disables proxy handling, and ``None`` enables auto-detection.
|
|
194
|
+
refresh_abi: Cache control. ``True`` forces refresh, ``False`` uses cache
|
|
195
|
+
only, and ``None`` uses cache then falls back to explorer.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
NoActiveChain: If no chain is available.
|
|
199
|
+
FileNotFoundError: If an ABI path does not exist.
|
|
200
|
+
TypeError: If ``abi`` has an unsupported type.
|
|
201
|
+
ValueError: If the ABI format is invalid.
|
|
202
|
+
"""
|
|
203
|
+
if chain is None:
|
|
204
|
+
chain, _ = Chain._get_default_chain()
|
|
205
|
+
if chain is None:
|
|
206
|
+
raise NoActiveChain("No chain specified for Contract")
|
|
207
|
+
|
|
208
|
+
chain = Chain(chain)
|
|
209
|
+
self.address = Account(address, chain=chain)
|
|
210
|
+
_CONTRACT_STATE[self] = _ContractState(chain=chain, refresh_abi=refresh_abi)
|
|
211
|
+
|
|
212
|
+
cache = AddressMetadataCache()
|
|
213
|
+
|
|
214
|
+
if abi is not None:
|
|
215
|
+
abi_list = _load_abi(abi)
|
|
216
|
+
_install_abi(self, abi_list)
|
|
217
|
+
|
|
218
|
+
if refresh_abi is True:
|
|
219
|
+
cache.set(chain.id, str(self.address), "abi", abi_list)
|
|
220
|
+
elif refresh_abi is None and cache.get(chain.id, str(self.address), "abi") is None:
|
|
221
|
+
cache.set(chain.id, str(self.address), "abi", abi_list)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
state = _CONTRACT_STATE[self]
|
|
225
|
+
forced_implementation = _normalize_implementation(implementation, chain)
|
|
226
|
+
resolve_proxy = implementation is None
|
|
227
|
+
|
|
228
|
+
if refresh_abi is not True:
|
|
229
|
+
cached_abi = cache.get(chain.id, str(self.address), "abi")
|
|
230
|
+
cached_implementation = forced_implementation
|
|
231
|
+
if cached_implementation is None and implementation is None:
|
|
232
|
+
cached_implementation = cache.get(chain.id, str(self.address), "implementation")
|
|
233
|
+
|
|
234
|
+
if cached_abi is not None:
|
|
235
|
+
if cached_implementation is None:
|
|
236
|
+
_install_abi(self, _load_abi(cached_abi))
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
state.proxy_abi = _load_abi(cached_abi)
|
|
240
|
+
state.implementation = cached_implementation
|
|
241
|
+
_start_implementation_lookup(self, refresh_abi)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
if refresh_abi is False:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
state.implementation = forced_implementation
|
|
248
|
+
if state.implementation is not None:
|
|
249
|
+
_start_implementation_lookup(self, refresh_abi)
|
|
250
|
+
|
|
251
|
+
def on_success(result):
|
|
252
|
+
_cache_abi_result(cache, chain.id, str(self.address), result)
|
|
253
|
+
_, implementation = result
|
|
254
|
+
if state.implementation is None and implementation is not None:
|
|
255
|
+
state.implementation = implementation
|
|
256
|
+
_start_implementation_lookup(self, refresh_abi)
|
|
257
|
+
|
|
258
|
+
state.abi_job = fetch_abi(
|
|
259
|
+
chain.id,
|
|
260
|
+
str(self.address),
|
|
261
|
+
ignore_negative_cache=refresh_abi is True,
|
|
262
|
+
resolve_proxy=resolve_proxy,
|
|
263
|
+
on_success=on_success,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def __str__(self):
|
|
267
|
+
return str(self.address)
|
|
268
|
+
|
|
269
|
+
def __getattr__(self, name: str):
|
|
270
|
+
state = _CONTRACT_STATE[self]
|
|
271
|
+
if state.abi_job is None and state.proxy_abi is None:
|
|
272
|
+
raise AttributeError(name)
|
|
273
|
+
|
|
274
|
+
_resolve_abi(self)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
return object.__getattribute__(self, name)
|
|
278
|
+
except AttributeError:
|
|
279
|
+
raise AttributeError(name) from None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class _ContractMethod:
|
|
283
|
+
def __init__(self, address: Account, method_abi: dict, chain: Chain):
|
|
284
|
+
self.address = address
|
|
285
|
+
self.chain = chain
|
|
286
|
+
self.method_abi = method_abi
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def signature(self) -> str:
|
|
290
|
+
"""Return the canonical function signature."""
|
|
291
|
+
return abi.function_signature(self.method_abi)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def selector(self) -> bytes:
|
|
295
|
+
"""Return the four-byte function selector."""
|
|
296
|
+
return abi.function_selector(self.method_abi)
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def mutability(self) -> str:
|
|
300
|
+
"""Return the function state mutability."""
|
|
301
|
+
if "stateMutability" in self.method_abi:
|
|
302
|
+
return self.method_abi["stateMutability"]
|
|
303
|
+
|
|
304
|
+
constant = self.method_abi.get("constant", False)
|
|
305
|
+
payable = self.method_abi.get("payable", False)
|
|
306
|
+
|
|
307
|
+
if constant:
|
|
308
|
+
return "view"
|
|
309
|
+
if payable:
|
|
310
|
+
return "payable"
|
|
311
|
+
return "nonpayable"
|
|
312
|
+
|
|
313
|
+
def call(
|
|
314
|
+
self,
|
|
315
|
+
*args,
|
|
316
|
+
sender: Account = None,
|
|
317
|
+
value: int | str | None = None,
|
|
318
|
+
gas_limit: int | str | None = None,
|
|
319
|
+
block_identifier: str | int | None = None,
|
|
320
|
+
):
|
|
321
|
+
"""Execute the function with ``eth_call``.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
*args: Contract function arguments.
|
|
325
|
+
sender: Optional account to use as ``msg.sender``. Uses the zero address when
|
|
326
|
+
omitted.
|
|
327
|
+
value: Call value in wei.
|
|
328
|
+
gas_limit: Optional gas limit.
|
|
329
|
+
block_identifier: Optional block number or tag.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Decoded return value.
|
|
333
|
+
"""
|
|
334
|
+
if sender is None:
|
|
335
|
+
sender = Account("0x0000000000000000000000000000000000000000")
|
|
336
|
+
data = self.encode_input(*args)
|
|
337
|
+
resp = sender.call(
|
|
338
|
+
to=str(self.address),
|
|
339
|
+
value=value,
|
|
340
|
+
data=data,
|
|
341
|
+
gas_limit=gas_limit,
|
|
342
|
+
chain=self.chain,
|
|
343
|
+
block_identifier=block_identifier,
|
|
344
|
+
)
|
|
345
|
+
return deferred_response(None, ref_func=lambda h: h.set_value(self.decode_output(resp)))
|
|
346
|
+
|
|
347
|
+
def estimate_gas(self, *args, sender: Account, value: int | str | None = None):
|
|
348
|
+
"""Estimate gas for this contract function.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
*args: Contract function arguments.
|
|
352
|
+
sender: Account sending the transaction.
|
|
353
|
+
value: Transaction value in wei.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Estimated gas limit.
|
|
357
|
+
"""
|
|
358
|
+
data = self.encode_input(*args)
|
|
359
|
+
return sender.estimate_gas(to=self.address, value=value, data=data, chain=self.chain)
|
|
360
|
+
|
|
361
|
+
def transact(
|
|
362
|
+
self,
|
|
363
|
+
*args,
|
|
364
|
+
sender: Account,
|
|
365
|
+
value: int | str | None = None,
|
|
366
|
+
gas_limit: int | str | None = None,
|
|
367
|
+
gas_buffer: float | None = None,
|
|
368
|
+
gas_price: int | str | None = None,
|
|
369
|
+
max_fee_per_gas: int | str | None = None,
|
|
370
|
+
max_priority_fee_per_gas: int | str | None = None,
|
|
371
|
+
nonce: int | str | None = None,
|
|
372
|
+
):
|
|
373
|
+
"""Sign and broadcast a transaction for this contract function.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
*args: Contract function arguments.
|
|
377
|
+
sender: Account sending the transaction.
|
|
378
|
+
value: Transaction value in wei.
|
|
379
|
+
gas_limit: Explicit gas limit. Estimated when omitted.
|
|
380
|
+
gas_buffer: Multiplier applied to the estimated gas limit.
|
|
381
|
+
gas_price: Legacy gas price.
|
|
382
|
+
max_fee_per_gas: EIP-1559 max fee per gas.
|
|
383
|
+
max_priority_fee_per_gas: EIP-1559 max priority fee per gas.
|
|
384
|
+
nonce: Explicit nonce. Queried when omitted.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Transaction object for the broadcast transaction.
|
|
388
|
+
"""
|
|
389
|
+
data = self.encode_input(*args)
|
|
390
|
+
return sender.transact(
|
|
391
|
+
to=str(self.address),
|
|
392
|
+
value=value,
|
|
393
|
+
data=data,
|
|
394
|
+
gas_limit=gas_limit,
|
|
395
|
+
gas_buffer=gas_buffer,
|
|
396
|
+
gas_price=gas_price,
|
|
397
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
398
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
399
|
+
chain=self.chain,
|
|
400
|
+
nonce=nonce,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def decode_input(self, hexstr: str):
|
|
404
|
+
"""Decode calldata for this contract function.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
hexstr: Hex-encoded calldata including the function selector.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Decoded input values.
|
|
411
|
+
"""
|
|
412
|
+
return abi.decode_calldata(self.method_abi, hexstr)
|
|
413
|
+
|
|
414
|
+
def encode_input(self, *args):
|
|
415
|
+
"""Encode calldata for this contract function.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
*args: Contract function arguments.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Hex-encoded calldata including the function selector.
|
|
422
|
+
"""
|
|
423
|
+
return abi.encode_calldata(self.method_abi, args)
|
|
424
|
+
|
|
425
|
+
def decode_output(self, hexstr: str):
|
|
426
|
+
"""Decode return data for this contract function.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
hexstr: Hex-encoded return data.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Decoded return value.
|
|
433
|
+
"""
|
|
434
|
+
return abi.decode_returndata(self.method_abi, hexstr)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class OverloadedMethod:
|
|
438
|
+
"""Callable wrapper for a contract function with multiple overloads."""
|
|
439
|
+
|
|
440
|
+
def __init__(self, address: Account, method_abis: list[dict], chain: Chain):
|
|
441
|
+
self.address = address
|
|
442
|
+
self.chain = chain
|
|
443
|
+
self.method_abis = method_abis
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def name(self) -> str:
|
|
447
|
+
"""Return the overloaded function name."""
|
|
448
|
+
return self.method_abis[0]["name"]
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def signatures(self) -> list[str]:
|
|
452
|
+
"""Return all available overload signatures."""
|
|
453
|
+
return [abi.function_signature(i) for i in self.method_abis]
|
|
454
|
+
|
|
455
|
+
def _make_method(self, method_abi: dict) -> _ContractMethod:
|
|
456
|
+
cls = _method_class(method_abi)
|
|
457
|
+
return cls(address=self.address, method_abi=method_abi, chain=self.chain)
|
|
458
|
+
|
|
459
|
+
def _input_types(self, method_abi: dict) -> tuple[str, ...]:
|
|
460
|
+
return tuple(i["type"] for i in method_abi.get("inputs", []))
|
|
461
|
+
|
|
462
|
+
def _format_available_overloads(self) -> str:
|
|
463
|
+
return "\n".join(self.signatures)
|
|
464
|
+
|
|
465
|
+
def _resolve_by_args(self, args: tuple) -> _ContractMethod:
|
|
466
|
+
matches = [i for i in self.method_abis if len(i.get("inputs", [])) == len(args)]
|
|
467
|
+
|
|
468
|
+
if len(matches) == 1:
|
|
469
|
+
return self._make_method(matches[0])
|
|
470
|
+
|
|
471
|
+
if not matches:
|
|
472
|
+
raise ValueError(
|
|
473
|
+
f"No matching overload for {self.name} with {len(args)} arguments. "
|
|
474
|
+
f"Available overloads:\n{self._format_available_overloads()}"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
raise ValueError(
|
|
478
|
+
f"Ambiguous overload for {self.name} with {len(args)} arguments. "
|
|
479
|
+
f"Available overloads:\n{self._format_available_overloads()}"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def _normalize_key(self, key) -> tuple[str, ...]:
|
|
483
|
+
if isinstance(key, str):
|
|
484
|
+
if not key:
|
|
485
|
+
return ()
|
|
486
|
+
return tuple(i.strip() for i in key.split(","))
|
|
487
|
+
if isinstance(key, tuple):
|
|
488
|
+
if not all(isinstance(i, str) for i in key):
|
|
489
|
+
raise TypeError("Overload selector tuple must contain only strings")
|
|
490
|
+
return tuple(i.strip() for i in key)
|
|
491
|
+
raise TypeError("Overload selector must be a comma-separated string or tuple of strings")
|
|
492
|
+
|
|
493
|
+
def __getitem__(self, key):
|
|
494
|
+
"""Select an overload by input type signature.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
key: Comma-separated input type string or tuple of input type strings.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Contract method wrapper for the selected overload.
|
|
501
|
+
"""
|
|
502
|
+
input_types = self._normalize_key(key)
|
|
503
|
+
matches = [i for i in self.method_abis if self._input_types(i) == input_types]
|
|
504
|
+
|
|
505
|
+
if not matches:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"No overload for {self.name} with input types {input_types}. "
|
|
508
|
+
f"Available overloads:\n{self._format_available_overloads()}"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return self._make_method(matches[0])
|
|
512
|
+
|
|
513
|
+
def call(
|
|
514
|
+
self,
|
|
515
|
+
*args,
|
|
516
|
+
sender: Account = None,
|
|
517
|
+
value: int | str | None = None,
|
|
518
|
+
gas_limit: int | str | None = None,
|
|
519
|
+
block_identifier: str | int | None = None,
|
|
520
|
+
):
|
|
521
|
+
"""Call the overload matching the provided arguments."""
|
|
522
|
+
method = self._resolve_by_args(args)
|
|
523
|
+
return method.call(
|
|
524
|
+
*args,
|
|
525
|
+
sender=sender,
|
|
526
|
+
value=value,
|
|
527
|
+
gas_limit=gas_limit,
|
|
528
|
+
block_identifier=block_identifier,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def estimate_gas(self, *args, sender: Account, value: int | str | None = None):
|
|
532
|
+
"""Estimate gas for the overload matching the provided arguments."""
|
|
533
|
+
method = self._resolve_by_args(args)
|
|
534
|
+
return method.estimate_gas(*args, sender=sender, value=value)
|
|
535
|
+
|
|
536
|
+
def transact(
|
|
537
|
+
self,
|
|
538
|
+
*args,
|
|
539
|
+
sender: Account,
|
|
540
|
+
value: int | str | None = None,
|
|
541
|
+
gas_limit: int | str | None = None,
|
|
542
|
+
gas_buffer: float | None = None,
|
|
543
|
+
gas_price: int | str | None = None,
|
|
544
|
+
max_fee_per_gas: int | str | None = None,
|
|
545
|
+
max_priority_fee_per_gas: int | str | None = None,
|
|
546
|
+
nonce: int | str | None = None,
|
|
547
|
+
):
|
|
548
|
+
"""Broadcast a transaction for the overload matching the provided arguments."""
|
|
549
|
+
method = self._resolve_by_args(args)
|
|
550
|
+
return method.transact(
|
|
551
|
+
*args,
|
|
552
|
+
sender=sender,
|
|
553
|
+
value=value,
|
|
554
|
+
gas_limit=gas_limit,
|
|
555
|
+
gas_buffer=gas_buffer,
|
|
556
|
+
gas_price=gas_price,
|
|
557
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
558
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
559
|
+
nonce=nonce,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def __call__(
|
|
563
|
+
self,
|
|
564
|
+
*args,
|
|
565
|
+
sender=None,
|
|
566
|
+
value: int | str | None = None,
|
|
567
|
+
gas_limit: int | str | None = None,
|
|
568
|
+
gas_buffer: float | None = None,
|
|
569
|
+
gas_price: int | str | None = None,
|
|
570
|
+
max_fee_per_gas: int | str | None = None,
|
|
571
|
+
max_priority_fee_per_gas: int | str | None = None,
|
|
572
|
+
nonce: int | str | None = None,
|
|
573
|
+
block_identifier: str | int | None = None,
|
|
574
|
+
):
|
|
575
|
+
"""Call or transact using the overload matching the provided arguments."""
|
|
576
|
+
method = self._resolve_by_args(args)
|
|
577
|
+
|
|
578
|
+
if isinstance(method, ContractCall):
|
|
579
|
+
return method(
|
|
580
|
+
*args,
|
|
581
|
+
sender=sender,
|
|
582
|
+
value=value,
|
|
583
|
+
gas_limit=gas_limit,
|
|
584
|
+
block_identifier=block_identifier,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
return method(
|
|
588
|
+
*args,
|
|
589
|
+
sender=sender,
|
|
590
|
+
value=value,
|
|
591
|
+
gas_limit=gas_limit,
|
|
592
|
+
gas_buffer=gas_buffer,
|
|
593
|
+
gas_price=gas_price,
|
|
594
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
595
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
596
|
+
nonce=nonce,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class ContractCall(_ContractMethod):
|
|
601
|
+
"""Callable wrapper for a view or pure contract function."""
|
|
602
|
+
|
|
603
|
+
def __call__(
|
|
604
|
+
self,
|
|
605
|
+
*args,
|
|
606
|
+
sender=None,
|
|
607
|
+
value: int | str | None = None,
|
|
608
|
+
gas_limit: int | str | None = None,
|
|
609
|
+
block_identifier: str | int | None = None,
|
|
610
|
+
):
|
|
611
|
+
"""Execute the contract function with ``eth_call``."""
|
|
612
|
+
return self.call(
|
|
613
|
+
*args,
|
|
614
|
+
sender=sender,
|
|
615
|
+
value=value,
|
|
616
|
+
gas_limit=gas_limit,
|
|
617
|
+
block_identifier=block_identifier,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
class ContractTx(_ContractMethod):
|
|
622
|
+
"""Callable wrapper for a nonpayable or payable contract function."""
|
|
623
|
+
|
|
624
|
+
def __call__(
|
|
625
|
+
self,
|
|
626
|
+
*args,
|
|
627
|
+
sender: Account,
|
|
628
|
+
value: int | str | None = None,
|
|
629
|
+
gas_limit: int | str | None = None,
|
|
630
|
+
gas_buffer: float | None = None,
|
|
631
|
+
gas_price: int | str | None = None,
|
|
632
|
+
max_fee_per_gas: int | str | None = None,
|
|
633
|
+
max_priority_fee_per_gas: int | str | None = None,
|
|
634
|
+
nonce: int | str | None = None,
|
|
635
|
+
):
|
|
636
|
+
"""Sign and broadcast a transaction for the contract function."""
|
|
637
|
+
return self.transact(
|
|
638
|
+
*args,
|
|
639
|
+
sender=sender,
|
|
640
|
+
value=value,
|
|
641
|
+
gas_limit=gas_limit,
|
|
642
|
+
gas_buffer=gas_buffer,
|
|
643
|
+
gas_price=gas_price,
|
|
644
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
645
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
646
|
+
nonce=nonce,
|
|
647
|
+
)
|