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.
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.
EIP-712 enables human-readable message signing with domain separation. Instead of signing opaque hex blobs, users see structured data in their wallet prompts.
Domain Separator
The domain separator cryptographically binds signatures to specific contract + chain combinations, preventing replay attacks.
import * as EIP712 from '@voltaire/crypto/eip712';
import * as Address from '@voltaire/primitives/address';
const domain = {
name: 'MyDApp', // Application name
version: '1', // Version string
chainId: 1n, // Chain ID (bigint)
verifyingContract: Address.from('0x742d35Cc6634C0532925a3b844Bc9e7595f251e3')
};
// Hash the domain separator
const domainHash = EIP712.Domain.hash(domain);
// Returns: Uint8Array(32) - keccak256 hash
Domain fields:
- name: DApp name shown to user
- version: Versioning for signature schema changes
- chainId: Prevents cross-chain replay (mainnet=1, polygon=137)
- verifyingContract: Contract that will verify the signature
- salt (optional): Additional entropy for extra separation
Type Definitions
Define message structure using Solidity-like type definitions:
const types = {
// Simple struct
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
],
// Struct with nested reference
Mail: [
{ name: 'from', type: 'Person' }, // References Person type
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' }
]
};
// Encode type for debugging
const typeString = EIP712.encodeType('Mail', types);
// "Mail(Person from,Person to,string contents)Person(string name,address wallet)"
// Get type hash
const typeHash = EIP712.hashType('Mail', types);
Supported types:
- Integers:
uint8 through uint256, int8 through int256
- Address:
address
- Boolean:
bool
- Fixed bytes:
bytes1 through bytes32
- Dynamic:
bytes, string
- Arrays:
type[], type[N]
- Custom structs: Referenced by name
Hashing Typed Data
Complete typed data combines domain, types, primary type, and message:
import * as EIP712 from '@voltaire/crypto/eip712';
import * as Address from '@voltaire/primitives/address';
const typedData = {
domain: {
name: 'Ether Mail',
version: '1',
chainId: 1n,
verifyingContract: Address.from('0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC')
},
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' }
]
},
primaryType: 'Mail',
message: {
from: {
name: 'Alice',
wallet: Address.from('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826')
},
to: {
name: 'Bob',
wallet: Address.from('0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB')
},
contents: 'Hello, Bob!'
}
};
// Hash for signing: keccak256("\x19\x01" || domainSeparator || hashStruct(message))
const hash = EIP712.hashTypedData(typedData);
// Returns: Uint8Array(32)
Signing Typed Data
Sign with a private key to produce ECDSA signature:
import * as EIP712 from '@voltaire/crypto/eip712';
import * as PrivateKey from '@voltaire/primitives/private-key';
// Your private key (32 bytes)
const privateKey = PrivateKey.from('0x...');
// Sign - produces { r, s, v }
const signature = EIP712.signTypedData(typedData, privateKey);
console.log(signature.r); // Uint8Array(32)
console.log(signature.s); // Uint8Array(32)
console.log(signature.v); // 27 or 28
The signature can be submitted to:
- Smart contracts via
ecrecover
- Relayers for meta-transactions
- Order books for DEX orders
Verifying Typed Signatures
Recover the signer’s address and verify it matches expected:
import * as EIP712 from '@voltaire/crypto/eip712';
import * as Address from '@voltaire/primitives/address';
// Recover signer address from signature
const recoveredAddress = EIP712.recoverAddress(signature, typedData);
// Verify against expected signer
const expectedSigner = Address.from('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826');
const isValid = EIP712.verifyTypedData(signature, typedData, expectedSigner);
if (isValid) {
console.log('Signature is valid');
} else {
console.log('Invalid signature or wrong signer');
}
Verification uses constant-time comparison to prevent timing attacks.
Complete Example: ERC-2612 Permit
Gasless token approvals using EIP-712:
import * as EIP712 from '@voltaire/crypto/eip712';
import * as Address from '@voltaire/primitives/address';
import * as PrivateKey from '@voltaire/primitives/private-key';
// Token holder's private key
const ownerKey = PrivateKey.from('0x...');
// Permit message for gasless approval
const permitData = {
domain: {
name: 'USD Coin',
version: '2',
chainId: 1n,
verifyingContract: Address.from('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')
},
types: {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
},
primaryType: 'Permit',
message: {
owner: Address.from('0xOwnerAddress...'),
spender: Address.from('0xSpenderAddress...'),
value: 1000000n, // 1 USDC (6 decimals)
nonce: 0n, // Current nonce from contract
deadline: 1735689600n // Unix timestamp expiry
}
};
// Sign the permit
const signature = EIP712.signTypedData(permitData, ownerKey);
// Submit to contract: permit(owner, spender, value, deadline, v, r, s)
// Relayer pays gas, owner's tokens get approved
Validation
Validate typed data structure before signing:
import * as EIP712 from '@voltaire/crypto/eip712';
try {
EIP712.validate(typedData);
console.log('Valid typed data structure');
} catch (error) {
console.error('Invalid:', error.message);
}
Validation checks:
- Primary type exists in types
- All referenced types are defined
- Message fields match type definitions
- No circular type references
Error Handling
import * as EIP712 from '@voltaire/crypto/eip712';
import {
Eip712Error,
Eip712TypeNotFoundError,
Eip712InvalidMessageError,
Eip712EncodingError
} from '@voltaire/crypto/eip712';
try {
const hash = EIP712.hashTypedData(typedData);
} catch (error) {
if (error instanceof Eip712TypeNotFoundError) {
// Referenced type not in types object
} else if (error instanceof Eip712InvalidMessageError) {
// Message doesn't match type definition
} else if (error instanceof Eip712EncodingError) {
// Value encoding failed (wrong type, size, etc.)
}
}
Security Best Practices
- Always include deadlines - Prevent signature reuse after expiry
- Use nonces - Contract tracks nonces to prevent replay
- Validate before signing - Call
validate() on user-provided data
- Verify recovered address - Never trust signatures without verification
- Include chainId - Prevents cross-chain replay attacks
// Good: Time-bounded with nonce
message: {
action: 'approve',
nonce: await contract.nonces(userAddress),
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour
}
API Reference
| Function | Description |
|---|
hashTypedData(typedData) | Hash complete typed data for signing |
signTypedData(typedData, privateKey) | Sign typed data, returns {r, s, v} |
verifyTypedData(signature, typedData, address) | Verify signature matches expected signer |
recoverAddress(signature, typedData) | Recover signer address from signature |
Domain.hash(domain) | Hash domain separator |
encodeType(primaryType, types) | Get canonical type encoding string |
hashType(primaryType, types) | Hash type encoding |
hashStruct(primaryType, message, types) | Hash struct data |
encodeValue(type, value, types) | Encode single value |
validate(typedData) | Validate typed data structure |
format(typedData) | Format for human-readable display |