Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
TypedData
EIP-712 typed structured data - enables human-readable, type-safe signatures in Ethereum.
Overview
TypedData implements EIP-712 for typed structured data signing. Instead of signing opaque hex strings, users sign structured data with clear field names and types. This improves UX and security by making signatures human-readable in wallets like MetaMask.
Type Definition
type TypedDataType<T = Record<string, unknown>> = {
readonly types: {
readonly EIP712Domain: readonly TypedDataField[];
readonly [key: string]: readonly TypedDataField[];
};
readonly primaryType: string;
readonly domain: DomainType;
readonly message: T;
};
type TypedDataField = {
readonly name: string;
readonly type: string;
};
Usage
Define Typed Data
import * as TypedData from './primitives/TypedData/index.js';
const typedData = TypedData.from({
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,
},
message: {
from: {
name: 'Alice',
wallet: '0xaaa...',
},
to: {
name: 'Bob',
wallet: '0xbbb...',
},
contents: 'Hello, Bob!',
},
});
Hash for Signing
import { keccak256 } from './crypto/keccak256/index.js';
const hash = TypedData.hash(typedData, { keccak256 });
// Sign with wallet
const signature = await wallet.signMessage(hash);
Encode Message
const encoded = TypedData.encode(typedData, { keccak256 });
Validate Structure
TypedData.validate(typedData); // throws if invalid
Type System
Atomic Types
// Integers
'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256'
'int8', 'int16', 'int32', 'int64', 'int128', 'int256'
// Other
'address' // Ethereum address
'bool' // Boolean
'string' // UTF-8 string
'bytes' // Dynamic bytes
// Fixed bytes
'bytes1', 'bytes2', ..., 'bytes32'
Struct Types
Define custom types with named fields:
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
]
Array Types
Use [] suffix for arrays:
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
]
Common Use Cases
ERC-20 Permit (Gasless Approval)
const permitData = TypedData.from({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
domain: {
name: 'USD Coin',
version: '2',
chainId: 1,
verifyingContract: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
},
message: {
owner: '0x123...',
spender: '0x456...',
value: 1000000n,
nonce: 0n,
deadline: 1234567890n,
},
});
const hash = TypedData.hash(permitData, { keccak256 });
const signature = await wallet.signMessage(hash);
// Submit permit transaction
await token.permit(owner, spender, value, deadline, v, r, s);
DAI Permit
const daiPermit = TypedData.from({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Permit: [
{ name: 'holder', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'expiry', type: 'uint256' },
{ name: 'allowed', type: 'bool' },
],
},
primaryType: 'Permit',
domain: {
name: 'Dai Stablecoin',
version: '1',
chainId: 1,
verifyingContract: '0x6b175474e89094c44da98b954eedeac495271d0f',
},
message: {
holder: '0x123...',
spender: '0x456...',
nonce: 0n,
expiry: 0n, // 0 = infinite
allowed: true,
},
});
const metaTx = TypedData.from({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
ForwardRequest: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'gas', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
},
primaryType: 'ForwardRequest',
domain: {
name: 'MinimalForwarder',
version: '0.0.1',
chainId: 1,
verifyingContract: '0x123...',
},
message: {
from: '0x123...',
to: '0x456...',
value: 0n,
gas: 100000n,
nonce: 0n,
data: '0xabcd...',
},
});
Voting/Governance
const vote = TypedData.from({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Ballot: [
{ name: 'proposalId', type: 'uint256' },
{ name: 'support', type: 'uint8' },
],
},
primaryType: 'Ballot',
domain: {
name: 'MyDAO',
version: '1',
chainId: 1,
verifyingContract: '0x123...',
},
message: {
proposalId: 42n,
support: 1, // 0=against, 1=for, 2=abstain
},
});
Hash Calculation
EIP-712 hash is computed as:
hash = keccak256(
"\x19\x01" ||
domainSeparator ||
keccak256(encodeData(primaryType, message, types))
)
Where:
\x19\x01 - EIP-191 structured data prefix
domainSeparator - keccak256(encodeData(‘EIP712Domain’, domain))
encodeData() - Recursive encoding of message fields
Type Encoding
Simple Type
encodeType('Person', types)
// "Person(string name,address wallet)"
Nested Types
encodeType('Mail', types)
// "Mail(Person from,Person to,string contents)Person(string name,address wallet)"
Dependencies are sorted alphabetically after the primary type.
Security
Benefits
- Human-readable - Users see structured data, not hex
- Type-safe - Field types are validated
- Domain-specific - Signatures bound to contract/chain
- Replay-protected - Domain separator prevents replay
Validation
Always validate typed data:
try {
TypedData.validate(typedData);
} catch (error) {
console.error('Invalid typed data:', error);
}
MetaMask shows structured data clearly:
Sign TypedData
Domain
name: "USD Coin"
version: "2"
chainId: 1
verifyingContract: 0xa0b8...
Message
owner: 0x123...
spender: 0x456...
value: 1000000
nonce: 0
deadline: 1234567890
Error Handling
TypedData operations throw typed errors for precise error handling:
InvalidTypedDataError
Thrown when typed data structure is invalid.
import * as TypedData from './primitives/TypedData/index.js';
import { InvalidTypedDataError } from './primitives/TypedData/errors.js';
try {
TypedData.from({ types: undefined, primaryType: 'Mail', domain: {}, message: {} });
} catch (e) {
if (e instanceof InvalidTypedDataError) {
console.log(e.name); // "InvalidTypedDataError"
console.log(e.code); // "INVALID_TYPED_DATA"
console.log(e.value); // The invalid typed data object
console.log(e.expected); // "valid EIP-712 typed data"
}
}
Validation Errors
The validate() function throws InvalidTypedDataError for various issues:
try {
TypedData.validate(typedData);
} catch (e) {
if (e instanceof InvalidTypedDataError) {
// Common validation messages:
// - "types must be an object"
// - "types must include EIP712Domain"
// - "primaryType must be a string"
// - "primaryType 'X' not found in types"
// - "domain must be an object"
// - "message is required"
// - "type 'X' not found in types"
console.log(e.message);
}
}
Specification
See Also