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/abi.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Utilities for ABI encoding, decoding, and signature handling."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from Crypto.Hash import keccak
|
|
6
|
+
from eth.codecs import abi as _abi
|
|
7
|
+
from fw3_keypass.utils import checksum_address
|
|
8
|
+
|
|
9
|
+
from .errors import ABITypeError, ABIValueError
|
|
10
|
+
|
|
11
|
+
_ARRAY_RE = re.compile(r"\[(\d*)\]")
|
|
12
|
+
_INT_RE = re.compile(r"^(u?int)(\d*)$")
|
|
13
|
+
_BYTES_RE = re.compile(r"^bytes(\d*)$")
|
|
14
|
+
_HEX_RE = re.compile(r"^[0-9a-fA-F]*$")
|
|
15
|
+
_DECIMAL_INT_RE = re.compile(r"^-?[0-9]+$")
|
|
16
|
+
_HEX_INT_RE = re.compile(r"^-?0[xX][0-9a-fA-F]+$")
|
|
17
|
+
_ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$")
|
|
18
|
+
_DYNAMIC_TYPES = {"bytes", "string"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _abi_item_type(item: dict) -> str:
|
|
22
|
+
item_type = item["type"]
|
|
23
|
+
|
|
24
|
+
if not item_type.startswith("tuple"):
|
|
25
|
+
return item_type
|
|
26
|
+
|
|
27
|
+
suffix = item_type[5:]
|
|
28
|
+
components = item.get("components", [])
|
|
29
|
+
inner = ",".join(_abi_item_type(component) for component in components)
|
|
30
|
+
return f"({inner}){suffix}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _abi_schema(items: list[dict]) -> str:
|
|
34
|
+
types = ",".join(_abi_item_type(item) for item in items)
|
|
35
|
+
return f"({types})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _split_array_type(item_type: str) -> tuple[str, list[int | None]]:
|
|
39
|
+
if "[" not in item_type:
|
|
40
|
+
return item_type, []
|
|
41
|
+
|
|
42
|
+
base = item_type[: item_type.index("[")]
|
|
43
|
+
suffix = item_type[len(base) :]
|
|
44
|
+
dims = []
|
|
45
|
+
|
|
46
|
+
matches = list(_ARRAY_RE.finditer(suffix))
|
|
47
|
+
if not matches or "".join(i.group(0) for i in matches) != suffix:
|
|
48
|
+
raise ABIValueError(f"Invalid ABI array type: {item_type}")
|
|
49
|
+
|
|
50
|
+
for match in matches:
|
|
51
|
+
size = match.group(1)
|
|
52
|
+
dims.append(None if size == "" else int(size))
|
|
53
|
+
|
|
54
|
+
return base, dims
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _coerce_args(items: list[dict], values: tuple) -> tuple:
|
|
58
|
+
if len(items) != len(values):
|
|
59
|
+
raise ABITypeError(f"Expected {len(items)} arguments, got {len(values)}")
|
|
60
|
+
return tuple(_coerce_value(item, value) for item, value in zip(items, values, strict=True))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _coerce_value(item: dict, value):
|
|
64
|
+
base_type, dims = _split_array_type(item["type"])
|
|
65
|
+
|
|
66
|
+
if dims:
|
|
67
|
+
return _coerce_array(item, value, base_type, dims)
|
|
68
|
+
|
|
69
|
+
if base_type == "address":
|
|
70
|
+
return _coerce_address(value)
|
|
71
|
+
if base_type == "bool":
|
|
72
|
+
return _coerce_bool(value)
|
|
73
|
+
if base_type == "string":
|
|
74
|
+
return _coerce_string(value)
|
|
75
|
+
if base_type == "bytes":
|
|
76
|
+
return _coerce_dynamic_bytes(value)
|
|
77
|
+
if base_type.startswith("bytes"):
|
|
78
|
+
return _coerce_fixed_bytes(base_type, value)
|
|
79
|
+
if base_type.startswith("uint") or base_type.startswith("int"):
|
|
80
|
+
return _coerce_int(base_type, value)
|
|
81
|
+
if base_type == "tuple":
|
|
82
|
+
return _coerce_tuple(item, value)
|
|
83
|
+
|
|
84
|
+
raise ABIValueError(f"Unsupported ABI type: {item['type']}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _coerce_array(item: dict, value, base_type: str, dims: list[int | None]):
|
|
88
|
+
if not isinstance(value, (list, tuple)):
|
|
89
|
+
raise ABITypeError(f"Expected list or tuple for {item['type']}")
|
|
90
|
+
|
|
91
|
+
size = dims[-1]
|
|
92
|
+
if size is not None and len(value) != size:
|
|
93
|
+
raise ABIValueError(f"Expected array of length {size} for {item['type']}, got {len(value)}")
|
|
94
|
+
|
|
95
|
+
child = dict(item)
|
|
96
|
+
child["type"] = base_type + "".join("[]" if i is None else f"[{i}]" for i in dims[:-1])
|
|
97
|
+
return tuple(_coerce_value(child, i) for i in value)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _coerce_tuple(item: dict, value):
|
|
101
|
+
if not isinstance(value, (list, tuple)):
|
|
102
|
+
raise ABITypeError("Expected list or tuple for tuple ABI argument")
|
|
103
|
+
return _coerce_args(item.get("components", []), tuple(value))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _coerce_address(value) -> str:
|
|
107
|
+
from .account import Account
|
|
108
|
+
from .contract import Contract
|
|
109
|
+
|
|
110
|
+
if isinstance(value, Contract):
|
|
111
|
+
value = value.address.address
|
|
112
|
+
|
|
113
|
+
if isinstance(value, Account):
|
|
114
|
+
value = value.address
|
|
115
|
+
|
|
116
|
+
if not isinstance(value, str):
|
|
117
|
+
raise ABITypeError("Expected address string or Account")
|
|
118
|
+
if not _ADDRESS_RE.fullmatch(value):
|
|
119
|
+
raise ABIValueError(f"Invalid address: {value}")
|
|
120
|
+
|
|
121
|
+
return checksum_address(value)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _coerce_bool(value) -> bool:
|
|
125
|
+
if not isinstance(value, bool):
|
|
126
|
+
raise ABITypeError("Expected bool")
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _coerce_string(value) -> str:
|
|
131
|
+
if not isinstance(value, str):
|
|
132
|
+
raise ABITypeError("Expected string")
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _coerce_dynamic_bytes(value) -> bytes:
|
|
137
|
+
if isinstance(value, bytes):
|
|
138
|
+
return value
|
|
139
|
+
if isinstance(value, str):
|
|
140
|
+
return _coerce_hexbytes(value)
|
|
141
|
+
raise ABITypeError("Expected bytes or 0x-prefixed hex string")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _coerce_fixed_bytes(item_type: str, value) -> bytes:
|
|
145
|
+
match = _BYTES_RE.fullmatch(item_type)
|
|
146
|
+
if match is None or match.group(1) == "":
|
|
147
|
+
raise ABIValueError(f"Invalid ABI bytes type: {item_type}")
|
|
148
|
+
|
|
149
|
+
size = int(match.group(1))
|
|
150
|
+
if size < 1 or size > 32:
|
|
151
|
+
raise ABIValueError(f"Invalid ABI bytes size: {size}")
|
|
152
|
+
|
|
153
|
+
value = _coerce_dynamic_bytes(value)
|
|
154
|
+
if len(value) != size:
|
|
155
|
+
raise ABIValueError(f"Expected {item_type} value of length {size}, got {len(value)}")
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _coerce_hexbytes(value: str) -> bytes:
|
|
160
|
+
if not value.startswith("0x"):
|
|
161
|
+
raise ABITypeError("Expected 0x-prefixed hex string")
|
|
162
|
+
|
|
163
|
+
value = value[2:]
|
|
164
|
+
if len(value) % 2:
|
|
165
|
+
raise ABIValueError("Hex string must contain an even number of digits")
|
|
166
|
+
if not _HEX_RE.fullmatch(value):
|
|
167
|
+
raise ABIValueError("Invalid hex string")
|
|
168
|
+
return bytes.fromhex(value)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _coerce_int(item_type: str, value) -> int:
|
|
172
|
+
match = _INT_RE.fullmatch(item_type)
|
|
173
|
+
if match is None:
|
|
174
|
+
raise ABIValueError(f"Invalid ABI integer type: {item_type}")
|
|
175
|
+
|
|
176
|
+
signed = match.group(1) == "int"
|
|
177
|
+
bits = int(match.group(2) or 256)
|
|
178
|
+
if bits < 8 or bits > 256 or bits % 8:
|
|
179
|
+
raise ABIValueError(f"Invalid ABI integer size: {bits}")
|
|
180
|
+
|
|
181
|
+
if isinstance(value, bool):
|
|
182
|
+
raise ABITypeError(f"Expected {item_type}")
|
|
183
|
+
if isinstance(value, int):
|
|
184
|
+
coerced = value
|
|
185
|
+
elif isinstance(value, float):
|
|
186
|
+
coerced = _coerce_float_int(item_type, value)
|
|
187
|
+
elif isinstance(value, str):
|
|
188
|
+
coerced = _coerce_string_int(item_type, value)
|
|
189
|
+
else:
|
|
190
|
+
raise ABITypeError(f"Expected {item_type}")
|
|
191
|
+
|
|
192
|
+
if signed:
|
|
193
|
+
lower = -(2 ** (bits - 1))
|
|
194
|
+
upper = 2 ** (bits - 1) - 1
|
|
195
|
+
else:
|
|
196
|
+
lower = 0
|
|
197
|
+
upper = 2**bits - 1
|
|
198
|
+
|
|
199
|
+
if coerced < lower or coerced > upper:
|
|
200
|
+
raise ABIValueError(f"{item_type} value {coerced} is outside bounds [{lower}, {upper}]")
|
|
201
|
+
return coerced
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _coerce_float_int(item_type: str, value: float) -> int:
|
|
205
|
+
try:
|
|
206
|
+
coerced = int(value)
|
|
207
|
+
except (OverflowError, ValueError) as exc:
|
|
208
|
+
raise ABIValueError(f"Invalid {item_type} value: {value}") from exc
|
|
209
|
+
|
|
210
|
+
if value != coerced:
|
|
211
|
+
raise ABIValueError(f"Expected integral float for {item_type}")
|
|
212
|
+
return coerced
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _coerce_string_int(item_type: str, value: str) -> int:
|
|
216
|
+
try:
|
|
217
|
+
if _HEX_INT_RE.fullmatch(value):
|
|
218
|
+
return int(value, 16)
|
|
219
|
+
if _DECIMAL_INT_RE.fullmatch(value):
|
|
220
|
+
return int(value, 10)
|
|
221
|
+
raise ValueError
|
|
222
|
+
except ValueError as exc:
|
|
223
|
+
raise ABIValueError(f"Invalid {item_type} string: {value}") from exc
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def encode(schema: str, values: tuple) -> bytes:
|
|
227
|
+
"""Encode values using an ABI schema string.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
schema: Parenthesized ABI schema, such as ``"(address,uint256)"``.
|
|
231
|
+
values: Values to encode.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
ABI-encoded bytes.
|
|
235
|
+
"""
|
|
236
|
+
return _abi.encode(schema, values)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def decode(schema: str, data: bytes):
|
|
240
|
+
"""Decode ABI-encoded bytes using an ABI schema string.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
schema: Parenthesized ABI schema, such as ``"(address,uint256)"``.
|
|
244
|
+
data: ABI-encoded bytes.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Decoded values.
|
|
248
|
+
"""
|
|
249
|
+
return _abi.decode(schema, data)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _coerce_hex(value, name: str) -> str:
|
|
253
|
+
if isinstance(value, bytes):
|
|
254
|
+
return "0x" + value.hex()
|
|
255
|
+
if not isinstance(value, str):
|
|
256
|
+
raise ABITypeError(f"{name} must be bytes or a hex string")
|
|
257
|
+
if not value.startswith("0x"):
|
|
258
|
+
raise ABIValueError(f"{name} must be 0x-prefixed")
|
|
259
|
+
try:
|
|
260
|
+
bytes.fromhex(value[2:])
|
|
261
|
+
except ValueError as exc:
|
|
262
|
+
raise ABIValueError(f"{name} must be valid hex") from exc
|
|
263
|
+
return value.lower()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _hex_to_bytes(value, name: str) -> bytes:
|
|
267
|
+
value = _coerce_hex(value, name)
|
|
268
|
+
return bytes.fromhex(value[2:])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _is_dynamic_indexed_type(item: dict) -> bool:
|
|
272
|
+
item_type = item["type"]
|
|
273
|
+
base_type, dims = _split_array_type(item_type)
|
|
274
|
+
return bool(dims) or base_type in _DYNAMIC_TYPES or base_type == "tuple"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _normalize_decoded_value(item: dict, value):
|
|
278
|
+
item_type = item["type"]
|
|
279
|
+
base_type, dims = _split_array_type(item_type)
|
|
280
|
+
|
|
281
|
+
if dims:
|
|
282
|
+
child = dict(item)
|
|
283
|
+
child["type"] = base_type + "".join("[]" if i is None else f"[{i}]" for i in dims[:-1])
|
|
284
|
+
return tuple(_normalize_decoded_value(child, i) for i in value)
|
|
285
|
+
|
|
286
|
+
if base_type == "address":
|
|
287
|
+
return checksum_address(value)
|
|
288
|
+
if base_type == "tuple":
|
|
289
|
+
return tuple(
|
|
290
|
+
_normalize_decoded_value(component, item_value)
|
|
291
|
+
for component, item_value in zip(item.get("components", []), value, strict=True)
|
|
292
|
+
)
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def event_signature(event_abi: dict) -> str:
|
|
297
|
+
"""Return the canonical signature for an event ABI item.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
event_abi: Event ABI item.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Signature string such as ``"Transfer(address,address,uint256)"``.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ABIValueError: If the ABI item is not an event.
|
|
307
|
+
"""
|
|
308
|
+
if event_abi.get("type") != "event":
|
|
309
|
+
raise ABIValueError("ABI item is not an event")
|
|
310
|
+
|
|
311
|
+
name = event_abi["name"]
|
|
312
|
+
inputs = event_abi.get("inputs", [])
|
|
313
|
+
types = ",".join(_abi_item_type(i) for i in inputs)
|
|
314
|
+
return f"{name}({types})"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def event_topic(event_abi: dict) -> str:
|
|
318
|
+
"""Return the topic hash for an event ABI item.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
event_abi: Event ABI item.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Hex-encoded keccak256 hash of the event signature.
|
|
325
|
+
"""
|
|
326
|
+
k = keccak.new(digest_bits=256)
|
|
327
|
+
k.update(event_signature(event_abi).encode())
|
|
328
|
+
return "0x" + k.hexdigest()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def decode_event(event_abi: dict, raw_log: dict) -> dict[str, object]:
|
|
332
|
+
"""Decode a raw log using an event ABI item.
|
|
333
|
+
|
|
334
|
+
Indexed dynamic values are returned as their topic hash, matching Ethereum log
|
|
335
|
+
semantics.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
event_abi: Event ABI item.
|
|
339
|
+
raw_log: RPC log object containing ``topics`` and ``data``.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Mapping of event argument names to decoded values.
|
|
343
|
+
|
|
344
|
+
Raises:
|
|
345
|
+
ABIValueError: If the ABI item, topic count, or event topic is invalid.
|
|
346
|
+
"""
|
|
347
|
+
if event_abi.get("type") != "event":
|
|
348
|
+
raise ABIValueError("ABI item is not an event")
|
|
349
|
+
if event_abi.get("anonymous", False):
|
|
350
|
+
raise ABIValueError("Anonymous events are not supported")
|
|
351
|
+
|
|
352
|
+
topics = tuple(_coerce_hex(i, "topic") for i in raw_log.get("topics", []))
|
|
353
|
+
expected_topic = event_topic(event_abi)
|
|
354
|
+
inputs = event_abi.get("inputs", [])
|
|
355
|
+
indexed = [i for i in inputs if i.get("indexed", False)]
|
|
356
|
+
non_indexed = [i for i in inputs if not i.get("indexed", False)]
|
|
357
|
+
|
|
358
|
+
expected_topics = 1 + len(indexed)
|
|
359
|
+
if len(topics) != expected_topics:
|
|
360
|
+
raise ABIValueError(f"Expected {expected_topics} topics, got {len(topics)}")
|
|
361
|
+
if topics[0] != expected_topic:
|
|
362
|
+
raise ABIValueError(f"Topic {topics[0]} does not match event topic {expected_topic}")
|
|
363
|
+
|
|
364
|
+
values = {}
|
|
365
|
+
topic_index = 1
|
|
366
|
+
data_values = decode(_abi_schema(non_indexed), _hex_to_bytes(raw_log.get("data", "0x"), "data"))
|
|
367
|
+
data_iter = iter(data_values)
|
|
368
|
+
|
|
369
|
+
for item in inputs:
|
|
370
|
+
name = item.get("name", "")
|
|
371
|
+
if item.get("indexed", False):
|
|
372
|
+
topic = topics[topic_index]
|
|
373
|
+
topic_index += 1
|
|
374
|
+
if _is_dynamic_indexed_type(item):
|
|
375
|
+
value = topic
|
|
376
|
+
else:
|
|
377
|
+
decoded = decode(_abi_schema([item]), _hex_to_bytes(topic, "topic"))[0]
|
|
378
|
+
value = _normalize_decoded_value(item, decoded)
|
|
379
|
+
else:
|
|
380
|
+
value = _normalize_decoded_value(item, next(data_iter))
|
|
381
|
+
values[name] = value
|
|
382
|
+
|
|
383
|
+
return values
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def function_signature(method_abi: dict) -> str:
|
|
387
|
+
"""Return the canonical signature for a function ABI item.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
method_abi: Function ABI item.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Signature string such as ``"balanceOf(address)"``.
|
|
394
|
+
"""
|
|
395
|
+
name = method_abi["name"]
|
|
396
|
+
inputs = method_abi.get("inputs", [])
|
|
397
|
+
types = ",".join(i["type"] for i in inputs)
|
|
398
|
+
return f"{name}({types})"
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def function_selector(method_abi: dict) -> bytes:
|
|
402
|
+
"""Return the four-byte selector for a function ABI item.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
method_abi: Function ABI item.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
First four bytes of the keccak256 function signature hash.
|
|
409
|
+
"""
|
|
410
|
+
k = keccak.new(digest_bits=256)
|
|
411
|
+
k.update(function_signature(method_abi).encode())
|
|
412
|
+
return k.digest()[:4]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def overlay_abi(implementation_abi: list[dict], proxy_abi: list[dict]) -> list[dict]:
|
|
416
|
+
"""Overlay proxy ABI items on top of implementation ABI items.
|
|
417
|
+
|
|
418
|
+
Function selectors from the proxy ABI take precedence over matching
|
|
419
|
+
implementation selectors. Non-function ABI items from both lists are preserved.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
implementation_abi: ABI for the implementation contract.
|
|
423
|
+
proxy_abi: ABI for the proxy contract.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Combined ABI list.
|
|
427
|
+
"""
|
|
428
|
+
items = []
|
|
429
|
+
functions = {}
|
|
430
|
+
|
|
431
|
+
for item in [*implementation_abi, *proxy_abi]:
|
|
432
|
+
if item.get("type", "function") != "function":
|
|
433
|
+
items.append(item)
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
selector = function_selector(item)
|
|
437
|
+
if selector in functions:
|
|
438
|
+
items[functions[selector]] = item
|
|
439
|
+
else:
|
|
440
|
+
functions[selector] = len(items)
|
|
441
|
+
items.append(item)
|
|
442
|
+
|
|
443
|
+
return items
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def decode_calldata(method_abi: dict, hexstr: str):
|
|
447
|
+
"""Decode function calldata for a specific ABI item.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
method_abi: Function ABI item.
|
|
451
|
+
hexstr: Hex-encoded calldata including the function selector.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Decoded input values.
|
|
455
|
+
|
|
456
|
+
Raises:
|
|
457
|
+
ValueError: If calldata is too short or has the wrong selector.
|
|
458
|
+
"""
|
|
459
|
+
data = bytes.fromhex(hexstr.removeprefix("0x"))
|
|
460
|
+
|
|
461
|
+
if len(data) < 4:
|
|
462
|
+
raise ValueError("Input data is shorter than a function selector")
|
|
463
|
+
|
|
464
|
+
selector = data[:4]
|
|
465
|
+
expected_selector = function_selector(method_abi)
|
|
466
|
+
if selector != expected_selector:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"Input selector 0x{selector.hex()} does not match "
|
|
469
|
+
f"method selector 0x{expected_selector.hex()}"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
schema = _abi_schema(method_abi.get("inputs", []))
|
|
473
|
+
return decode(schema, data[4:])
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def encode_calldata(method_abi: dict, args: tuple):
|
|
477
|
+
"""Encode function calldata for a specific ABI item.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
method_abi: Function ABI item.
|
|
481
|
+
args: Function arguments.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Hex-encoded calldata including the function selector.
|
|
485
|
+
"""
|
|
486
|
+
inputs = method_abi.get("inputs", [])
|
|
487
|
+
schema = _abi_schema(inputs)
|
|
488
|
+
coerced = _coerce_args(inputs, args)
|
|
489
|
+
data = function_selector(method_abi) + encode(schema, coerced)
|
|
490
|
+
return f"0x{data.hex()}"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def decode_returndata(method_abi: dict, hexstr: str):
|
|
494
|
+
"""Decode function return data for a specific ABI item.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
method_abi: Function ABI item.
|
|
498
|
+
hexstr: Hex-encoded return data.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
``None`` for no outputs, a single value for one output, or a tuple for
|
|
502
|
+
multiple outputs.
|
|
503
|
+
"""
|
|
504
|
+
schema = _abi_schema(method_abi.get("outputs", []))
|
|
505
|
+
values = decode(schema, bytes.fromhex(hexstr.removeprefix("0x")))
|
|
506
|
+
|
|
507
|
+
if len(values) == 0:
|
|
508
|
+
return None
|
|
509
|
+
if len(values) == 1:
|
|
510
|
+
return values[0]
|
|
511
|
+
return values
|