This commit is contained in:
lz_db
2025-11-16 12:31:03 +08:00
commit 0fab423a18
1451 changed files with 743213 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from .messages import *
__all__ = ["messages"]

View File

@@ -0,0 +1,4 @@
from .encoding_and_hashing import (
hash_domain,
hash_eip712_message,
)

View File

@@ -0,0 +1,239 @@
from typing import (
Any,
Dict,
List,
Tuple,
Union,
)
from ...abi import (
encode,
)
from ....keccak import (
SHA3 as keccak
)
from ...utils import (
to_bytes,
to_int,
)
from .helpers import (
EIP712_SOLIDITY_TYPES,
is_0x_prefixed_hexstr,
is_array_type,
parse_core_array_type,
parse_parent_array_type,
)
def get_primary_type(types: Dict[str, List[Dict[str, str]]]) -> str:
custom_types = set(types.keys())
custom_types_that_are_deps = set()
for type_ in custom_types:
type_fields = types[type_]
for field in type_fields:
parsed_type = parse_core_array_type(field["type"])
if parsed_type in custom_types and parsed_type != type_:
custom_types_that_are_deps.add(parsed_type)
primary_type = list(custom_types.difference(custom_types_that_are_deps))
if len(primary_type) == 1:
return primary_type[0]
else:
raise ValueError("Unable to determine primary type")
def encode_field(
types: Dict[str, List[Dict[str, str]]],
name: str,
type_: str,
value: Any,
) -> Tuple[str, Union[int, bytes]]:
if type_ in types.keys():
# type is a custom type
if value is None:
return ("bytes32", b"\x00" * 32)
else:
return ("bytes32", keccak(encode_data(type_, types, value)))
elif type_ in ["string", "bytes"] and value is None:
return ("bytes32", b"")
# None is allowed only for custom and dynamic types
elif value is None:
raise ValueError(f"Missing value for field `{name}` of type `{type_}`")
elif is_array_type(type_):
# handle array type with non-array value
if not isinstance(value, list):
raise ValueError(
f"Invalid value for field `{name}` of type `{type_}`: "
f"expected array, got `{value}` of type `{type(value)}`"
)
parsed_type = parse_parent_array_type(type_)
type_value_pairs = [
encode_field(types, name, parsed_type, item) for item in value
]
if not type_value_pairs:
# the keccak hash of `encode((), ())`
return (
"bytes32",
b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501
)
data_types, data_hashes = zip(*type_value_pairs)
return ("bytes32", keccak(encode(data_types, data_hashes)))
elif type_ == "bool":
return (type_, bool(value))
# all bytes types allow hexstr and str values
elif type_.startswith("bytes"):
if not isinstance(value, bytes):
if is_0x_prefixed_hexstr(value):
value = to_bytes(hexstr=value)
elif isinstance(value, str):
value = to_bytes(text=value)
else:
if isinstance(value, int) and value < 0:
value = 0
value = to_bytes(value)
return (
# keccak hash if dynamic `bytes` type
("bytes32", keccak(value))
if type_ == "bytes"
# if fixed bytesXX type, do not hash
else (type_, value)
)
elif type_ == "string":
if isinstance(value, int):
value = to_bytes(value)
else:
value = to_bytes(text=value)
return ("bytes32", keccak(value))
# allow string values for int and uint types
elif type(value) == str and type_.startswith(("int", "uint")):
if is_0x_prefixed_hexstr(value):
return (type_, to_int(hexstr=value))
else:
return (type_, to_int(text=value))
return (type_, value)
def find_type_dependencies(type_, types, results=None):
if results is None:
results = set()
# a type must be a string
if not isinstance(type_, str):
raise ValueError(
"Invalid find_type_dependencies input: expected string, got "
f"`{type_}` of type `{type(type_)}`"
)
# get core type if it's an array type
type_ = parse_core_array_type(type_)
if (
# don't look for dependencies of solidity types
type_ in EIP712_SOLIDITY_TYPES
# found a type that's already been added
or type_ in results
):
return results
# found a type that isn't defined
elif type_ not in types:
raise ValueError(f"No definition of type `{type_}`")
results.add(type_)
for field in types[type_]:
find_type_dependencies(field["type"], types, results)
return results
def encode_type(type_: str, types: Dict[str, List[Dict[str, str]]]) -> str:
result = ""
unsorted_deps = find_type_dependencies(type_, types)
if type_ in unsorted_deps:
unsorted_deps.remove(type_)
deps = [type_] + sorted(list(unsorted_deps))
for type_ in deps:
children_list = []
for child in types[type_]:
child_type = child["type"]
child_name = child["name"]
children_list.append(f"{child_type} {child_name}")
result += f"{type_}({','.join(children_list)})"
return result
def hash_type(type_: str, types: Dict[str, List[Dict[str, str]]]) -> bytes:
return keccak(to_bytes(text=encode_type(type_, types)))
def encode_data(
type_: str,
types: Dict[str, List[Dict[str, str]]],
data: Dict[str, Any],
) -> bytes:
encoded_types: List[str] = ["bytes32"]
encoded_values: List[Union[bytes, int]] = [hash_type(type_, types)]
for field in types[type_]:
type, value = encode_field(
types, field["name"], field["type"], data.get(field["name"])
)
encoded_types.append(type)
encoded_values.append(value)
return encode(encoded_types, encoded_values)
def hash_struct(
type_: str,
types: Dict[str, List[Dict[str, str]]],
data: Dict[str, Any],
) -> bytes:
encoded = encode_data(type_, types, data)
return keccak(encoded)
def hash_eip712_message(
# returns the same hash as `hash_struct`, but automatically determines primary type
message_types: Dict[str, List[Dict[str, str]]],
message_data: Dict[str, Any],
) -> bytes:
primary_type = get_primary_type(message_types)
return keccak(encode_data(primary_type, message_types, message_data))
def hash_domain(domain_data: Dict[str, Any]) -> bytes:
eip712_domain_map = {
"name": {"name": "name", "type": "string"},
"version": {"name": "version", "type": "string"},
"chainId": {"name": "chainId", "type": "uint256"},
"verifyingContract": {"name": "verifyingContract", "type": "address"},
"salt": {"name": "salt", "type": "bytes32"},
}
for k in domain_data.keys():
if k not in eip712_domain_map.keys():
raise ValueError(f"Invalid domain key: `{k}`")
domain_types = {
"EIP712Domain": [
eip712_domain_map[k] for k in eip712_domain_map.keys() if k in domain_data
]
}
return hash_struct("EIP712Domain", domain_types, domain_data)

