Skip to main content

Overview

Sign typed structured data following the EIP-712 standard for better security and readability.

Prerequisites

Sign Typed Data

import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.BaseWallet
import com.dynamic.sdk.android.Chains.EVM.signTypedData

val sdk = DynamicSDK.getInstance()

suspend fun signTypedData(wallet: BaseWallet, typedData: String): String {
    return try {
        val signature = sdk.wallets.signTypedData(wallet, typedData)
        println("Typed data signed successfully!")
        println("Signature: $signature")
        signature
    } catch (e: Exception) {
        println("Failed to sign typed data: ${e.message}")
        throw e
    }
}

EIP-712 Typed Data Structure

// Example: Mail message typed data
val typedDataJson = """
{
  "types": {
    "EIP712Domain": [
      { "name": "name", "type": "string" },
      { "name": "version", "type": "string" },
      { "name": "chainId", "type": "uint256" },
      { "name": "verifyingContract", "type": "address" }
    ],
    "Person": [
      { "name": "name", "type": "string" },
      { "name": "wallet", "type": "address" }
    ],
    "Mail": [
      { "name": "from", "type": "Person" },
      { "name": "to", "type": "Person" },
      { "name": "contents", "type": "string" }
    ]
  },
  "primaryType": "Mail",
  "domain": {
    "name": "Ether Mail",
    "version": "1",
    "chainId": 1,
    "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
  },
  "message": {
    "from": {
      "name": "Alice",
      "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
    },
    "to": {
      "name": "Bob",
      "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
    },
    "contents": "Hello, Bob!"
  }
}
""".trimIndent()

Common Use Cases

ERC-20 Permit (Gasless Approval)

data class Erc20Permit(
    val owner: String,
    val spender: String,
    val value: String,
    val nonce: String,
    val deadline: Long
)

