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 DynamicSDKSwift

let sdk = DynamicSDK.instance()

func signTypedData(wallet: BaseWallet) async {
    // Example EIP-712 typed data
    let typedDataJson = """
    {
        "types": {
            "EIP712Domain": [
                {"name": "name", "type": "string"},
                {"name": "version", "type": "string"},
                {"name": "chainId", "type": "uint256"}
            ],
            "Message": [
                {"name": "content", "type": "string"}
            ]
        },
        "primaryType": "Message",
        "domain": {
            "name": "Example App",
            "version": "1",
            "chainId": 1
        },
        "message": {
            "content": "Hello, World!"
        }
    }
    """

    do {
        let signature = try await sdk.wallets.signTypedData(
            wallet: wallet,
            typedDataJson: typedDataJson
        )
        print("Typed data signed!")
        print("Signature: \(signature)")
    } catch {
        print("Failed to sign typed data: \(error)")
    }
}

Common EIP-712 Structures

Basic Message

let 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

func createPermitTypedData(
    tokenName: String,
    tokenAddress: String,
    owner: String,
    spender: String,
    value: String,
    nonce: Int,
    deadline: Int,
    chainId: Int
) -> String {
    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
let permitData = createPermitTypedData(
    tokenName: "USDC",
    tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    owner: wallet.address,
    spender: "0xSpenderAddress",
    value: "1000000", // 1 USDC (6 decimals)
    nonce: 0,
    deadline: Int(Date().timeIntervalSince1970) + 3600,
    chainId: 1
)

let signature = try await sdk.wallets.signTypedData(
    wallet: wallet,
    typedDataJson: permitData
)

NFT Transfer Authorization

func createNFTTransferTypedData(
    nftContractAddress: String,
    tokenId: String,
    from: String,
    to: String,
    chainId: Int
) -> String {
    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

func createMetaTxTypedData(
    from: String,
    to: String,
    value: String,
    gas: String,
    nonce: Int,
    data: String,
    chainId: Int
) -> String {
    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)"
        }
    }
    """
}

SwiftUI Example

import SwiftUI
import DynamicSDKSwift

struct SignTypedDataView: View {
    let wallet: BaseWallet

    @State private var signature: String?
    @State private var isLoading = false
    @State private var errorMessage: String?

    private let sdk = DynamicSDK.instance()

    private let 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!"
        }
    }
    """

    var body: some View {
        VStack(spacing: 16) {
            Text("EIP-712 Typed Data")
                .font(.headline)

            Text(exampleTypedData)
                .font(.system(.caption2, design: .monospaced))
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(8)

            Button("Sign Typed Data") {
                signTypedData()
            }
            .disabled(isLoading)

            if isLoading {
                ProgressView()
            }

            if let signature = signature {
                VStack(alignment: .leading) {
                    Text("Signature:")
                        .font(.caption)
                    Text(signature)
                        .font(.system(.caption2, design: .monospaced))
                        .lineLimit(3)
                }
                .padding()
                .background(Color.green.opacity(0.1))
                .cornerRadius(8)
            }

            if let error = errorMessage {
                Text(error)
                    .foregroundColor(.red)
                    .font(.caption)
            }
        }
        .padding()
    }

    private func signTypedData() {
        isLoading = true
        errorMessage = nil
        signature = nil

        Task { @MainActor in
            do {
                signature = try await sdk.wallets.signTypedData(
                    wallet: wallet,
                    typedDataJson: exampleTypedData
                )
            } catch {
                errorMessage = "Failed to sign: \(error.localizedDescription)"
            }
            isLoading = false
        }
    }
}

Use Cases

Gasless Transactions

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

let signature = try await sdk.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
let permitSignature = try await sdk.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
let orderData = createOrderTypedData(
    tokenIn: "0xUSDC",
    tokenOut: "0xWETH",
    amountIn: "1000000000",
    amountOut: "500000000000000000",
    deadline: Int(Date().timeIntervalSince1970) + 3600
)

let orderSignature = try await sdk.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:
let typedData = """
{
    "domain": {
        "name": "Your App Name",
        "version": "1",
        "chainId": \(expectedChainId),
        "verifyingContract": "\(contractAddress)"
    },
    ...
}
"""

2. Include Nonces

Prevent replay attacks by including nonces:
"message": {
    "nonce": \(currentNonce),
    ...
}

3. Set Deadlines

Always include expiration timestamps:
let deadline = Int(Date().timeIntervalSince1970) + 3600 // 1 hour

"message": {
    "deadline": \(deadline),
    ...
}

4. Handle Errors

do {
    let signature = try await sdk.wallets.signTypedData(
        wallet: wallet,
        typedDataJson: typedDataJson
    )
} catch {
    if error.localizedDescription.contains("rejected") {
        print("User rejected the signature")
    } else {
        print("Signing failed: \(error)")
    }
}

EIP-712 Structure

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

Next Steps