Skip to main content

Overview

EIP-712 is a standard for signing typed structured data. It provides a secure way to sign complex data structures that are human-readable and verifiable on-chain.

Prerequisites

Sign Typed Data

import 'package:dynamic_sdk/dynamic_sdk.dart';

final sdk = DynamicSDK.instance;

Future<String> signTypedData({
  required BaseWallet wallet,
  required String typedDataJson,
}) async {
  try {
    final signature = await sdk.wallets.signTypedData(
      wallet: wallet,
      typedDataJson: typedDataJson,
    );
    print('Typed data signed!');
    print('Signature: $signature');
    return signature;
  } catch (e) {
    print('Failed to sign typed data: $e');
    rethrow;
  }
}

Common EIP-712 Structures

Basic Message

const messageTypedData = '''
{
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"}
    ],
    "Mail": [
      {"name": "from", "type": "string"},
      {"name": "to", "type": "string"},
      {"name": "contents", "type": "string"}
    ]
  },
  "primaryType": "Mail",
  "domain": {
    "name": "Example DApp",
    "version": "1",
    "chainId": 1
  },
  "message": {
    "from": "Alice",
    "to": "Bob",
    "contents": "Hello!"
  }
}
''';

ERC-20 Permit

String createPermitTypedData({
  required String tokenName,
  required String tokenAddress,
  required String owner,
  required String spender,
  required String value,
  required int nonce,
  required int deadline,
  required int chainId,
}) {
  return '''
{
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"},
      {"name": "verifyingContract", "type": "address"}
    ],
    "Permit": [
      {"name": "owner", "type": "address"},
      {"name": "spender", "type": "address"},
      {"name": "value", "type": "uint256"},
      {"name": "nonce", "type": "uint256"},
      {"name": "deadline", "type": "uint256"}
    ]
  },
  "primaryType": "Permit",
  "domain": {
    "name": "$tokenName",
    "version": "1",
    "chainId": $chainId,
    "verifyingContract": "$tokenAddress"
  },
  "message": {
    "owner": "$owner",
    "spender": "$spender",
    "value": "$value",
    "nonce": $nonce,
    "deadline": $deadline
  }
}
''';
}

// Usage
final permitData = createPermitTypedData(
  tokenName: 'USDC',
  tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  owner: wallet.address,
  spender: '0xSpenderAddress',
  value: '1000000', // 1 USDC (6 decimals)
  nonce: 0,
  deadline: DateTime.now().add(const Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000,
  chainId: 1,
);

final signature = await DynamicSDK.instance.wallets.signTypedData(
  wallet: wallet,
  typedDataJson: permitData,
);

NFT Transfer Authorization

String createNFTTransferTypedData({
  required String nftContractAddress,
  required String tokenId,
  required String from,
  required String to,
  required int chainId,
}) {
  return '''
{
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"},
      {"name": "verifyingContract", "type": "address"}
    ],
    "Transfer": [
      {"name": "tokenId", "type": "uint256"},
      {"name": "from", "type": "address"},
      {"name": "to", "type": "address"}
    ]
  },
  "primaryType": "Transfer",
  "domain": {
    "name": "MyNFT",
    "version": "1",
    "chainId": $chainId,
    "verifyingContract": "$nftContractAddress"
  },
  "message": {
    "tokenId": "$tokenId",
    "from": "$from",
    "to": "$to"
  }
}
''';
}

Meta-Transaction

String createMetaTxTypedData({
  required String from,
  required String to,
  required String value,
  required String gas,
  required int nonce,
  required String data,
  required int chainId,
}) {
  return '''
{
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"}
    ],
    "MetaTransaction": [
      {"name": "from", "type": "address"},
      {"name": "to", "type": "address"},
      {"name": "value", "type": "uint256"},
      {"name": "gas", "type": "uint256"},
      {"name": "nonce", "type": "uint256"},
      {"name": "data", "type": "bytes"}
    ]
  },
  "primaryType": "MetaTransaction",
  "domain": {
    "name": "MetaTxRelay",
    "version": "1",
    "chainId": $chainId
  },
  "message": {
    "from": "$from",
    "to": "$to",
    "value": "$value",
    "gas": "$gas",
    "nonce": $nonce,
    "data": "$data"
  }
}
''';
}

Flutter Widget Example

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dynamic_sdk/dynamic_sdk.dart';

class SignTypedDataWidget extends StatefulWidget {
  final BaseWallet wallet;

  const SignTypedDataWidget({Key? key, required this.wallet}) : super(key: key);

  @override
  State<SignTypedDataWidget> createState() => _SignTypedDataWidgetState();
}

class _SignTypedDataWidgetState extends State<SignTypedDataWidget> {
  final sdk = DynamicSDK.instance;

  String? signature;
  bool isLoading = false;
  String? errorMessage;

  final exampleTypedData = '''
{
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"}
    ],
    "Mail": [
      {"name": "from", "type": "string"},
      {"name": "to", "type": "string"},
      {"name": "contents", "type": "string"}
    ]
  },
  "primaryType": "Mail",
  "domain": {
    "name": "Example DApp",
    "version": "1",
    "chainId": 1
  },
  "message": {
    "from": "Alice",
    "to": "Bob",
    "contents": "Hello!"
  }
}
''';

  Future<void> _signTypedData() async {
    setState(() {
      isLoading = true;
      errorMessage = null;
      signature = null;
    });

    try {
      final sig = await sdk.wallets.signTypedData(
        wallet: widget.wallet,
        typedDataJson: exampleTypedData,
      );
      setState(() => signature = sig);
    } catch (e) {
      setState(() => errorMessage = 'Failed to sign: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'EIP-712 Typed Data',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: Colors.grey[200],
              borderRadius: BorderRadius.circular(8),
            ),
            child: SelectableText(
              exampleTypedData,
              style: const TextStyle(
                fontSize: 10,
                fontFamily: 'monospace',
              ),
            ),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: isLoading ? null : _signTypedData,
            child: isLoading
                ? const SizedBox(
                    height: 20,
                    width: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Sign Typed Data'),
          ),
          if (signature != null) ...[
            const SizedBox(height: 16),
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.green.withOpacity(0.1),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Signature:',
                    style: TextStyle(fontSize: 12),
                  ),
                  const SizedBox(height: 8),
                  SelectableText(
                    signature!,
                    style: const TextStyle(
                      fontSize: 10,
                      fontFamily: 'monospace',
                    ),
                    maxLines: 3,
                  ),
                  const SizedBox(height: 8),
                  TextButton(
                    onPressed: () {
                      Clipboard.setData(ClipboardData(text: signature!));
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Signature copied!')),
                      );
                    },
                    child: const Text('Copy'),
                  ),
                ],
              ),
            ),
          ],
          if (errorMessage != null) ...[
            const SizedBox(height: 16),
            Text(
              errorMessage!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ],
        ],
      ),
    );
  }
}

