> ## Documentation Index
> Fetch the complete documentation index at: https://docs.streambird.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign Typed Data

Sign structured data with a user's embedded Ethereum wallet using EIP-712. Typed data signing provides better UX and security by displaying human-readable structured information instead of raw hex strings.

<Info>
  This method uses Ethereum's `eth_signTypedData_v4` RPC method, implementing [EIP-712](https://eips.ethereum.org/EIPS/eip-712). For simple message signing, see [Sign Message](/wallet-as-a-service/using-wallets/ethereum/sign-message).
</Info>

## Overview

EIP-712 (Typed Structured Data Hashing and Signing) enables users to sign structured, human-readable data instead of opaque hex strings. This improves both security and user experience by:

* **Displaying readable data**: Users see what they're actually signing
* **Preventing phishing**: Clear structure makes malicious requests more obvious
* **Type safety**: Structured format reduces errors
* **Better UX**: Wallets can format data nicely in signing modals

### Common Use Cases

* **Token permits**: Gasless token approvals (ERC-20 Permit)
* **Meta-transactions**: Sign transactions for relayers to submit
* **NFT approvals**: Approve NFT transfers without gas
* **Governance voting**: Structured voting data
* **Delegation**: Delegate voting power or permissions

## React SDK

<Tabs>
  <Tab title="Basic Usage">
    To sign typed data, use the `signTypedData` method from the `useSignTypedData` hook:

    ```tsx theme={null}
    import { useMoonKey } from '@moon-key/react-auth';
    import { useSignTypedData } from '@moon-key/react-auth/ethereum';

    function SignTypedDataButton() {
      const { user } = useMoonKey();
      const { signTypedData } = useSignTypedData();

      const handleSign = async () => {
        if (!user?.wallet) {
          alert('No wallet found');
          return;
        }

        // Define the EIP-712 typed data
        const typedData = {
          domain: {
            name: 'My dApp',
            version: '1',
            chainId: 1,
            verifyingContract: '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: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
            },
            to: {
              name: 'Bob',
              wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
            },
            contents: 'Hello, Bob!'
          }
        };

        try {
          const result = await signTypedData(typedData);
          console.log('Typed data signed:', result.signature);
          alert('Signed successfully!');
        } catch (error) {
          console.error('Signing failed:', error);
        }
      };

      return (
        <button onClick={handleSign}>
          Sign Typed Data
        </button>
      );
    }
    ```
  </Tab>

  <Tab title="With Callbacks">
    Use callbacks to handle success and error cases:

    ```tsx theme={null}
    import { useMoonKey } from '@moon-key/react-auth';
    import { useSignTypedData } from '@moon-key/react-auth/ethereum';

    function SignWithCallbacks() {
      const { user } = useMoonKey();
      
      const { signTypedData } = useSignTypedData({
        onSuccess: ({ signature }) => {
          console.log('Signature:', signature);
          alert('Successfully signed!');
          // Submit to backend
          fetch('/api/submit-signature', {
            method: 'POST',
            body: JSON.stringify({ signature })
          });
        },
        onError: (error) => {
          console.error('Signing error:', error);
          alert('Failed to sign');
        }
      });

      const handleSign = async () => {
        if (!user?.wallet) return;

        const typedData = {
          domain: {
            name: 'My dApp',
            version: '1',
            chainId: 1
          },
          types: {
            Vote: [
              { name: 'proposalId', type: 'uint256' },
              { name: 'support', type: 'bool' }
            ]
          },
          primaryType: 'Vote',
          message: {
            proposalId: 42,
            support: true
          }
        };

        await signTypedData(typedData);
      };

      return (
        <button onClick={handleSign}>
          Vote
        </button>
      );
    }
    ```
  </Tab>

  <Tab title="With UI Customization">
    Customize the signing modal:

    ```tsx theme={null}
    import { useMoonKey } from '@moon-key/react-auth';
    import { useSignTypedData } from '@moon-key/react-auth/ethereum';

    function CustomSignButton() {
      const { user } = useMoonKey();
      const { signTypedData } = useSignTypedData();

      const handleSign = async () => {
        if (!user?.wallet) return;

        const typedData = {
          domain: {
            name: 'My NFT Marketplace',
            version: '1',
            chainId: 1
          },
          types: {
            Order: [
              { name: 'tokenAddress', type: 'address' },
              { name: 'tokenId', type: 'uint256' },
              { name: 'price', type: 'uint256' }
            ]
          },
          primaryType: 'Order',
          message: {
            tokenAddress: '0x1234...',
            tokenId: 1,
            price: '1000000000000000000' // 1 ETH
          }
        };

        try {
          const result = await signTypedData(typedData, {
            uiConfig: {
              title: 'Sign Listing',
              description: 'List your NFT for sale',
              confirmButtonText: 'Sign Listing',
              cancelButtonText: 'Cancel'
            }
          });

          console.log('Listing signed:', result.signature);
        } catch (error) {
          console.error('Listing failed:', error);
        }
      };

      return (
        <button onClick={handleSign}>
          List NFT
        </button>
      );
    }
    ```
  </Tab>
