Overview
Sign typed structured data following the EIP-712 standard for better security and readability.Prerequisites
- Dynamic SDK initialized (see Installation Guide)
- User authenticated (see Authentication Guide)
- EVM wallet available (see Wallet Creation)
- Understanding of EIP-712 standard
Sign Typed Data
Copy
Ask AI
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
Copy
Ask AI
// 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)
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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)
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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 networkWhat’s Next
- Message Signing - Sign simple messages
- Smart Contract Interactions - Use signatures with contracts
- Send ETH Transactions - Send transactions
- Authentication - Authenticate with typed data signatures