fun createPermitTypedData(
    permit: Erc20Permit,
    tokenAddress: String,
    tokenName: String,
    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": "${permit.owner}",
        "spender": "${permit.spender}",
        "value": "${permit.value}",
        "nonce": "${permit.nonce}",
        "deadline": ${permit.deadline}
      }
    }
    """.trimIndent()
}

// Usage
suspend fun signPermit(
    wallet: BaseWallet,
    spenderAddress: String,
    amount: String,
    tokenAddress: String,
    tokenName: String,
    chainId: Int
): String {
    val permit = Erc20Permit(
        owner = wallet.address,
        spender = spenderAddress,
        value = amount,
        nonce = "0", // Get from contract
        deadline = System.currentTimeMillis() / 1000 + 3600 // 1 hour from now
    )

    val typedData = createPermitTypedData(permit, tokenAddress, tokenName, chainId)
    return sdk.wallets.signTypedData(wallet, typedData)
}

Meta-Transaction

data class MetaTransaction(
    val from: String,
    val to: String,
    val value: String,
    val gas: String,
    val nonce: String,
    val data: String
)

fun createMetaTxTypedData(
    metaTx: MetaTransaction,
    forwarderAddress: String,
    chainId: Int
): String {
    return """
    {
      "types": {
        "EIP712Domain": [
          { "name": "name", "type": "string" },
          { "name": "version", "type": "string" },
          { "name": "chainId", "type": "uint256" },
          { "name": "verifyingContract", "type": "address" }
        ],
        "ForwardRequest": [
          { "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": "ForwardRequest",
      "domain": {
        "name": "MinimalForwarder",
        "version": "0.0.1",
        "chainId": $chainId,
        "verifyingContract": "$forwarderAddress"
      },
      "message": {
        "from": "${metaTx.from}",
        "to": "${metaTx.to}",
        "value": "${metaTx.value}",
        "gas": "${metaTx.gas}",
        "nonce": "${metaTx.nonce}",
        "data": "${metaTx.data}"
      }
    }
    """.trimIndent()
}

NFT Voucher

data class NftVoucher(
    val tokenId: String,
    val minPrice: String,
    val uri: String
)

fun createNftVoucherTypedData(
    voucher: NftVoucher,
    nftContractAddress: String,
    chainId: Int
): String {
    return """
    {
      "types": {
        "EIP712Domain": [
          { "name": "name", "type": "string" },
          { "name": "version", "type": "string" },
          { "name": "chainId", "type": "uint256" },
          { "name": "verifyingContract", "type": "address" }
        ],
        "NFTVoucher": [
          { "name": "tokenId", "type": "uint256" },
          { "name": "minPrice", "type": "uint256" },
          { "name": "uri", "type": "string" }
        ]
      },
      "primaryType": "NFTVoucher",
      "domain": {
        "name": "LazyNFT",
        "version": "1",
        "chainId": $chainId,
        "verifyingContract": "$nftContractAddress"
      },
      "message": {
        "tokenId": "${voucher.tokenId}",
        "minPrice": "${voucher.minPrice}",
        "uri": "${voucher.uri}"
      }
    }
    """.trimIndent()
}

Order Signature (DEX)

data class Order(
    val maker: String,
    val taker: String,
    val makerAsset: String,
    val takerAsset: String,
    val makerAmount: String,
    val takerAmount: String,
    val expiration: Long
)

fun createOrderTypedData(
    order: Order,
    exchangeAddress: String,
    chainId: Int
): String {
    return """
    {
      "types": {
        "EIP712Domain": [
          { "name": "name", "type": "string" },
          { "name": "version", "type": "string" },
          { "name": "chainId", "type": "uint256" },
          { "name": "verifyingContract", "type": "address" }
        ],
        "Order": [
          { "name": "maker", "type": "address" },
          { "name": "taker", "type": "address" },
          { "name": "makerAsset", "type": "address" },
          { "name": "takerAsset", "type": "address" },
          { "name": "makerAmount", "type": "uint256" },
          { "name": "takerAmount", "type": "uint256" },
          { "name": "expiration", "type": "uint256" }
        ]
      },
      "primaryType": "Order",
      "domain": {
        "name": "Exchange",
        "version": "1",
        "chainId": $chainId,
        "verifyingContract": "$exchangeAddress"
      },
      "message": {
        "maker": "${order.maker}",
        "taker": "${order.taker}",
        "makerAsset": "${order.makerAsset}",
        "takerAsset": "${order.takerAsset}",
        "makerAmount": "${order.makerAmount}",
        "takerAmount": "${order.takerAmount}",
        "expiration": ${order.expiration}
      }
    }
    """.trimIndent()
}

Typed Data Builder

class TypedDataBuilder {
    data class Type(val name: String, val type: String)
    data class Domain(
        val name: String,
        val version: String,
        val chainId: Int,
        val verifyingContract: String
    )

    private val types = mutableMapOf<String, List<Type>>()
    private var primaryType: String? = null
    private var domain: Domain? = null
    private val message = mutableMapOf<String, Any>()

    fun addType(name: String, fields: List<Type>): TypedDataBuilder {
        types[name] = fields
        return this
    }

    fun setPrimaryType(type: String): TypedDataBuilder {
        primaryType = type
        return this
    }

    fun setDomain(domain: Domain): TypedDataBuilder {
        this.domain = domain
        return this
    }

    fun addMessageField(key: String, value: Any): TypedDataBuilder {
        message[key] = value
        return this
    }

    fun build(): String {
        require(primaryType != null) { "Primary type must be set" }
        require(domain != null) { "Domain must be set" }

        // Add EIP712Domain type if not present
        if (!types.containsKey("EIP712Domain")) {
            types["EIP712Domain"] = listOf(
                Type("name", "string"),
                Type("version", "string"),
                Type("chainId", "uint256"),
                Type("verifyingContract", "address")
            )
        }

        // Build JSON
        val typesJson = types.entries.joinToString(",\n    ") { (name, fields) ->
            val fieldsJson = fields.joinToString(",\n      ") {
                """{ "name": "${it.name}", "type": "${it.type}" }"""
            }
            """"$name": [
      $fieldsJson
    ]"""
        }

        val messageJson = message.entries.joinToString(",\n    ") { (key, value) ->
            val valueStr = when (value) {
                is String -> "\"$value\""
                else -> value.toString()
            }
            """"$key": $valueStr"""
        }

        return """
{
  "types": {
    $typesJson
  },
  "primaryType": "$primaryType",
  "domain": {
    "name": "${domain!!.name}",
    "version": "${domain!!.version}",
    "chainId": ${domain!!.chainId},
    "verifyingContract": "${domain!!.verifyingContract}"
  },
  "message": {
    $messageJson
  }
}
        """.trimIndent()
    }
}

// Usage
val typedData = TypedDataBuilder()
    .addType("Person", listOf(
        TypedDataBuilder.Type("name", "string"),
        TypedDataBuilder.Type("wallet", "address")
    ))
    .addType("Mail", listOf(
        TypedDataBuilder.Type("from", "Person"),
        TypedDataBuilder.Type("to", "Person"),
        TypedDataBuilder.Type("contents", "string")
    ))
    .setPrimaryType("Mail")
    .setDomain(TypedDataBuilder.Domain(
        name = "Ether Mail",
        version = "1",
        chainId = 1,
        verifyingContract = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
    ))
    .addMessageField("from", mapOf("name" to "Alice", "wallet" to "0x..."))
    .addMessageField("to", mapOf("name" to "Bob", "wallet" to "0x..."))
    .addMessageField("contents", "Hello, Bob!")
    .build()

Best Practices

  • Validate typed data structure has all required fields
  • Display message data in readable format to users
  • Verify chain ID matches current network
  • Show clear explanation of what data is being signed
  • Never sign typed data you don’t understand

Error Handling

sealed class TypedDataError {
    data class InvalidStructure(val message: String) : TypedDataError()
    data class UserRejected(val message: String) : TypedDataError()
    data class ChainMismatch(val message: String) : TypedDataError()
    data class Unknown(val message: String) : TypedDataError()
}

fun parseTypedDataError(e: Exception): TypedDataError {
    val errorMessage = e.message?.lowercase() ?: ""

    return when {
        errorMessage.contains("parse") || errorMessage.contains("invalid") ->
            TypedDataError.InvalidStructure("Invalid typed data format")
        errorMessage.contains("rejected") || errorMessage.contains("denied") ->
            TypedDataError.UserRejected("User rejected the signature request")
        errorMessage.contains("chain") ->
            TypedDataError.ChainMismatch("Chain ID does not match current network")
        else ->
            TypedDataError.Unknown("Failed to sign typed data: ${e.message}")
    }
}

Troubleshooting

Invalid JSON: Ensure proper formatting with quotes, commas, and braces Chain ID mismatch: Verify chain ID in typed data matches wallet’s current network

What’s Next