</Tabs>

## EIP-712 Structure

### Typed Data Format

The typed data object follows the EIP-712 specification:

<ParamField path="domain" type="object" required>
  The domain separator defining the context of the signature.

  <Expandable title="domain properties">
    <ParamField path="name" type="string">
      The name of the dApp or protocol.
    </ParamField>

    <ParamField path="version" type="string">
      The version of the signing domain.
    </ParamField>

    <ParamField path="chainId" type="number">
      The chain ID where the signature is valid.
    </ParamField>

    <ParamField path="verifyingContract" type="string">
      The address of the contract that will verify the signature.
    </ParamField>

    <ParamField path="salt" type="string">
      Optional salt value for additional uniqueness.
    </ParamField>
  </Expandable>

  **Example:**

  ```tsx theme={null}
  domain: {
    name: 'My dApp',
    version: '1',
    chainId: 1,
    verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
  }
  ```
</ParamField>

<ParamField path="types" type="object" required>
  The type definitions for the structured data. Must include `EIP712Domain` and your custom types.

  **Example:**

  ```tsx theme={null}
  types: {
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' }
    ],
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person' },
      { name: 'contents', type: 'string' }
    ]
  }
  ```
</ParamField>

<ParamField path="primaryType" type="string" required>
  The primary type to sign from the `types` definition.

  **Example:**

  ```tsx theme={null}
  primaryType: 'Mail'
  ```
</ParamField>

<ParamField path="message" type="object" required>
  The actual data to sign, matching the structure of the `primaryType`.

  **Example:**

  ```tsx theme={null}
  message: {
    from: {
      name: 'Alice',
      wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
    },
    to: {
      name: 'Bob',
      wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
    },
    contents: 'Hello, Bob!'
  }
  ```
</ParamField>

### Supported Types

EIP-712 supports various Solidity types:

* **Basic types**: `uint8` through `uint256`, `int8` through `int256`, `bool`, `address`, `bytes1` through `bytes32`, `bytes`, `string`
* **Arrays**: `uint256[]`, `string[]`, etc.
* **Custom types**: References to other defined types

## Options Object

The second parameter is an optional configuration object:

<ParamField path="uiConfig" type="object">
  Configuration for the signing confirmation modal UI.

  <Expandable title="uiConfig properties">
    <ParamField path="title" type="string">
      Custom title for the confirmation modal.
    </ParamField>

    <ParamField path="description" type="string">
      Description text shown in the modal.
    </ParamField>

    <ParamField path="confirmButtonText" type="string">
      Text for the confirm button.
    </ParamField>

    <ParamField path="cancelButtonText" type="string">
      Text for the cancel button.
    </ParamField>

    <ParamField path="showWalletUI" type="boolean">
      Whether to show the confirmation modal. Set to `false` to sign without confirmation.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="wallet" type="Wallet">
  Specific wallet to use for signing. If not provided, uses the user's default wallet.
</ParamField>

### Hook Callbacks

<ParamField path="onSuccess" type="(result: { signature: string }) => void">
  Callback executed after successful signing. Receives the signature.
</ParamField>

<ParamField path="onError" type="(error: Error) => void">
  Callback executed if signing fails or user cancels.
</ParamField>

## Returns

<ResponseField name="signature" type="string">
  The hex-encoded signature produced by the wallet.
</ResponseField>

<ResponseField name="encoding" type="'hex'">
  The encoding format of the signature.