View File

@@ -0,0 +1,40 @@
from typing import (
Any,
)
from ...utils import (
is_hexstr,
)
def _get_eip712_solidity_types():
types = ["bool", "address", "string", "bytes", "uint", "int"]
ints = [f"int{(x + 1) * 8}" for x in range(32)]
uints = [f"uint{(x + 1) * 8}" for x in range(32)]
bytes_ = [f"bytes{x + 1}" for x in range(32)]
return types + ints + uints + bytes_
EIP712_SOLIDITY_TYPES = _get_eip712_solidity_types()
def is_array_type(type_: str) -> bool:
return type_.endswith("]")
def is_0x_prefixed_hexstr(value: Any) -> bool:
return is_hexstr(value) and value.startswith("0x")
# strip all brackets: Person[][] -> Person
def parse_core_array_type(type_: str) -> str:
if is_array_type(type_):
type_ = type_[: type_.index("[")]
return type_
# strip only last set of brackets: Person[3][1] -> Person[3]
def parse_parent_array_type(type_: str) -> str:
if is_array_type(type_):
type_ = type_[: type_.rindex("[")]
return type_

View File

@@ -0,0 +1,263 @@
from collections.abc import (
Mapping,
)
from typing import (
Any,
Dict,
NamedTuple,
)
import warnings
from ..typing import (
Address,
)
from ..utils.curried import (
ValidationError,
)
from ..hexbytes import (
HexBytes,
)
from .encode_typed_data.encoding_and_hashing import (
hash_domain,
hash_eip712_message,
)
# watch for updates to signature format
class SignableMessage(NamedTuple):
"""
A message compatible with EIP-191_ that is ready to be signed.
The properties are components of an EIP-191_ signable message. Other message formats
can be encoded into this format for easy signing. This data structure doesn't need
to know about the original message format. For example, you can think of
EIP-712 as compiling down to an EIP-191 message.
In typical usage, you should never need to create these by hand. Instead, use
one of the available encode_* methods in this module, like:
- :meth:`encode_typed_data`
.. _EIP-191: https://eips.ethereum.org/EIPS/eip-191
"""
version: bytes # must be length 1
header: bytes # aka "version specific data"
body: bytes # aka "data to sign"
def encode_typed_data(
domain_data: Dict[str, Any] = None,
message_types: Dict[str, Any] = None,
message_data: Dict[str, Any] = None,
full_message: Dict[str, Any] = None,
) -> SignableMessage:
r"""
Encode an EIP-712_ message in a manner compatible with other implementations
in use, such as the Metamask and Ethers ``signTypedData`` functions.
See the `EIP-712 spec <https://eips.ethereum.org/EIPS/eip-712>`_ for more information.
You may supply the information to be encoded in one of two ways:
As exactly three arguments:
- ``domain_data``, a dict of the EIP-712 domain data
- ``message_types``, a dict of custom types (do not include a ``EIP712Domain``
key)
- ``message_data``, a dict of the data to be signed
Or as a single argument:
- ``full_message``, a dict containing the following keys:
- ``types``, a dict of custom types (may include a ``EIP712Domain`` key)
- ``primaryType``, (optional) a string of the primary type of the message
- ``domain``, a dict of the EIP-712 domain data
- ``message``, a dict of the data to be signed
.. WARNING:: Note that this code has not gone through an external audit, and
the test cases are incomplete.
Type Coercion:
- For fixed-size bytes types, smaller values will be padded to fit in larger
types, but values larger than the type will raise ``ValueOutOfBounds``.
e.g., an 8-byte value will be padded to fit a ``bytes16`` type, but 16-byte
value provided for a ``bytes8`` type will raise an error.
- Fixed-size and dynamic ``bytes`` types will accept ``int``s. Any negative
values will be converted to ``0`` before being converted to ``bytes``
- ``int`` and ``uint`` types will also accept strings. If prefixed with ``"0x"``
, the string will be interpreted as hex. Otherwise, it will be interpreted as
decimal.
Noteable differences from ``signTypedData``:
- Custom types that are not alphanumeric will encode differently.
- Custom types that are used but not defined in ``types`` will not encode.
:param domain_data: EIP712 domain data
:param message_types: custom types used by the `value` data
:param message_data: data to be signed
:param full_message: a dict containing all data and types
:returns: a ``SignableMessage``, an encoded message ready to be signed
.. doctest:: python
>>> # examples of basic usage
>>> from eth_account import Account
>>> from .messages import encode_typed_data
>>> # 3-argument usage
>>> # all domain properties are optional
>>> domain_data = {
... "name": "Ether Mail",
... "version": "1",
... "chainId": 1,
... "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
... "salt": b"decafbeef",
... }
>>> # custom types
>>> message_types = {
... "Person": [
... {"name": "name", "type": "string"},
... {"name": "wallet", "type": "address"},
... ],
... "Mail": [
... {"name": "from", "type": "Person"},
... {"name": "to", "type": "Person"},
... {"name": "contents", "type": "string"},
... ],
... }
>>> # the data to be signed
>>> message_data = {
... "from": {
... "name": "Cow",
... "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
... },
... "to": {
... "name": "Bob",
... "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
... },
... "contents": "Hello, Bob!",
... }
>>> key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>>> signable_message = encode_typed_data(domain_data, message_types, message_data)
>>> signed_message = Account.sign_message(signable_message, key)
>>> signed_message.messageHash
HexBytes('0xc5bb16ccc59ae9a3ad1cb8343d4e3351f057c994a97656e1aff8c134e56f7530')
>>> # the message can be signed in one step using Account.sign_typed_data
>>> signed_typed_data = Account.sign_typed_data(key, domain_data, message_types, message_data)
>>> signed_typed_data == signed_message
True
>>> # 1-argument usage
>>> # all domain properties are optional
>>> full_message = {
... "types": {
... "EIP712Domain": [
... {"name": "name", "type": "string"},
... {"name": "version", "type": "string"},
... {"name": "chainId", "type": "uint256"},
... {"name": "verifyingContract", "type": "address"},
... {"name": "salt", "type": "bytes32"},
... ],
... "Person": [
... {"name": "name", "type": "string"},
... {"name": "wallet", "type": "address"},
... ],
... "Mail": [
... {"name": "from", "type": "Person"},
... {"name": "to", "type": "Person"},
... {"name": "contents", "type": "string"},
... ],
... },
... "primaryType": "Mail",
... "domain": {
... "name": "Ether Mail",
... "version": "1",
... "chainId": 1,
... "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
... "salt": b"decafbeef"
... },
... "message": {
... "from": {
... "name": "Cow",
... "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
... },
... "to": {
... "name": "Bob",
... "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
... },
... "contents": "Hello, Bob!",
... },
... }
>>> signable_message_2 = encode_typed_data(full_message=full_message)
>>> signed_message_2 = Account.sign_message(signable_message_2, key)
>>> signed_message_2.messageHash
HexBytes('0xc5bb16ccc59ae9a3ad1cb8343d4e3351f057c994a97656e1aff8c134e56f7530')
>>> signed_message_2 == signed_message
True
>>> # the full_message can be signed in one step using Account.sign_typed_data
>>> signed_typed_data_2 = Account.sign_typed_data(key, domain_data, message_types, message_data)
>>> signed_typed_data_2 == signed_message_2
True
.. _EIP-712: https://eips.ethereum.org/EIPS/eip-712
""" # noqa: E501
if full_message is not None:
if (
domain_data is not None
or message_types is not None
or message_data is not None
):
raise ValueError(
"You may supply either `full_message` as a single argument or "
"`domain_data`, `message_types`, and `message_data` as three arguments,"
" but not both."
)
full_message_types = full_message["types"].copy()
full_message_domain = full_message["domain"].copy()
# If EIP712Domain types were provided, check that they match the domain data
if "EIP712Domain" in full_message_types:
domain_data_keys = list(full_message_domain.keys())
domain_types_keys = [
field["name"] for field in full_message_types["EIP712Domain"]
]
if set(domain_data_keys) != (set(domain_types_keys)):
raise ValidationError(
"The fields provided in `domain` do not match the fields provided"
" in `types.EIP712Domain`. The fields provided in `domain` were"
f" `{domain_data_keys}`, but the fields provided in "
f"`types.EIP712Domain` were `{domain_types_keys}`."
)
full_message_types.pop("EIP712Domain", None)
# If primaryType was provided, check that it matches the derived primaryType
if "primaryType" in full_message:
derived_primary_type = get_primary_type(full_message_types)
provided_primary_type = full_message["primaryType"]
if derived_primary_type != provided_primary_type:
raise ValidationError(
"The provided `primaryType` does not match the derived "
"`primaryType`. The provided `primaryType` was "
f"`{provided_primary_type}`, but the derived `primaryType` was "
f"`{derived_primary_type}`."
)
parsed_domain_data = full_message_domain
parsed_message_types = full_message_types
parsed_message_data = full_message["message"]
else:
parsed_domain_data = domain_data
parsed_message_types = message_types
parsed_message_data = message_data
return SignableMessage(
HexBytes(b"\x01"),
hash_domain(parsed_domain_data),
hash_eip712_message(parsed_message_types, parsed_message_data),
)