Introduction: Tricks for Testing Type Strings

A single typo in a type hash is a medium-severity bug, yet verifying that hash by hand is tedious and error-prone. The most time-consuming part of EIP-712 debugging is figuring out what actually went into the digest. Wake’s Struct base class exposes encode_eip712_type and encode_eip712_data so you can see exactly what is being hashed, compare it directly to Solidity, and confirm that signatures match before deployment. If you need a refresher on raw versus structured signatures, start with Signing Data in Wake: Raw, Structured, and Hash Flows.

Testing Context And Preconditions

  • Run tests inside the Wake testing framework so Struct, Account, and chain fixtures are available.
  • Use dataclasses that subclass Struct. Field order and field names must mirror the Solidity structs verified on-chain.
  • Build an Eip712Domain that matches the contract exactly. The name, version, chainId, and verifyingContract values must be identical to the Solidity side.
  • When debugging failures, print the type string and call traces in tests to quickly spot schema or domain mismatches.

Technical Background: What The Helpers Do

encode_eip712_type builds the canonical type string, including referenced structs, instead of returning a hash. This visibility lets you inspect ordering and field names while debugging. encode_eip712_data returns the packed bytes that feed into the final digest. Together, these helpers mirror the Solidity side of hashTypedData and make it straightforward to assert that tests and contracts use the same preimage, even with nested structs or proxy-based verifying contracts.

Build A Typed Struct You Can Inspect

The base Struct class lives in wake.testing. Subclass it with dataclasses and mirror the Solidity names exactly. When a Python keyword differs from Solidity, use field(metadata={"original_name": ...}) to preserve the on-chain schema.

from dataclasses import dataclass, field
from wake.testing import *

@dataclass
class Person(Struct):
    name: str
    wallet: Address

@dataclass
class Mail(Struct):
    from_: Person = field(metadata={"original_name": "from"})
    to: Person
    contents: str

mail = Mail(
    from_=Person("Alice", Address(1)),
    to=Person("Bob", Address(2)),
    contents="Hello",
)

print(mail.encode_eip712_type())
# Mail(Person from,Person to,string contents)Person(string name,address wallet)

Keeping the type string human-readable helps you spot mismatches early, such as incorrect data types, stray spaces, missing nested structs, or renamed variables. Once the string matches the Solidity side, you can safely hash it.

Hash Typed Data The Same Way As Solidity

With the type string verified, build the type hash and data bytes exactly as the contract does. The snippet below mirrors the EIP-712 pipeline used inside hashTypedData.

type_hash = keccak256(mail.encode_eip712_type().encode())
data = mail.encode_eip712_data()

typed_data_hash = keccak256(abi.encode_packed(type_hash, data))
domain = Eip712Domain(
    name="Mail",
    version="1",
    chainId=chain.chain_id,
    verifyingContract=Address(0x1234),
)

signature = Account.new().sign_structured(mail, domain)

Because encode_eip712_data returns the exact bytes that feed into the digest, you can log and compare them against a Solidity helper or a live contract call without guessing.

Cross-Check Against A Contract

End-to-end assertions provide the strongest proof. The fuzz test below signs the same struct in two ways, using a manual hash and the high-level sign_structured helper, and verifies that both match the contract’s hashTypedData implementation on a base contract.

from wake.testing import *
from wake.testing.fuzzing import *
from dataclasses import dataclass, field

from pytypes.src.utils.ERC1967Factory import ERC1967Factory
from pytypes.ext.wake_tests.helpers.EIP712Mock import EIP712Mock

@dataclass
class Person(Struct):
    name: str
    wallet: Address

@dataclass
class Mail(Struct):
    from_: Person = field(metadata={"original_name": "from"})
    to: Person
    contents: str

class Eip712FuzzTest(FuzzTest):
    def __init__(self):
        self._factory = ERC1967Factory.deploy()

    def pre_sequence(self) -> None:
        self._impl = EIP712Mock.deploy()
        self._signer = Account.new()
        self._proxy = EIP712Mock(self._factory.deploy_(self._impl, self._signer).return_value)

    @flow()
    def sign_flow(self, mail: Mail) -> None:
        type_hash = keccak256(mail.encode_eip712_type().encode())
        mail_hash = keccak256(abi.encode_packed(type_hash, mail.encode_eip712_data()))

        for target in (self._impl, self._proxy):
            manual = self._signer.sign_hash(target.hashTypedData(mail_hash))
            structured = self._signer.sign_structured(
                mail,
                Eip712Domain(
                    name=target.NAME(),
                    version=target.VERSION(),
                    chainId=chain.chain_id,
                    verifyingContract=target.address,
                ),
            )
            assert manual == structured

@chain.connect()
def test_eip712_fuzz():
    Eip712FuzzTest().run(100, 100000)

Key points:

  • The type string is visible, so you can confirm that nested structs are included.
  • The data bytes match Solidity’s preimage exactly, rather than coming from an opaque helper.

Prevention Techniques For Stable Signatures

  • Align domains: name, version, chainId, and verifyingContract must match the contract’s EIP712Domain exactly.
  • Keep names canonical: use metadata={"original_name": ...} to prevent Python keywords from silently changing Solidity field names.
  • Log the type string: print encode_eip712_type() in failing tests to locate ordering or casing issues quickly.
  • Avoid blind hashes: prefer sign_structured unless an external API requires a precomputed digest.
  • Assert preimages: recompute keccak256(abi.encodePacked(typeHash, data)) in Solidity and assert that it equals Wake’s output before verifying a signature.
  • Fuzz nested structs: generate combinations of optional or nested fields to catch missing struct definitions in the type string.

Conclusion

Wake’s Struct.encode_eip712_type and encode_eip712_data remove guesswork from typed-data tests. By exposing the type string and preimage bytes, they let you align precisely with Solidity, validate contract domains, and prove that signatures match without trial and error. Keep domains consistent, log type strings, and assert digests on both sides to ship reliable permit and meta-transaction flows.

Additional Resources