</ResponseField>

## Complete Examples

### ERC-20 Permit (Gasless Approval)

```tsx theme={null}
'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSignTypedData } from '@moon-key/react-auth/ethereum';
import { useState } from 'react';

export default function ERC20Permit() {
  const { user } = useMoonKey();
  const [isApproving, setIsApproving] = useState(false);

  const { signTypedData } = useSignTypedData({
    onSuccess: async ({ signature }) => {
      console.log('Permit signature:', signature);

      // Submit permit to backend or contract
      try {
        await fetch('/api/submit-permit', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            owner: user?.wallet?.address,
            signature,
            tokenAddress: '0xTokenAddress...'
          })
        });

        alert('Approval successful (no gas used)!');
      } catch (error) {
        console.error('Permit submission failed:', error);
      } finally {
        setIsApproving(false);
      }
    },
    onError: (error) => {
      console.error('Permit failed:', error);
      setIsApproving(false);
    }
  });

  const handlePermit = async () => {
    if (!user?.wallet) return;

    setIsApproving(true);

    const tokenAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC
    const spenderAddress = '0xSpenderContractAddress...';
    const value = '1000000000'; // 1000 USDC (6 decimals)
    const nonce = 0; // Get from token contract
    const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now

    const typedData = {
      domain: {
        name: 'USD Coin',
        version: '2',
        chainId: 1,
        verifyingContract: tokenAddress
      },
      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: user.wallet.address,
        spender: spenderAddress,
        value,
        nonce,
        deadline
      }
    };

    await signTypedData(typedData, {
      uiConfig: {
        title: 'Approve USDC',
        description: 'Sign to approve USDC spending (no gas required)',
        confirmButtonText: 'Approve'
      }
    });
  };

  return (
    <button
      onClick={handlePermit}
      disabled={isApproving || !user?.wallet}
      className="permit-button"
    >
      {isApproving ? 'Approving...' : 'Approve USDC (Gasless)'}
    </button>
  );
}
```

## UI Customization

Customize the signing modal:

```tsx theme={null}
await signTypedData(typedData, {
  uiConfig: {
    title: 'Sign Permit',
    description: 'Approve token spending without gas fees',
    confirmButtonText: 'Sign Permit',
    cancelButtonText: 'Cancel'
  }
});
```

### Hide Modal

```tsx theme={null}
await signTypedData(typedData, {
  uiConfig: {
    showWalletUI: false
  }
});
```

<Warning>
  Hiding the modal removes the user's ability to review the structured data before signing. Only use this for trusted operations or after obtaining explicit user consent.
</Warning>

## Best Practices

