| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- import functools
- import re
- import parsimonious
- from parsimonious import (
- expressions,
- )
- from eth_abi.exceptions import (
- ABITypeError,
- ParseError,
- )
- grammar = parsimonious.Grammar(
- r"""
- type = tuple_type / basic_type
- tuple_type = components arrlist?
- components = non_zero_tuple
- non_zero_tuple = "(" type next_type* ")"
- next_type = "," type
- basic_type = base sub? arrlist?
- base = alphas
- sub = two_size / digits
- two_size = (digits "x" digits)
- arrlist = (const_arr / dynam_arr)+
- const_arr = "[" digits "]"
- dynam_arr = "[]"
- alphas = ~"[A-Za-z]+"
- digits = ~"[1-9][0-9]*"
- """
- )
- class NodeVisitor(parsimonious.NodeVisitor): # type: ignore[misc] # subclasses Any
- """
- Parsimonious node visitor which performs both parsing of type strings and
- post-processing of parse trees. Parsing operations are cached.
- """
- def __init__(self):
- self.parse = functools.lru_cache(maxsize=None)(self._parse_uncached)
- grammar = grammar
- def visit_non_zero_tuple(self, node, visited_children):
- # Ignore left and right parens
- _, first, rest, _ = visited_children
- return (first,) + rest
- def visit_tuple_type(self, node, visited_children):
- components, arrlist = visited_children
- return TupleType(components, arrlist, node=node)
- def visit_next_type(self, node, visited_children):
- # Ignore comma
- _, abi_type = visited_children
- return abi_type
- def visit_basic_type(self, node, visited_children):
- base, sub, arrlist = visited_children
- return BasicType(base, sub, arrlist, node=node)
- def visit_two_size(self, node, visited_children):
- # Ignore "x"
- first, _, second = visited_children
- return first, second
- def visit_const_arr(self, node, visited_children):
- # Ignore left and right brackets
- _, int_value, _ = visited_children
- return (int_value,)
- def visit_dynam_arr(self, node, visited_children):
- return tuple()
- def visit_alphas(self, node, visited_children):
- return node.text
- def visit_digits(self, node, visited_children):
- return int(node.text)
- def generic_visit(self, node, visited_children):
- expr = node.expr
- if isinstance(expr, expressions.OneOf):
- # Unwrap value chosen from alternatives
- return visited_children[0]
- if isinstance(expr, expressions.Quantifier) and expr.min == 0 and expr.max == 1:
- # Unwrap optional value or return `None`
- if len(visited_children) != 0:
- return visited_children[0]
- return None
- return tuple(visited_children)
- def _parse_uncached(self, type_str, **kwargs):
- """
- Parses a type string into an appropriate instance of
- :class:`~eth_abi.grammar.ABIType`. If a type string cannot be parsed,
- throws :class:`~eth_abi.exceptions.ParseError`.
- :param type_str: The type string to be parsed.
- :returns: An instance of :class:`~eth_abi.grammar.ABIType` containing
- information about the parsed type string.
- """
- if not isinstance(type_str, str):
- raise TypeError(f"Can only parse string values: got {type(type_str)}")
- try:
- return super().parse(type_str, **kwargs)
- except parsimonious.ParseError as e:
- # This is a good place to add some better messaging around the type string.
- # If this logic grows any bigger, we should abstract it to its own function.
- if "()" in type_str:
- # validate against zero-sized tuple types
- raise ValueError('Zero-sized tuple types "()" are not supported.')
- raise ParseError(e.text, e.pos, e.expr)
- visitor = NodeVisitor()
- class ABIType:
- """
- Base class for results of type string parsing operations.
- """
- __slots__ = ("arrlist", "node")
- def __init__(self, arrlist=None, node=None):
- self.arrlist = arrlist
- """
- The list of array dimensions for a parsed type. Equal to ``None`` if
- type string has no array dimensions.
- """
- self.node = node
- """
- The parsimonious ``Node`` instance associated with this parsed type.
- Used to generate error messages for invalid types.
- """
- def __repr__(self): # pragma: no cover
- return f"<{type(self).__qualname__} {repr(self.to_type_str())}>"
- def __eq__(self, other):
- # Two ABI types are equal if their string representations are equal
- return type(self) is type(other) and self.to_type_str() == other.to_type_str()
- def to_type_str(self): # pragma: no cover
- """
- Returns the string representation of an ABI type. This will be equal to
- the type string from which it was created.
- """
- raise NotImplementedError("Must implement `to_type_str`")
- @property
- def item_type(self):
- """
- If this type is an array type, equal to an appropriate
- :class:`~eth_abi.grammar.ABIType` instance for the array's items.
- """
- raise NotImplementedError("Must implement `item_type`")
- def validate(self): # pragma: no cover
- """
- Validates the properties of an ABI type against the solidity ABI spec:
- https://solidity.readthedocs.io/en/develop/abi-spec.html
- Raises :class:`~eth_abi.exceptions.ABITypeError` if validation fails.
- """
- raise NotImplementedError("Must implement `validate`")
- def invalidate(self, error_msg):
- # Invalidates an ABI type with the given error message. Expects that a
- # parsimonious node was provided from the original parsing operation
- # that yielded this type.
- node = self.node
- raise ABITypeError(
- f"For '{node.text}' type at column {node.start + 1} "
- f"in '{node.full_text}': {error_msg}"
- )
- @property
- def is_array(self):
- """
- Equal to ``True`` if a type is an array type (i.e. if it has an array
- dimension list). Otherwise, equal to ``False``.
- """
- return self.arrlist is not None
- @property
- def is_dynamic(self):
- """
- Equal to ``True`` if a type has a dynamically sized encoding.
- Otherwise, equal to ``False``.
- """
- raise NotImplementedError("Must implement `is_dynamic`")
- @property
- def _has_dynamic_arrlist(self):
- return self.is_array and any(len(dim) == 0 for dim in self.arrlist)
- class TupleType(ABIType):
- """
- Represents the result of parsing a tuple type string e.g. "(int,bool)".
- """
- __slots__ = ("components",)
- def __init__(self, components, arrlist=None, *, node=None):
- super().__init__(arrlist, node)
- self.components = components
- """
- A tuple of :class:`~eth_abi.grammar.ABIType` instances for each of the
- tuple type's components.
- """
- def to_type_str(self):
- arrlist = self.arrlist
- if isinstance(arrlist, tuple):
- arrlist = "".join(repr(list(a)) for a in arrlist)
- else:
- arrlist = ""
- return f"({','.join(c.to_type_str() for c in self.components)}){arrlist}"
- @property
- def item_type(self):
- if not self.is_array:
- raise ValueError(
- f"Cannot determine item type for non-array type '{self.to_type_str()}'"
- )
- return type(self)(
- self.components,
- self.arrlist[:-1] or None,
- node=self.node,
- )
- def validate(self):
- for c in self.components:
- c.validate()
- @property
- def is_dynamic(self):
- if self._has_dynamic_arrlist:
- return True
- return any(c.is_dynamic for c in self.components)
- class BasicType(ABIType):
- """
- Represents the result of parsing a basic type string e.g. "uint", "address",
- "ufixed128x19[][2]".
- """
- __slots__ = ("base", "sub")
- def __init__(self, base, sub=None, arrlist=None, *, node=None):
- super().__init__(arrlist, node)
- self.base = base
- """The base of a basic type e.g. "uint" for "uint256" etc."""
- self.sub = sub
- """
- The sub type of a basic type e.g. ``256`` for "uint256" or ``(128, 18)``
- for "ufixed128x18" etc. Equal to ``None`` if type string has no sub
- type.
- """
- def to_type_str(self):
- sub, arrlist = self.sub, self.arrlist
- if isinstance(sub, int):
- sub = str(sub)
- elif isinstance(sub, tuple):
- sub = "x".join(str(s) for s in sub)
- else:
- sub = ""
- if isinstance(arrlist, tuple):
- arrlist = "".join(repr(list(a)) for a in arrlist)
- else:
- arrlist = ""
- return self.base + sub + arrlist
- @property
- def item_type(self):
- if not self.is_array:
- raise ValueError(
- f"Cannot determine item type for non-array type '{self.to_type_str()}'"
- )
- return type(self)(
- self.base,
- self.sub,
- self.arrlist[:-1] or None,
- node=self.node,
- )
- @property
- def is_dynamic(self):
- if self._has_dynamic_arrlist:
- return True
- if self.base == "string":
- return True
- if self.base == "bytes" and self.sub is None:
- return True
- return False
- def validate(self):
- base, sub = self.base, self.sub
- # Check validity of string type
- if base == "string":
- if sub is not None:
- self.invalidate("string type cannot have suffix")
- # Check validity of bytes type
- elif base == "bytes":
- if not (sub is None or isinstance(sub, int)):
- self.invalidate(
- "bytes type must have either no suffix or a numerical suffix"
- )
- if isinstance(sub, int) and sub > 32:
- self.invalidate("maximum 32 bytes for fixed-length bytes")
- # Check validity of integer type
- elif base in ("int", "uint"):
- if not isinstance(sub, int):
- self.invalidate("integer type must have numerical suffix")
- if sub < 8 or 256 < sub:
- self.invalidate("integer size out of bounds (max 256 bits)")
- if sub % 8 != 0:
- self.invalidate("integer size must be multiple of 8")
- # Check validity of fixed type
- elif base in ("fixed", "ufixed"):
- if not isinstance(sub, tuple):
- self.invalidate(
- "fixed type must have suffix of form <bits>x<exponent>, "
- "e.g. 128x19",
- )
- bits, minus_e = sub
- if bits < 8 or 256 < bits:
- self.invalidate("fixed size out of bounds (max 256 bits)")
- if bits % 8 != 0:
- self.invalidate("fixed size must be multiple of 8")
- if minus_e < 1 or 80 < minus_e:
- self.invalidate(
- f"fixed exponent size out of bounds, {minus_e} must be in 1-80"
- )
- # Check validity of hash type
- elif base == "hash":
- if not isinstance(sub, int):
- self.invalidate("hash type must have numerical suffix")
- # Check validity of address type
- elif base == "address":
- if sub is not None:
- self.invalidate("address cannot have suffix")
- TYPE_ALIASES = {
- "int": "int256",
- "uint": "uint256",
- "fixed": "fixed128x18",
- "ufixed": "ufixed128x18",
- "function": "bytes24",
- "byte": "bytes1",
- }
- TYPE_ALIAS_RE = re.compile(
- rf"\b({'|'.join(re.escape(a) for a in TYPE_ALIASES.keys())})\b"
- )
- def normalize(type_str):
- """
- Normalizes a type string into its canonical version e.g. the type string
- 'int' becomes 'int256', etc.
- :param type_str: The type string to be normalized.
- :returns: The canonical version of the input type string.
- """
- return TYPE_ALIAS_RE.sub(
- lambda match: TYPE_ALIASES[match.group(0)],
- type_str,
- )
- parse = visitor.parse
|