add
This commit is contained in:
3
ccxt/static_dependencies/ethereum/account/__init__.py
Normal file
3
ccxt/static_dependencies/ethereum/account/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .messages import *
|
||||
|
||||
__all__ = ["messages"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from .encoding_and_hashing import (
|
||||
hash_domain,
|
||||
hash_eip712_message,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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_
|
||||
263
ccxt/static_dependencies/ethereum/account/messages.py
Normal file
263
ccxt/static_dependencies/ethereum/account/messages.py
Normal 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),
|
||||
)
|
||||
0
ccxt/static_dependencies/ethereum/account/py.typed
Normal file
0
ccxt/static_dependencies/ethereum/account/py.typed
Normal file
Reference in New Issue
Block a user