EIP-712 Typed Data
Overview
Brane provides two approaches for EIP-712 signing:
| Approach | Use Case | Type Safety |
|---|---|---|
| Type-Safe API | Known message structures (e.g., Permit, Order) | Compile-time |
| Dynamic API | Wallet-style signing from dapp JSON requests | Runtime |
Type-Safe API
Use Java records with TypeDefinition for compile-time type safety. This is the recommended approach when you know the message structure at compile time.
Defining a Type
import sh.brane.core.crypto.eip712.TypeDefinition;
import sh.brane.core.crypto.eip712.TypedDataField;
import sh.brane.core.types.Address;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
// Define a Permit record matching the Solidity struct
public record Permit(
Address owner,
Address spender,
BigInteger value,
BigInteger nonce,
BigInteger deadline) {
// Map Java record to EIP-712 struct format
public static final TypeDefinition<Permit> DEFINITION = TypeDefinition.forRecord(
Permit.class,
"Permit",
Map.of("Permit", List.of(
TypedDataField.of("owner", "address"),
TypedDataField.of("spender", "address"),
TypedDataField.of("value", "uint256"),
TypedDataField.of("nonce", "uint256"),
TypedDataField.of("deadline", "uint256"))));
}Nested Types
For messages with nested structs, include all referenced types in the types map:
public record Person(String name, Address wallet) {}
public record Mail(Person from, Person to, String contents) {
public static final TypeDefinition<Mail> DEFINITION = TypeDefinition.forRecord(
Mail.class,
"Mail",
Map.of(
"Mail", List.of(
TypedDataField.of("from", "Person"),
TypedDataField.of("to", "Person"),
TypedDataField.of("contents", "string")),
"Person", List.of(
TypedDataField.of("name", "string"),
TypedDataField.of("wallet", "address"))));
}Signing
import sh.brane.core.crypto.PrivateKeySigner;
import sh.brane.core.crypto.Signature;
import sh.brane.core.crypto.eip712.Eip712Domain;
import sh.brane.core.crypto.eip712.TypedData;
import sh.brane.core.types.Address;
import sh.brane.core.types.Hash;
// Create signer
var signer = new PrivateKeySigner("0x...");
// Define domain separator
Address tokenAddress = new Address("0x6B175474E89094C44Da98b954EecdeAC495271dF");
Eip712Domain domain = Eip712Domain.builder()
.name("Dai Stablecoin")
.version("1")
.chainId(1L)
.verifyingContract(tokenAddress)
.build();
// Create permit message
var permit = new Permit(
signer.address(), // owner
new Address("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), // spender
BigInteger.valueOf(1_000_000), // value
BigInteger.ZERO, // nonce
BigInteger.valueOf(Long.MAX_VALUE) // deadline
);
// Create TypedData and sign
TypedData<Permit> typedData = TypedData.create(domain, Permit.DEFINITION, permit);
// Get hash (useful for verification)
Hash hash = typedData.hash();
// Sign
Signature sig = typedData.sign(signer);Extracting Signature Components
The permit() function expects (owner, spender, value, deadline, v, r, s):
byte[] r = sig.r();
byte[] s = sig.s();
int v = sig.v(); // 27 or 28Dynamic API (JSON Parsing)
Use TypedDataJson for wallet-style signing when receiving JSON from dapps via WalletConnect or eth_signTypedData_v4.
Parsing JSON
import sh.brane.core.crypto.eip712.TypedData;
import sh.brane.core.crypto.eip712.TypedDataJson;
import sh.brane.core.crypto.eip712.TypedDataPayload;
String jsonFromDapp = """
{
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"primaryType": "Mail",
"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"}
]
},
"message": {
"from": {
"name": "Alice",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}
""";
// Parse and create signable TypedData in one step
TypedData<?> typedData = TypedDataJson.parseAndValidate(jsonFromDapp);
// Hash and sign
Hash hash = typedData.hash();
Signature sig = typedData.sign(signer);Two-Step Parsing
For displaying request details before signing (wallet UX):
// Step 1: Parse to inspect
TypedDataPayload payload = TypedDataJson.parse(jsonFromDapp);
// Display for user approval
System.out.println("Domain: " + payload.domain().name());
System.out.println("Chain ID: " + payload.domain().chainId());
System.out.println("Contract: " + payload.domain().verifyingContract().value());
System.out.println("Primary Type: " + payload.primaryType());
System.out.println("Message: " + payload.message());
// Step 2: After user approval, validate and sign
TypedData<?> typedData = TypedDataJson.parseAndValidate(jsonFromDapp);
Signature sig = typedData.sign(signer);Real-World Examples
The JSON format supports complex protocol messages like:
- Uniswap Permit2: Token approval signatures
- OpenSea Seaport: NFT marketplace orders
- CoW Protocol: DEX swap orders
// Permit2 example
String permit2Request = """
{
"domain": {
"name": "Permit2",
"chainId": 1,
"verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
},
"primaryType": "PermitSingle",
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"PermitSingle": [
{"name": "details", "type": "PermitDetails"},
{"name": "spender", "type": "address"},
{"name": "sigDeadline", "type": "uint256"}
],
"PermitDetails": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint160"},
{"name": "expiration", "type": "uint48"},
{"name": "nonce", "type": "uint48"}
]
},
"message": {
"details": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "1461501637330902918203684832716283019655932542975",
"expiration": "1735689600",
"nonce": "0"
},
"spender": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
"sigDeadline": "1704067200"
}
}
""";
TypedData<?> typedData = TypedDataJson.parseAndValidate(permit2Request);
Signature sig = typedData.sign(signer);Domain Separator Configuration
The domain separator uniquely identifies the protocol/contract that will verify the signature. All fields are optional but should match what the verifying contract expects.
Available Fields
| Field | Type | Description |
|---|---|---|
name | String | Protocol/dApp name |
version | String | Signing domain version |
chainId | Long | EIP-155 chain ID |
verifyingContract | Address | Contract that verifies the signature |
salt | Hash | Disambiguation salt (rarely used) |
Builder API
import sh.brane.core.crypto.eip712.Eip712Domain;
import sh.brane.core.types.Address;
// Minimal domain (e.g., Permit2 style)
Eip712Domain minimalDomain = Eip712Domain.builder()
.name("Permit2")
.chainId(1L)
.verifyingContract(new Address("0x000000000022D473030F116dDEE9F6B43aC78BA3"))
.build();
// Full domain (e.g., DAI permit style)
Eip712Domain fullDomain = Eip712Domain.builder()
.name("Dai Stablecoin")
.version("1")
.chainId(1L)
.verifyingContract(new Address("0x6B175474E89094C44Da98b954EecdeAC495271dF"))
.build();
// With salt (rare)
Eip712Domain domainWithSalt = Eip712Domain.builder()
.name("MyProtocol")
.version("1")
.chainId(1L)
.verifyingContract(contractAddress)
.salt(new Hash("0x..."))
.build();Computing the Domain Separator Hash
Use separator() to compute the domain separator hash. This is useful for verifying against a contract's DOMAIN_SEPARATOR constant:
Eip712Domain domain = Eip712Domain.builder()
.name("Dai Stablecoin")
.version("1")
.chainId(1L)
.verifyingContract(new Address("0x6B175474E89094C44Da98b954EecdeAC495271dF"))
.build();
// Get the domain separator hash
Hash domainSeparator = domain.separator();
// Compare with contract's DOMAIN_SEPARATOR
// Hash contractSeparator = contract.DOMAIN_SEPARATOR();
// assert domainSeparator.equals(contractSeparator);Accessing TypedData Properties
TypedData<?> typedData = TypedDataJson.parseAndValidate(json);
// Access components
Eip712Domain domain = typedData.domain();
String primaryType = typedData.primaryType();
Object message = typedData.message();
// Domain fields
String name = domain.name();
String version = domain.version();
Long chainId = domain.chainId();
Address contract = domain.verifyingContract();Error Handling
import sh.brane.core.error.Eip712Exception;
try {
TypedData<?> typedData = TypedDataJson.parseAndValidate(invalidJson);
} catch (Eip712Exception e) {
// Invalid JSON or missing required fields
System.err.println("EIP-712 error: " + e.getMessage());
}See Also
- Signers - Signer interface and PrivateKeySigner
- Sending Transactions - Transaction signing with Brane.Signer