EIP712++ (single-header) — Typed-data hashing & signing for Ethereum
Project goal
EIP712++ is a compact, proprietary C++ header that computes EIP-712 typed-data hashes and produces the EIP-191 digest ready for (r,s,v) signing. It gives you domain separators, hashStruct(...)
, and ergonomic encoders for Solidity types—so you can implement Permit (EIP-2612), meta-transactions, and off-chain authorizations without boilerplate.
Why it exists
- Typed-data is easy to get wrong. Field order, dynamic types, and hashing rules are strict; small mistakes = invalid signatures.
- You want a minimal, auditable API. One header, clear functions, predictable bytes in/bytes out.
- Common flows should be one-liners. domain → struct hash → EIP-191 digest → sign → (r,s,v).
Features
-
Domain separator Compute
EIP712Domain(...)
hash for(name, chainId, verifyingContract, …)
. -
hashStruct(...)
for messages Variadic encoding + Keccak-256 per spec:hashStruct(T) = keccak(typeHash || encodeData(T))
. -
Solidity-type encoding Atomic types (
bytes1..32
,uint8..256
,int8..256
,bool
,address
) via compile-time traits; dynamicbytes
/string
are hashed (keccak(contents)
), as required by EIP-712. -
Final signing digest Produce
keccak("\x19\x01" || domainSeparator || hashStruct(message))
(EIP-191). -
Interop with your signer Outputs raw byte strings (
bytes_t
) that plug into your existing(r,s,v)
signing stack (e.g., secp256k1 ECDSA). -
Single header Drop-in, exception-based errors, zero ceremony.
Minimal API (at a glance)
namespace eip712 {
using bytes_t = std::basic_string<uint8_t>;
// Atomic & dynamic encoders
bytes_t encode(const std::string& s); // keccak(s)
bytes_t encode(const bytes_t& b); // identity
template<class T> bytes_t encode(const T& v); // static Solidity types
// Variadic struct encoding / hashing
template<class... args_t>
bytes_t encode(const std::string& types, args_t&&... args);
template<class... args_t>
bytes_t hash(const std::string& types, args_t&&... args); // keccak(typeHash || encodeData)
}
Example — ERC-20 Permit (EIP-2612)
#include "eip712.hpp" // this header
#include "openssl.hpp" // your keccak + signer
using bytes_t = eip712::bytes_t;
// 1) Domain separator
// EIP712Domain(string name,uint256 chainId,address verifyingContract)
bytes_t domain_sep = eip712::hash(
"EIP712Domain(string name,uint256 chainId,address verifyingContract)",
std::string("MyToken"), // name
solidity::u256_t{1}, // chainId = 1
solidity::address_t{"0xabc...def"} // ERC-20 contract
);
// 2) Message struct hash
// Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
bytes_t permit_hash = eip712::hash(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)",
solidity::address_t{"0xdead...beef"}, // owner
solidity::address_t{"0xfeed...cafe"}, // spender
solidity::u256_t{"1000000000000000000"}, // value = 1e18
solidity::u256_t{42}, // nonce
solidity::u256_t{1699999999} // deadline (unix)
);
// 3) EIP-191 digest to sign
bytes_t digest = openssl::keccak(
std::string("\x19\x01", 2) + domain_sep + permit_hash
);
// 4) Sign (secp256k1 ECDSA) and recover
auto [r, s, v] = openssl::sign(digest, private_key_bytes);
auto pub = openssl::ecrecover(digest, v[0], r, s);
Notes: •
solidity::address_t / u256_t
stand for your static-type wrappers that map to Solidity encodings. •string
/bytes
fields are hashed (keccak) as specified by EIP-712. • The exact type string must match the Solidity struct, including field order.
separation** — domain vs. message hashes kept explicit to avoid cross-domain replay.
Typical uses
- Permit / Meta-tx: gasless approvals and relayed calls.
- Off-chain authz: sign intents that execute on-chain later.
- Custom typed messages: safe UX prompts in wallets.
Licensing & availability Proprietary, single-header library. Commercial licensing via Varga Consulting (Toronto, Canada). Contact: info@vargaconsulting.ca.