EIP-712 enables signing complex, structured data in a standardized way, providing better security and user experience compared to simple message signing. This walkthrough shows how to implement EIP-712 typed data signing using Para’s Ethers integration.

What is EIP-712?

EIP-712 is a standard for signing typed structured data, offering several advantages:
  • Structured data signing - Sign complex objects with multiple fields and nested structures
  • Domain separation - Prevents signature replay attacks across different applications or chains
  • Human-readable format - Users can see exactly what they’re signing in wallet interfaces
  • Type safety - Ensures data conforms to expected structure before signing

Common Use Cases

EIP-712 is commonly used for:
  • Meta-transactions - Gasless transactions with relayer support
  • Permit signatures - Token approvals without on-chain transactions
  • Attestations - Cryptographically signed claims or certificates
  • Voting systems - Off-chain voting with on-chain verification
  • Order signing - DEX orders and marketplace listings

Setup Requirements

You need an authenticated Para client and Ethers provider to implement EIP-712 signing.

Install Dependencies

npm install @getpara/react-sdk @getpara/ethers-v6-integration ethers

Create Ethers Provider Hook

hooks/useEthersProvider.ts
import { useMemo } from "react";
import { ethers } from "ethers";

const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL || 
  "https://ethereum-holesky-rpc.publicnode.com";

export function useEthersProvider() {
  const provider = useMemo(() => {
    return new ethers.JsonRpcProvider(RPC_URL);
  }, []);

  return { provider };
}

Create Para Signer Hook

hooks/useParaSigner.ts
import { useState, useEffect } from "react";
import { ParaEthersSigner } from "@getpara/ethers-v6-integration";
import { useAccount, useClient } from "@getpara/react-sdk";
import { useEthersProvider } from "./useEthersProvider";

export function useParaSigner() {
  const { data: account } = useAccount();
  const client = useClient();
  const { provider } = useEthersProvider();
  const [signer, setSigner] = useState<ParaEthersSigner | null>(null);

  useEffect(() => {
    if (account?.isConnected && provider && client) {
      try {
        const newSigner = new ParaEthersSigner(client, provider);
        setSigner(newSigner);
      } catch (error) {
        console.error("Failed to initialize Para signer:", error);
        setSigner(null);
      }
    } else {
      setSigner(null);
    }
  }, [account?.isConnected, provider, client]);

  return { signer, provider };
}

EIP-712 Implementation

Define Domain and Types

The domain separator provides context and prevents replay attacks:
// Domain definition - provides context for the signature
const domain = {
  name: "MyDApp",                    // Application name
  version: "1",                      // Version of signing domain
  chainId: 17000,                    // Network chain ID (Holesky testnet)
  verifyingContract: "0x..." as Address  // Contract that will verify signatures
};

// Types definition - structure of the data being signed
const types = {
  TokenAttestation: [
    { name: "holder", type: "address" },
    { name: "balance", type: "string" },
    { name: "purpose", type: "string" },
    { name: "timestamp", type: "uint256" },
    { name: "nonce", type: "uint256" }
  ]
};

Create Typed Data Structure

interface TokenAttestation {
  holder: string;
  balance: string;
  purpose: string;
  timestamp: number;
  nonce: number;
}

// Create the data object to sign
const attestation: TokenAttestation = {
  holder: "0x742d35Cc6634C0532925a3b8D756e3C98d8a3a1B",
  balance: "1000.5",
  purpose: "Identity verification",
  timestamp: Math.floor(Date.now() / 1000),
  nonce: 1
};

Sign Typed Data

import { useParaSigner } from "./hooks/useParaSigner";