<AccordionGroup>
  <Accordion title="Always include domain separator">
    The domain separator prevents signature replay across chains and contracts:

    ```tsx theme={null}
    // ✅ Good - Includes full domain
    domain: {
      name: 'My dApp',
      version: '1',
      chainId: 1,
      verifyingContract: '0xContractAddress...'
    }

    // ❌ Bad - Missing critical fields
    domain: {
      name: 'My dApp'
    }
    ```
  </Accordion>

  <Accordion title="Use expiration times">
    Always include expiration to prevent old signatures from being reused:

    ```tsx theme={null}
    const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour

    message: {
      // ... other fields
      deadline
    }

    // Verify expiration on backend
    if (Date.now() / 1000 > deadline) {
      throw new Error('Signature expired');
    }
    ```
  </Accordion>

  <Accordion title="Include nonces for uniqueness">
    Use nonces to prevent replay attacks:

    ```tsx theme={null}
    // Get nonce from contract or database
    const nonce = await getNonce(userAddress);

    message: {
      // ... other fields
      nonce
    }

    // Increment nonce after use to prevent replay
    await incrementNonce(userAddress);
    ```
  </Accordion>

  <Accordion title="Validate on the backend">
    Always verify signatures server-side:

    ```tsx theme={null}
    // ❌ Bad - Only client-side verification
    const isValid = await verifyTypedData({ ... });
    if (isValid) {
      proceedWithAction(); // Insecure!
    }

    // ✅ Good - Backend verification
    const response = await fetch('/api/verify-signature', {
      method: 'POST',
      body: JSON.stringify({ signature, typedData })
    });
    const { valid } = await response.json();
    if (valid) {
      proceedWithAction(); // Secure!
    }
    ```
  </Accordion>

  <Accordion title="Use standard type definitions">
    Follow established standards (ERC-20 Permit, EIP-2612):

    ```tsx theme={null}
    // ✅ Use standard Permit type
    types: {
      Permit: [
        { name: 'owner', type: 'address' },
        { name: 'spender', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Handle errors gracefully">
    Provide clear error messages for common issues:

    ```tsx theme={null}
    const { signTypedData } = useSignTypedData({
      onError: (error: any) => {
        if (error.code === 'USER_REJECTED') {
          // User cancelled - don't show error
          return;
        } else if (error.message?.includes('expired')) {
          alert('Permit has expired. Please try again.');
        } else if (error.message?.includes('nonce')) {
          alert('Invalid nonce. Please refresh and try again.');
        } else {
          alert('Failed to sign: ' + error.message);
        }
      }
    });
    ```
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Signature verification fails">
    Common causes:

    * **Type mismatch**: Ensure types match exactly between signing and verification
    * **Wrong domain**: Domain must match exactly (including chainId)
    * **Incorrect primaryType**: Must reference a type in the types object
    * **Message structure mismatch**: Message must match primaryType structure

    ```tsx theme={null}
    // Ensure exact match
    const typedData = { domain, types, primaryType, message };

    // Sign
    const { signature } = await signTypedData(typedData);

    // Verify with SAME typedData
    const isValid = await verifyTypedData({
      address: signerAddress,
      ...typedData,
      signature
    });
    ```
  </Accordion>

  <Accordion title="Invalid type definition">
    Ensure all types are properly defined:

    ```tsx theme={null}
    // ✅ Correct - All referenced types defined
    types: {
      Person: [
        { name: 'name', type: 'string' },
        { name: 'wallet', type: 'address' }
      ],
      Mail: [
        { name: 'from', type: 'Person' }, // Person is defined
        { name: 'to', type: 'Person' },
        { name: 'contents', type: 'string' }
      ]
    }

    // ❌ Wrong - Person not defined
    types: {
      Mail: [
        { name: 'from', type: 'Person' }, // Error: Person undefined
        { name: 'contents', type: 'string' }
      ]
    }
    ```
  </Accordion>

  <Accordion title="ChainId mismatch">
    Ensure chainId matches the network:

    ```tsx theme={null}
    // Get current chain from wallet
    const chainId = await walletClient.getChainId();

    // Use in domain
    domain: {
      name: 'My dApp',
      version: '1',
      chainId // Must match current network
    }
    ```
  </Accordion>

  <Accordion title="Callback not firing">
    Configure callbacks on hook initialization:

    ```tsx theme={null}
    // ✅ Correct
    const { signTypedData } = useSignTypedData({
      onSuccess: ({ signature }) => {
        console.log('Success:', signature);
      },
      onError: (error) => {
        console.log('Error:', error);
      }
    });

    // Then call it
    await signTypedData(typedData);
    ```
  </Accordion>
</AccordionGroup>

## Related Documentation

<CardGroup cols={2}>
  <Card title="Sign Message" icon="signature" href="/wallet-as-a-service/using-wallets/ethereum/sign-message">
    Sign simple messages with personal\_sign
  </Card>

  <Card title="Sign Transaction" icon="pen-to-square" href="/wallet-as-a-service/using-wallets/ethereum/sign-transaction">
    Sign transactions without broadcasting
  </Card>

  <Card title="EIP-712 Specification" icon="book" href="https://eips.ethereum.org/EIPS/eip-712">
    Official EIP-712 documentation
  </Card>

  <Card title="Viem Documentation" icon="code" href="https://viem.sh/docs/utilities/verifyTypedData">
    Verify typed data with viem
  </Card>
</CardGroup>

## Next Steps

* Learn about [EIP-712](https://eips.ethereum.org/EIPS/eip-712) specification details
* Explore [ERC-20 Permit](https://eips.ethereum.org/EIPS/eip-2612) for gasless approvals
* Understand [signature verification](https://viem.sh/docs/utilities/verifyTypedData) with viem
* Implement [gasless meta-transactions](https://docs.openzeppelin.com/contracts/4.x/api/metatx) in your dApp
