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
- Dynamic SDK initialized (see Installation Guide)
- User authenticated (see Authentication Guide)
- EVM wallet available (see Wallet Creation)
Sign Typed Data
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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:Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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:Copy
Ask AI
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:Copy
Ask AI
// Example message with nonce
final message = {
'nonce': currentNonce,
// ... other fields
};
3. Set Deadlines
Always include expiration timestamps:Copy
Ask AI
final deadline = DateTime.now().add(const Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000;
// Include in message
final message = {
'deadline': deadline,
// ... other fields
};
4. Handle Errors
Copy
Ask AI
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:Copy
Ask AI
const typedDataStructure = '''
{
"types": {
"EIP712Domain": [...],
"YourTypeName": [...]
},
"primaryType": "YourTypeName",
"domain": {
"name": "...",
"version": "...",
"chainId": ...,
"verifyingContract": "..."
},
"message": {
// Your data fields
}
}
''';
Next Steps
- Message Signing - Sign plain text messages
- Smart Contract Interactions - Interact with contracts
- Send ETH Transactions - Send transactions