export function TypedDataSigning() {
  const { signer } = useParaSigner();
  const [signature, setSignature] = useState<string>("");
  const [isLoading, setIsLoading] = useState(false);

  const signAttestation = async () => {
    if (!signer) {
      throw new Error("Signer not available");
    }

    setIsLoading(true);
    
    try {
      // Sign the typed data
      const signature = await signer.signTypedData(domain, types, attestation);
      
      setSignature(signature);
      console.log("Signed attestation:", signature);
    } catch (error) {
      console.error("Signing failed:", error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <button 
        onClick={signAttestation}
        disabled={!signer || isLoading}
      >
        {isLoading ? "Signing..." : "Sign Attestation"}
      </button>
      
      {signature && (
        <div>
          <h3>Signature:</h3>
          <code>{signature}</code>
        </div>
      )}
    </div>
  );
}

Advanced Examples

Permit Signature for Token Approvals

// EIP-2612 Permit signature
const permitTypes = {
  Permit: [
    { name: "owner", type: "address" },
    { name: "spender", type: "address" },
    { name: "value", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" }
  ]
};

const permitData = {
  owner: userAddress,
  spender: spenderAddress,
  value: ethers.parseUnits("100", 18),
  nonce: await token.nonces(userAddress),
  deadline: Math.floor(Date.now() / 1000) + 3600 // 1 hour
};

const permitSignature = await signer.signTypedData(
  {
    name: "MyToken",
    version: "1",
    chainId: 1,
    verifyingContract: tokenAddress
  },
  permitTypes,
  permitData
);

Voting Signature

const voteTypes = {
  Vote: [
    { name: "proposalId", type: "uint256" },
    { name: "support", type: "bool" },
    { name: "voter", type: "address" },
    { name: "reason", type: "string" },
    { name: "timestamp", type: "uint256" }
  ]
};

const voteData = {
  proposalId: 42,
  support: true,
  voter: userAddress,
  reason: "I support this proposal",
  timestamp: Math.floor(Date.now() / 1000)
};

const voteSignature = await signer.signTypedData(domain, voteTypes, voteData);

Marketplace Order Signature

const orderTypes = {
  Order: [
    { name: "seller", type: "address" },
    { name: "buyer", type: "address" },
    { name: "tokenContract", type: "address" },
    { name: "tokenId", type: "uint256" },
    { name: "price", type: "uint256" },
    { name: "deadline", type: "uint256" },
    { name: "nonce", type: "uint256" }
  ]
};

const orderData = {
  seller: userAddress,
  buyer: "0x0000000000000000000000000000000000000000", // Any buyer
  tokenContract: nftContractAddress,
  tokenId: 123,
  price: ethers.parseEther("1.5"),
  deadline: Math.floor(Date.now() / 1000) + 86400, // 24 hours
  nonce: 1
};

const orderSignature = await signer.signTypedData(domain, orderTypes, orderData);

Signature Verification

Client-Side Verification

import { ethers } from "ethers";

async function verifySignature(
  domain: any,
  types: any,
  data: any,
  signature: string,
  expectedSigner: string
) {
  try {
    const recoveredSigner = ethers.verifyTypedData(domain, types, data, signature);
    
    return recoveredSigner.toLowerCase() === expectedSigner.toLowerCase();
  } catch (error) {
    console.error("Verification failed:", error);
    return false;
  }
}

// Usage
const isValid = await verifySignature(
  domain,
  types,
  attestation,
  signature,
  userAddress
);

console.log("Signature valid:", isValid);

Smart Contract Verification

// Solidity contract for verifying EIP-712 signatures
contract AttestationVerifier {
    bytes32 private constant ATTESTATION_TYPEHASH = keccak256(
        "TokenAttestation(address holder,string balance,string purpose,uint256 timestamp,uint256 nonce)"
    );
    
    bytes32 private immutable DOMAIN_SEPARATOR;
    
    constructor() {
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes("MyDApp")),
                keccak256(bytes("1")),
                block.chainid,
                address(this)
            )
        );
    }
    
    function verifyAttestation(
        TokenAttestation memory attestation,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public view returns (address) {
        bytes32 structHash = keccak256(
            abi.encode(
                ATTESTATION_TYPEHASH,
                attestation.holder,
                keccak256(bytes(attestation.balance)),
                keccak256(bytes(attestation.purpose)),
                attestation.timestamp,
                attestation.nonce
            )
        );
        
        bytes32 digest = keccak256(
            abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)
        );
        
        return ecrecover(digest, v, r, s);
    }
}

Error Handling

Common Issues and Solutions

const signWithErrorHandling = async () => {
  try {
    const signature = await signer.signTypedData(domain, types, data);
    return signature;
  } catch (error) {
    if (error.code === 'ACTION_REJECTED') {
      console.log("User rejected the signing request");
    } else if (error.message.includes('invalid domain')) {
      console.error("Domain configuration error:", error);
    } else if (error.message.includes('invalid types')) {
      console.error("Types definition error:", error);
    } else {
      console.error("Unexpected signing error:", error);
    }
    throw error;
  }
};

Validation Before Signing

function validateTypedData(domain: any, types: any, data: any) {
  // Validate domain
  if (!domain.name || !domain.version || !domain.chainId) {
    throw new Error("Invalid domain: missing required fields");
  }
  
  // Validate types
  if (!types || Object.keys(types).length === 0) {
    throw new Error("Invalid types: empty or undefined");
  }
  
  // Validate data matches types
  const primaryType = Object.keys(types)[0];
  const typeFields = types[primaryType];
  
  for (const field of typeFields) {
    if (!(field.name in data)) {
      throw new Error(`Missing required field: ${field.name}`);
    }
  }
  
  return true;
}

Best Practices

Security Considerations

  • Validate all inputs before signing
  • Use proper domain separation to prevent replay attacks
  • Include nonces to prevent signature reuse
  • Set reasonable deadlines for time-sensitive signatures
  • Verify contract addresses in domain separator

Type Definition Guidelines

  • Use specific types (uint256 instead of uint)
  • Order fields consistently across your application
  • Document field purposes for maintainability
  • Version your types when making changes

User Experience

  • Provide clear descriptions of what users are signing
  • Show human-readable summaries before signing
  • Handle rejection gracefully with meaningful error messages
  • Cache signatures when appropriate to avoid re-signing

Next Steps