Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

EIP-712 Typed Data

Overview

Brane provides two approaches for EIP-712 signing:

ApproachUse CaseType Safety
Type-Safe APIKnown message structures (e.g., Permit, Order)Compile-time
Dynamic APIWallet-style signing from dapp JSON requestsRuntime

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 28

Dynamic 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

FieldTypeDescription
nameStringProtocol/dApp name
versionStringSigning domain version
chainIdLongEIP-155 chain ID
verifyingContractAddressContract that verifies the signature
saltHashDisambiguation 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