Documentation Index Fetch the complete documentation index at: https://docs.getpara.com/llms.txt
Use this file to discover all available pages before exploring further.
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
import { useState , useEffect } from "react" ;
import { createParaEthersSigner } 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 = createParaEthersSigner ({ para: client , provider: 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
Ethers Integration Learn more about Para’s Ethers integration
Session Management Understand Para’s authentication system
Ethereum Transfers Intermediate · 20 min · Send ETH with Ethers and Viem
Porto Integration Intermediate · 30 min · EIP-7702 smart accounts with session keys
Aave v3 Integration Advanced · 45 min · Lending, borrowing, and yield strategies