micro-web3 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.
micro_web3/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """micro-web3: a small Ethereum JSON-RPC + ABI client.
2
+
3
+ Replaces the web3.py surface used by the rotki web backend: JSON-RPC transport,
4
+ calldata encoding, output decoding, log queries and Multicall3 batching.
5
+
6
+ >>> from micro_web3 import Contract, RpcNode
7
+ >>> node = RpcNode('https://ethereum-rpc.publicnode.com', timeout=4)
8
+ >>> node.is_connected()
9
+ True
10
+ >>> contract = Contract('0xA0b8...eB48', erc20_abi)
11
+ >>> contract.call(node, 'decimals')
12
+ 6
13
+ """
14
+
15
+ from .abi import (
16
+ AbiType,
17
+ canonical,
18
+ decode,
19
+ decode_outputs,
20
+ encode,
21
+ encode_call,
22
+ find_function,
23
+ parse_type,
24
+ )
25
+ from .addresses import is_address, to_checksum_address
26
+ from .contract import Contract
27
+ from .errors import AbiError, RpcConnectionError, RpcError, RpcResponseError
28
+ from .methods import eth_call, get_block_number, get_logs, to_quantity
29
+ from .multicall import MULTICALL3_ADDRESS, try_aggregate
30
+ from .rpc import RpcNode
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ "MULTICALL3_ADDRESS",
36
+ "AbiError",
37
+ "AbiType",
38
+ "Contract",
39
+ "RpcConnectionError",
40
+ "RpcError",
41
+ "RpcNode",
42
+ "RpcResponseError",
43
+ "__version__",
44
+ "canonical",
45
+ "decode",
46
+ "decode_outputs",
47
+ "encode",
48
+ "encode_call",
49
+ "eth_call",
50
+ "find_function",
51
+ "get_block_number",
52
+ "get_logs",
53
+ "is_address",
54
+ "parse_type",
55
+ "to_checksum_address",
56
+ "to_quantity",
57
+ "try_aggregate",
58
+ ]
micro_web3/_keccak.py ADDED
@@ -0,0 +1,14 @@
1
+ """Keccak-256.
2
+
3
+ Not SHA3-256: Ethereum uses the original Keccak padding, which the standard
4
+ library's ``hashlib.sha3_256`` does not implement. pycryptodome provides the C
5
+ implementation.
6
+ """
7
+
8
+ from Crypto.Hash import keccak
9
+
10
+
11
+ def keccak256(data: bytes) -> bytes:
12
+ digest = keccak.new(digest_bits=256)
13
+ digest.update(data)
14
+ return digest.digest()
micro_web3/abi.py ADDED
@@ -0,0 +1,629 @@
1
+ """Hand-rolled Solidity ABI codec.
2
+
3
+ Covers every type that appears in practice: ``address``, ``bool``, ``uintN``,
4
+ ``intN``, ``bytesN``, ``bytes``, ``string``, static/dynamic arrays and tuples
5
+ (including nested ``tuple[]``). Semantics mirror ``eth_abi`` 6: decoded arrays
6
+ and tuples are tuples, addresses are lower-case ``0x`` hex, ``bytesN`` is
7
+ ``bytes``.
8
+ """
9
+
10
+ from collections.abc import Mapping, Sequence
11
+ from functools import lru_cache
12
+ from typing import Any
13
+
14
+ from ._keccak import keccak256
15
+ from .addresses import to_bytes20
16
+ from .errors import AbiError
17
+
18
+ _WORD = 1 << 256
19
+
20
+
21
+ _STATIC_KINDS = frozenset({"uint", "int", "address", "bool", "fixedbytes"})
22
+
23
+
24
+ class AbiType:
25
+ """A parsed ABI type. ``kind`` is one of uint, int, address, bool,
26
+ fixedbytes, bytes, string, array, tuple.
27
+
28
+ Immutable. ``dynamic`` and ``head_size`` are derived once here so the codec
29
+ does not recurse per value; ``head_size`` is ``None`` for a malformed type.
30
+ """
31
+
32
+ __slots__ = ("bits", "dynamic", "elem", "fields", "head_size", "kind", "length", "size")
33
+ __match_args__ = ("kind", "bits", "size", "length", "elem", "fields")
34
+
35
+ kind: str
36
+ bits: int
37
+ size: int
38
+ length: "int | None"
39
+ elem: "AbiType | None"
40
+ fields: "tuple[AbiType, ...]"
41
+ dynamic: bool
42
+ head_size: "int | None"
43
+
44
+ def __init__(
45
+ self,
46
+ kind: str,
47
+ bits: int = 0,
48
+ size: int = 0,
49
+ length: "int | None" = None,
50
+ elem: "AbiType | None" = None,
51
+ fields: "tuple[AbiType, ...]" = (),
52
+ ) -> None:
53
+ dynamic = False
54
+ head_size: int | None = None
55
+ if kind in _STATIC_KINDS:
56
+ head_size = 32
57
+ elif kind in ("bytes", "string"):
58
+ dynamic, head_size = True, 32
59
+ elif kind == "array":
60
+ if length is None:
61
+ dynamic, head_size = True, 32
62
+ elif elem is not None:
63
+ dynamic = elem.dynamic
64
+ if dynamic:
65
+ head_size = 32
66
+ elif elem.head_size is not None:
67
+ head_size = length * elem.head_size
68
+ elif kind == "tuple":
69
+ dynamic = any(field.dynamic for field in fields)
70
+ if dynamic:
71
+ head_size = 32
72
+ elif all(field.head_size is not None for field in fields):
73
+ head_size = sum(field.head_size or 0 for field in fields)
74
+ put = object.__setattr__
75
+ put(self, "kind", kind)
76
+ put(self, "bits", bits)
77
+ put(self, "size", size)
78
+ put(self, "length", length)
79
+ put(self, "elem", elem)
80
+ put(self, "fields", fields)
81
+ put(self, "dynamic", dynamic)
82
+ put(self, "head_size", head_size)
83
+
84
+ def _key(self) -> tuple[Any, ...]:
85
+ return (self.kind, self.bits, self.size, self.length, self.elem, self.fields)
86
+
87
+ def __eq__(self, other: object) -> bool:
88
+ if not isinstance(other, AbiType):
89
+ return NotImplemented
90
+ return self._key() == other._key()
91
+
92
+ def __hash__(self) -> int:
93
+ return hash(self._key())
94
+
95
+ def __repr__(self) -> str:
96
+ return (
97
+ f"AbiType(kind={self.kind!r}, bits={self.bits!r}, size={self.size!r}, "
98
+ f"length={self.length!r}, elem={self.elem!r}, fields={self.fields!r})"
99
+ )
100
+
101
+ def __setattr__(self, name: str, value: Any) -> None:
102
+ raise AttributeError(f"cannot assign to field {name!r}")
103
+
104
+ def __delattr__(self, name: str) -> None:
105
+ raise AttributeError(f"cannot delete field {name!r}")
106
+
107
+ def __reduce__(self) -> tuple[Any, ...]:
108
+ return (AbiType, self._key())
109
+
110
+
111
+ def parse_type(type_str: str, components: Sequence[Mapping[str, Any]] = ()) -> AbiType:
112
+ """Parse a canonical ABI type such as ``(uint256,address)[]``."""
113
+ if components:
114
+ return _parse_type(type_str, components)
115
+ return _parse_string(type_str)
116
+
117
+
118
+ @lru_cache(maxsize=1024)
119
+ def _parse_string(type_str: str) -> AbiType:
120
+ return _parse_type(type_str, ())
121
+
122
+
123
+ def _parse_type(type_str: str, components: Sequence[Mapping[str, Any]]) -> AbiType:
124
+ base, dims = _split_dimensions(type_str)
125
+ if base.startswith("("):
126
+ inner = base[1:-1]
127
+ parts = _split_top_level(inner) if inner.strip() else []
128
+ parsed = AbiType(kind="tuple", fields=tuple(parse_type(part) for part in parts))
129
+ elif base == "tuple":
130
+ if not components:
131
+ raise AbiError("tuple type requires components")
132
+ parsed = AbiType(
133
+ kind="tuple",
134
+ fields=tuple(
135
+ parse_type(component.get("type", ""), component.get("components", ()))
136
+ for component in components
137
+ ),
138
+ )
139
+ else:
140
+ parsed = _simple_type(base)
141
+
142
+ for length in dims:
143
+ parsed = AbiType(kind="array", elem=parsed, length=length)
144
+ return parsed
145
+
146
+
147
+ def type_from_abi(entry: Mapping[str, Any]) -> AbiType:
148
+ type_str = entry.get("type")
149
+ if not isinstance(type_str, str):
150
+ raise AbiError(f"ABI entry without a string type: {entry!r}")
151
+ return parse_type(type_str, entry.get("components", ()) or ())
152
+
153
+
154
+ def canonical(abi_type: AbiType) -> str:
155
+ kind = abi_type.kind
156
+ if kind in ("uint", "int"):
157
+ return f"{kind}{abi_type.bits}"
158
+ if kind == "fixedbytes":
159
+ return f"bytes{abi_type.size}"
160
+ if kind in ("address", "bool", "bytes", "string"):
161
+ return kind
162
+ if kind == "array":
163
+ if abi_type.elem is None:
164
+ raise AbiError("array type without element type")
165
+ suffix = "[]" if abi_type.length is None else f"[{abi_type.length}]"
166
+ return f"{canonical(abi_type.elem)}{suffix}"
167
+ if kind == "tuple":
168
+ return f"({','.join(canonical(field) for field in abi_type.fields)})"
169
+ raise AbiError(f"unknown ABI type kind {kind!r}")
170
+
171
+
172
+ def is_dynamic(abi_type: AbiType) -> bool:
173
+ kind = abi_type.kind
174
+ if kind in ("bytes", "string"):
175
+ return True
176
+ if kind == "array":
177
+ if abi_type.length is None:
178
+ return True
179
+ if abi_type.elem is None:
180
+ raise AbiError("array type without element type")
181
+ return is_dynamic(abi_type.elem)
182
+ if kind == "tuple":
183
+ return any(is_dynamic(field) for field in abi_type.fields)
184
+ return False
185
+
186
+
187
+ def static_size(abi_type: AbiType) -> int:
188
+ kind = abi_type.kind
189
+ if kind in ("uint", "int", "address", "bool", "fixedbytes"):
190
+ return 32
191
+ if kind in ("bytes", "string"):
192
+ raise AbiError(f"{kind} is dynamic and has no static size")
193
+ if kind == "array":
194
+ if abi_type.elem is None or abi_type.length is None:
195
+ raise AbiError("dynamic array has no static size")
196
+ return abi_type.length * static_size(abi_type.elem)
197
+ if kind == "tuple":
198
+ return sum(static_size(field) for field in abi_type.fields)
199
+ raise AbiError(f"unknown ABI type kind {kind!r}")
200
+
201
+
202
+ # --- encoding -----------------------------------------------------------------
203
+
204
+
205
+ def encode(types: Sequence[str | AbiType], values: Sequence[Any]) -> bytes:
206
+ """Encode ``values`` according to ``types`` (no selector)."""
207
+ parsed = [_coerce_type(item) for item in types]
208
+ return _encode_sequence(parsed, values)
209
+
210
+
211
+ def encode_call(
212
+ abi: Sequence[Mapping[str, Any]],
213
+ name: str,
214
+ args: Sequence[Any] = (),
215
+ ) -> str:
216
+ """Encode a contract call, returning ``0x`` hex including the selector."""
217
+ function, body = _resolve(abi, name, args)
218
+ return "0x" + (selector(function) + body).hex()
219
+
220
+
221
+ def _encode_sequence(types: Sequence[AbiType], values: Sequence[Any]) -> bytes:
222
+ if len(types) != len(values):
223
+ raise AbiError(f"expected {len(types)} value(s), got {len(values)}")
224
+ try:
225
+ head_size = sum([item.head_size for item in types]) # type: ignore[misc]
226
+ except TypeError:
227
+ for item in types:
228
+ if not is_dynamic(item):
229
+ static_size(item) # raises the specific error for a malformed type
230
+ raise AbiError(f"malformed ABI type in {types!r}") from None
231
+ heads: list[bytes] = []
232
+ tails: list[bytes] = []
233
+ tail_size = 0
234
+ for abi_type, value in zip(types, values, strict=True):
235
+ if abi_type.dynamic:
236
+ heads.append(_to_word(head_size + tail_size))
237
+ tail = _encode_value(abi_type, value)
238
+ tails.append(tail)
239
+ tail_size += len(tail)
240
+ else:
241
+ heads.append(_encode_value(abi_type, value))
242
+ return b"".join(heads) + b"".join(tails)
243
+
244
+
245
+ def _encode_value(abi_type: AbiType, value: Any) -> bytes:
246
+ kind = abi_type.kind
247
+ if kind == "uint":
248
+ number = _as_int(value, "uint")
249
+ if not 0 <= number < (1 << abi_type.bits):
250
+ raise AbiError(f"{number} out of range for uint{abi_type.bits}")
251
+ return _to_word(number)
252
+ if kind == "int":
253
+ number = _as_int(value, "int")
254
+ bound = 1 << (abi_type.bits - 1)
255
+ if not -bound <= number < bound:
256
+ raise AbiError(f"{number} out of range for int{abi_type.bits}")
257
+ return _to_word(number % _WORD)
258
+ if kind == "bool":
259
+ if not isinstance(value, bool):
260
+ raise AbiError(f"expected bool, got {type(value).__name__}")
261
+ return _to_word(int(value))
262
+ if kind == "address":
263
+ try:
264
+ raw = to_bytes20(value)
265
+ except (TypeError, ValueError) as exc:
266
+ raise AbiError(str(exc)) from exc
267
+ return b"\x00" * 12 + raw
268
+ if kind == "fixedbytes":
269
+ raw = _as_bytes(value)
270
+ if len(raw) > abi_type.size:
271
+ raise AbiError(f"{len(raw)} bytes do not fit in bytes{abi_type.size}")
272
+ return raw.ljust(32, b"\x00")
273
+ if kind == "bytes":
274
+ raw = _as_bytes(value)
275
+ return _to_word(len(raw)) + raw.ljust(_ceil32(len(raw)), b"\x00")
276
+ if kind == "string":
277
+ if not isinstance(value, str):
278
+ raise AbiError(f"expected str, got {type(value).__name__}")
279
+ raw = value.encode("utf-8")
280
+ return _to_word(len(raw)) + raw.ljust(_ceil32(len(raw)), b"\x00")
281
+ if kind == "array":
282
+ if abi_type.elem is None:
283
+ raise AbiError("array type without element type")
284
+ items = _as_sequence(value, "array")
285
+ if abi_type.length is not None and len(items) != abi_type.length:
286
+ raise AbiError(
287
+ f"expected {abi_type.length} array element(s), got {len(items)}",
288
+ )
289
+ elem = abi_type.elem
290
+ if elem.dynamic:
291
+ body = _encode_dynamic_elements(elem, items)
292
+ else:
293
+ body = b"".join([_encode_value(elem, item) for item in items])
294
+ if abi_type.length is None:
295
+ return _to_word(len(items)) + body
296
+ return body
297
+ if kind == "tuple":
298
+ items = _as_sequence(value, "tuple")
299
+ return _encode_sequence(abi_type.fields, items)
300
+ raise AbiError(f"unknown ABI type kind {kind!r}")
301
+
302
+
303
+ def _encode_dynamic_elements(elem: AbiType, items: Sequence[Any]) -> bytes:
304
+ offset = 32 * len(items)
305
+ heads: list[bytes] = []
306
+ tails: list[bytes] = []
307
+ for item in items:
308
+ tail = _encode_value(elem, item)
309
+ heads.append(offset.to_bytes(32, "big"))
310
+ tails.append(tail)
311
+ offset += len(tail)
312
+ return b"".join(heads) + b"".join(tails)
313
+
314
+
315
+ # --- decoding -----------------------------------------------------------------
316
+
317
+
318
+ def decode(types: Sequence[str | AbiType], data: bytes | bytearray | str) -> tuple[Any, ...]:
319
+ """Decode ``data`` into a tuple of Python values."""
320
+ parsed = [_coerce_type(item) for item in types]
321
+ raw = _as_bytes(data)
322
+ if not parsed:
323
+ return ()
324
+ if not raw:
325
+ raise AbiError("cannot decode empty payload")
326
+ values, _ = _decode_sequence(parsed, raw, 0)
327
+ return values
328
+
329
+
330
+ def decode_outputs(
331
+ types: Sequence[str | AbiType],
332
+ data: bytes | bytearray | str,
333
+ ) -> tuple[Any, ...]:
334
+ """Alias of :func:`decode`, named after the web3/eth_abi helper it replaces."""
335
+ return decode(types, data)
336
+
337
+
338
+ def _decode_sequence(
339
+ types: Sequence[AbiType],
340
+ data: bytes,
341
+ start: int,
342
+ ) -> tuple[tuple[Any, ...], int]:
343
+ values: list[Any] = []
344
+ position = start
345
+ for abi_type in types:
346
+ if abi_type.dynamic:
347
+ offset = _read_word(data, position)
348
+ value, _ = _decode_value(abi_type, data, start + offset)
349
+ values.append(value)
350
+ position += 32
351
+ else:
352
+ value, position = _decode_value(abi_type, data, position)
353
+ values.append(value)
354
+ return tuple(values), position
355
+
356
+
357
+ def _decode_value(abi_type: AbiType, data: bytes, position: int) -> tuple[Any, int]:
358
+ kind = abi_type.kind
359
+ if kind == "uint":
360
+ return _read_word(data, position), position + 32
361
+ if kind == "int":
362
+ word = _read_word(data, position)
363
+ if word >= 1 << 255:
364
+ word -= _WORD
365
+ return word, position + 32
366
+ if kind == "bool":
367
+ return _read_word(data, position) != 0, position + 32
368
+ if kind == "address":
369
+ _require(data, position, 32)
370
+ return "0x" + data[position + 12 : position + 32].hex(), position + 32
371
+ if kind == "fixedbytes":
372
+ _require(data, position, 32)
373
+ return data[position : position + abi_type.size], position + 32
374
+ if kind in ("bytes", "string"):
375
+ length = _read_word(data, position)
376
+ payload_start = position + 32
377
+ payload_end = payload_start + length
378
+ _require(data, payload_start, length)
379
+ payload = data[payload_start:payload_end]
380
+ end = payload_start + _ceil32(length)
381
+ if kind == "string":
382
+ try:
383
+ return payload.decode("utf-8"), end
384
+ except UnicodeDecodeError as exc:
385
+ raise AbiError("string payload is not valid utf-8") from exc
386
+ return payload, end
387
+ if kind == "array":
388
+ if abi_type.elem is None:
389
+ raise AbiError("array type without element type")
390
+ if abi_type.length is None:
391
+ count = _read_word(data, position)
392
+ elements_start = position + 32
393
+ else:
394
+ count = abi_type.length
395
+ elements_start = position
396
+ if count > (len(data) - elements_start) // 32:
397
+ raise AbiError(f"array of {count} element(s) does not fit in payload")
398
+ return _decode_elements(abi_type.elem, count, data, elements_start)
399
+ if kind == "tuple":
400
+ return _decode_sequence(abi_type.fields, data, position)
401
+ raise AbiError(f"unknown ABI type kind {kind!r}")
402
+
403
+
404
+ def _decode_elements(
405
+ elem: AbiType,
406
+ count: int,
407
+ data: bytes,
408
+ start: int,
409
+ ) -> tuple[tuple[Any, ...], int]:
410
+ # The caller has checked that ``count`` words fit, so word slices are in bounds.
411
+ if elem.kind == "uint":
412
+ end = start + count * 32
413
+ return tuple([int.from_bytes(data[i : i + 32]) for i in range(start, end, 32)]), end
414
+ values: list[Any] = []
415
+ position = start
416
+ if elem.dynamic:
417
+ for _ in range(count):
418
+ value, _end = _decode_value(elem, data, start + _read_word(data, position))
419
+ values.append(value)
420
+ position += 32
421
+ else:
422
+ for _ in range(count):
423
+ value, position = _decode_value(elem, data, position)
424
+ values.append(value)
425
+ return tuple(values), position
426
+
427
+
428
+ # --- ABI JSON helpers ---------------------------------------------------------
429
+
430
+
431
+ def find_function(
432
+ abi: Sequence[Mapping[str, Any]],
433
+ name: str,
434
+ args: Sequence[Any] = (),
435
+ ) -> Mapping[str, Any]:
436
+ """Resolve an overload by try-encoding the arguments.
437
+
438
+ Raises :class:`AttributeError` when no function has that name (callers rely
439
+ on this to trigger an indexer fallback) and :class:`AbiError` when the name
440
+ exists but no overload accepts the arguments.
441
+ """
442
+ return _resolve(abi, name, args)[0]
443
+
444
+
445
+ def _resolve(
446
+ abi: Sequence[Mapping[str, Any]],
447
+ name: str,
448
+ args: Sequence[Any],
449
+ ) -> tuple[Mapping[str, Any], bytes]:
450
+ """Return the first overload accepting ``args`` and the encoded arguments."""
451
+ candidates = [
452
+ entry
453
+ for entry in abi
454
+ if entry.get("type", "function") == "function" and entry.get("name") == name
455
+ ]
456
+ if not candidates:
457
+ raise AttributeError(f"ABI has no function named {name!r}")
458
+
459
+ failures: list[str] = []
460
+ for entry in candidates:
461
+ try:
462
+ return entry, _encode_arguments(entry, args)
463
+ except AbiError as exc:
464
+ failures.append(str(exc))
465
+ raise AbiError(
466
+ f"no overload of {name!r} accepts {len(args)} argument(s): " + "; ".join(failures),
467
+ )
468
+
469
+
470
+ def function_signature(function: Mapping[str, Any]) -> str:
471
+ name = function.get("name")
472
+ if not isinstance(name, str):
473
+ raise AbiError(f"ABI function without a name: {function!r}")
474
+ types = ",".join(canonical(item) for item in inputs_types(function))
475
+ return f"{name}({types})"
476
+
477
+
478
+ def selector(function: Mapping[str, Any]) -> bytes:
479
+ """4-byte selector, Keccak-256 of the canonical signature."""
480
+ return _selector_of(function_signature(function))
481
+
482
+
483
+ @lru_cache(maxsize=1024)
484
+ def _selector_of(signature: str) -> bytes:
485
+ return keccak256(signature.encode("ascii"))[:4]
486
+
487
+
488
+ def inputs_types(function: Mapping[str, Any]) -> tuple[AbiType, ...]:
489
+ return tuple(type_from_abi(item) for item in function.get("inputs", ()) or ())
490
+
491
+
492
+ def outputs_types(function: Mapping[str, Any]) -> tuple[AbiType, ...]:
493
+ return tuple(type_from_abi(item) for item in function.get("outputs", ()) or ())
494
+
495
+
496
+ def _encode_arguments(function: Mapping[str, Any], args: Sequence[Any]) -> bytes:
497
+ return encode(inputs_types(function), args)
498
+
499
+
500
+ # --- primitive helpers --------------------------------------------------------
501
+
502
+
503
+ def _coerce_type(item: str | AbiType) -> AbiType:
504
+ if isinstance(item, AbiType):
505
+ return item
506
+ if isinstance(item, str):
507
+ return parse_type(item)
508
+ raise AbiError(f"expected a type string or AbiType, got {type(item).__name__}")
509
+
510
+
511
+ def _to_word(value: int) -> bytes:
512
+ return value.to_bytes(32, "big")
513
+
514
+
515
+ def _read_word(data: bytes, position: int) -> int:
516
+ word = data[position : position + 32]
517
+ if len(word) != 32 or position < 0:
518
+ _require(data, position, 32)
519
+ return int.from_bytes(word)
520
+
521
+
522
+ def _require(data: bytes, position: int, size: int) -> None:
523
+ if position < 0 or position + size > len(data):
524
+ raise AbiError(
525
+ f"payload too short: need {size} byte(s) at offset {position}, "
526
+ f"payload is {len(data)} byte(s)",
527
+ )
528
+
529
+
530
+ def _ceil32(size: int) -> int:
531
+ return (size + 31) // 32 * 32
532
+
533
+
534
+ def _as_int(value: Any, label: str) -> int:
535
+ if isinstance(value, bool) or not isinstance(value, int):
536
+ raise AbiError(f"expected int for {label}, got {type(value).__name__}")
537
+ return value
538
+
539
+
540
+ def _as_sequence(value: Any, label: str) -> Sequence[Any]:
541
+ if isinstance(value, (list, tuple)):
542
+ return value
543
+ raise AbiError(f"expected list or tuple for {label}, got {type(value).__name__}")
544
+
545
+
546
+ def _as_bytes(value: Any) -> bytes:
547
+ if isinstance(value, bytearray):
548
+ return bytes(value)
549
+ if isinstance(value, bytes):
550
+ return value
551
+ if isinstance(value, str):
552
+ digits = value[2:] if value[:2].lower() == "0x" else value
553
+ if len(digits) % 2 != 0:
554
+ raise AbiError(f"hex string with odd length: {value!r}")
555
+ try:
556
+ return bytes.fromhex(digits)
557
+ except ValueError as exc:
558
+ raise AbiError(f"invalid hex value: {value!r}") from exc
559
+ raise AbiError(f"expected bytes or hex str, got {type(value).__name__}")
560
+
561
+
562
+ def _split_dimensions(type_str: str) -> tuple[str, list[int | None]]:
563
+ base = type_str.strip()
564
+ dimensions: list[int | None] = []
565
+ while base.endswith("]"):
566
+ opening = base.rfind("[")
567
+ if opening == -1:
568
+ raise AbiError(f"invalid array type {type_str!r}")
569
+ inner = base[opening + 1 : -1].strip()
570
+ if inner == "":
571
+ dimensions.append(None)
572
+ else:
573
+ try:
574
+ dimensions.append(int(inner))
575
+ except ValueError as exc:
576
+ raise AbiError(f"invalid array length in {type_str!r}") from exc
577
+ base = base[:opening]
578
+ dimensions.reverse()
579
+ return base, dimensions
580
+
581
+
582
+ def _split_top_level(text: str) -> list[str]:
583
+ parts: list[str] = []
584
+ depth = 0
585
+ current: list[str] = []
586
+ for char in text:
587
+ if char == "(":
588
+ depth += 1
589
+ elif char == ")":
590
+ depth -= 1
591
+ if depth < 0:
592
+ raise AbiError(f"unbalanced parentheses in {text!r}")
593
+ if char == "," and depth == 0:
594
+ parts.append("".join(current).strip())
595
+ current = []
596
+ else:
597
+ current.append(char)
598
+ if depth != 0:
599
+ raise AbiError(f"unbalanced parentheses in {text!r}")
600
+ parts.append("".join(current).strip())
601
+ return parts
602
+
603
+
604
+ def _simple_type(base: str) -> AbiType:
605
+ if base == "address":
606
+ return AbiType(kind="address")
607
+ if base == "bool":
608
+ return AbiType(kind="bool")
609
+ if base == "string":
610
+ return AbiType(kind="string")
611
+ if base == "bytes":
612
+ return AbiType(kind="bytes")
613
+ if base == "function":
614
+ return AbiType(kind="fixedbytes", size=24)
615
+ if base.startswith(("uint", "int")) and base[-1].isdigit():
616
+ kind = "uint" if base.startswith("uint") else "int"
617
+ bits_text = base[len(kind) :]
618
+ if not bits_text.isdigit():
619
+ raise AbiError(f"invalid integer type {base!r}")
620
+ bits = int(bits_text)
621
+ if bits < 8 or bits > 256 or bits % 8 != 0:
622
+ raise AbiError(f"invalid integer width in {base!r}")
623
+ return AbiType(kind=kind, bits=bits)
624
+ if base.startswith("bytes") and base[5:].isdigit():
625
+ size = int(base[5:])
626
+ if not 1 <= size <= 32:
627
+ raise AbiError(f"invalid fixed bytes width in {base!r}")
628
+ return AbiType(kind="fixedbytes", size=size)
629
+ raise AbiError(f"unsupported ABI type {base!r}")