From ccfca49ea091a718be822c654c6a5314ec899276 Mon Sep 17 00:00:00 2001 From: John Peterson <98187317+John-peterson-coinbase@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:25:02 -0400 Subject: [PATCH] feat(PSDK-499): Message Hashing Utilities for EIP-191/712 (#23) --- CHANGELOG.md | 1 + cdp/__init__.py | 3 +++ cdp/hash_utils.py | 40 ++++++++++++++++++++++++++++++++++++ tests/test_hash_utils.py | 44 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 cdp/hash_utils.py create mode 100644 tests/test_hash_utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 585a06a..de6e6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Contract invocation support. - Arbitrary message signing support. - Deploy ERC20, ERC721, and ERC1155 smart contracts. +- Hashing utilities for EIP-191 / EIP-712 data messages. ### Fixed diff --git a/cdp/__init__.py b/cdp/__init__.py index 0c2f049..33a4168 100644 --- a/cdp/__init__.py +++ b/cdp/__init__.py @@ -6,6 +6,7 @@ from cdp.cdp import Cdp from cdp.contract_invocation import ContractInvocation from cdp.faucet_transaction import FaucetTransaction +from cdp.hash_utils import hash_message, hash_typed_data_message from cdp.payload_signature import PayloadSignature from cdp.smart_contract import SmartContract from cdp.sponsored_send import SponsoredSend @@ -34,4 +35,6 @@ "SponsoredSend", "PayloadSignature", "SmartContract", + "hash_message", + "hash_typed_data_message", ] diff --git a/cdp/hash_utils.py b/cdp/hash_utils.py new file mode 100644 index 0000000..70879c6 --- /dev/null +++ b/cdp/hash_utils.py @@ -0,0 +1,40 @@ +from typing import Any + +from eth_account.messages import _hash_eip191_message, encode_defunct, encode_typed_data +from eth_utils import to_hex + + +def hash_message(message_text: str) -> str: + """Hashes a message according to EIP-191 and returns the hash as a 0x-prefixed hexadecimal string. + + This function prefixes the message with the standard Ethereum message prefix and hashes it using Keccak-256. + + Args: + message_text (str): The message to hash. + + Returns: + str: The 0x-prefixed hexadecimal string of the message hash. + + """ + message = encode_defunct(text=message_text) + message_hash = _hash_eip191_message(message) + + return to_hex(message_hash) + + +def hash_typed_data_message(typed_data: dict[str, Any]) -> str: + """Hashes typed data according to EIP-712 and returns the hash as a 0x-prefixed hexadecimal string. + + This function encodes the typed data as per EIP-712 and hashes it using Keccak-256. + + Args: + typed_data (dict): The typed data to hash, following the EIP-712 specification. + + Returns: + str: The 0x-prefixed hexadecimal string of the typed data hash. + + """ + typed_data_message = encode_typed_data(full_message=typed_data) + typed_data_message_hash = _hash_eip191_message(typed_data_message) + + return to_hex(typed_data_message_hash) diff --git a/tests/test_hash_utils.py b/tests/test_hash_utils.py new file mode 100644 index 0000000..318d7ff --- /dev/null +++ b/tests/test_hash_utils.py @@ -0,0 +1,44 @@ +from cdp.hash_utils import hash_message, hash_typed_data_message + + +def test_hash_message(): + """Test hash_message utility method.""" + message_hash = hash_message("Hello, EIP-191!") + assert message_hash == "0x54c2490066f5b62dd2e2ea2310c2b3d63039bbce5e3f96175a1b66a0c484e74f" + + +def test_hash_typed_data_message(): + """Test hash_typed_data_message utility method.""" + typed_data = { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + "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", + }, + "message": { + "from": {"name": "Alice", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"}, + "to": {"name": "Bob", "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"}, + "contents": "Hello, Bob!", + }, + } + typed_data_message_hash = hash_typed_data_message(typed_data) + assert ( + typed_data_message_hash + == "0x4644890d40a2ebb8cbf599801c4412c03f8027f04044d0dd12a0605fbd72bb8b" + )