Use Cases

Gasless Transactions

EIP-712 signatures enable gasless transactions where a relayer pays the gas:
// 1. User signs typed data for the transaction
final metaTxData = createMetaTxTypedData(
  from: wallet.address,
  to: targetContract,
  value: '0',
  gas: '100000',
  nonce: 0,
  data: calldata,
  chainId: 1,
);

final signature = await DynamicSDK.instance.wallets.signTypedData(
  wallet: wallet,
  typedDataJson: metaTxData,
);

// 2. Send signature to relayer
// Relayer executes transaction and pays gas

Token Approvals Without Gas

// Sign permit instead of sending approve transaction
final permitSignature = await DynamicSDK.instance.wallets.signTypedData(
  wallet: wallet,
  typedDataJson: permitData,
);

// Contract can verify signature and grant approval without user paying gas

Off-Chain Order Books

// Sign order for DEX without on-chain transaction
String createOrderTypedData({
  required String tokenIn,
  required String tokenOut,
  required String amountIn,
  required String amountOut,
  required int deadline,
}) {
  return '''
{
  "types": {
    "Order": [
      {"name": "tokenIn", "type": "address"},
      {"name": "tokenOut", "type": "address"},
      {"name": "amountIn", "type": "uint256"},
      {"name": "amountOut", "type": "uint256"},
      {"name": "deadline", "type": "uint256"}
    ]
  },
  "primaryType": "Order",
  "message": {
    "tokenIn": "$tokenIn",
    "tokenOut": "$tokenOut",
    "amountIn": "$amountIn",
    "amountOut": "$amountOut",
    "deadline": $deadline
  }
}
''';
}

final orderSignature = await DynamicSDK.instance.wallets.signTypedData(
  wallet: wallet,
  typedDataJson: orderData,
);

// Submit signed order to off-chain order book

Best Practices

1. Validate Domain

Always include and validate the domain parameters:
String createTypedDataWithDomain({
  required String appName,
  required int chainId,
  required String contractAddress,
}) {
  return '''
{
  "domain": {
    "name": "$appName",
    "version": "1",
    "chainId": $chainId,
    "verifyingContract": "$contractAddress"
  },
  ...
}
''';
}

2. Include Nonces

Prevent replay attacks by including nonces:
// Example message with nonce
final message = {
  'nonce': currentNonce,
  // ... other fields
};

3. Set Deadlines

Always include expiration timestamps:
final deadline = DateTime.now().add(const Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000;

// Include in message
final message = {
  'deadline': deadline,
  // ... other fields
};

4. Handle Errors

Future<String?> signTypedDataSafely({
  required BaseWallet wallet,
  required String typedDataJson,
}) async {
  try {
    return await DynamicSDK.instance.wallets.signTypedData(
      wallet: wallet,
      typedDataJson: typedDataJson,
    );
  } catch (e) {
    if (e.toString().contains('rejected')) {
      print('User rejected the signature');
    } else {
      print('Signing failed: $e');
    }
    return null;
  }
}

EIP-712 Structure

The typed data must follow this structure:
const typedDataStructure = '''
{
  "types": {
    "EIP712Domain": [...],
    "YourTypeName": [...]
  },
  "primaryType": "YourTypeName",
  "domain": {
    "name": "...",
    "version": "...",
    "chainId": ...,
    "verifyingContract": "..."
  },
  "message": {
    // Your data fields
  }
}
''';

Next Steps