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 +58 -0
- micro_web3/_keccak.py +14 -0
- micro_web3/abi.py +629 -0
- micro_web3/addresses.py +62 -0
- micro_web3/contract.py +135 -0
- micro_web3/errors.py +48 -0
- micro_web3/methods.py +114 -0
- micro_web3/multicall.py +63 -0
- micro_web3/py.typed +0 -0
- micro_web3/rpc.py +143 -0
- micro_web3-0.1.0.dist-info/METADATA +155 -0
- micro_web3-0.1.0.dist-info/RECORD +14 -0
- micro_web3-0.1.0.dist-info/WHEEL +4 -0
- micro_web3-0.1.0.dist-info/licenses/LICENSE +21 -0
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}")
|