Sign and verify EIP-712 typed structured data using Nethereum (.NET). Use this skill whenever the user asks about EIP-712, typed data signing, structured data signing, domain separator, SignTypedData, ERC-2612 Permit signatures, or off-chain message signing with typed schemas using C# or .NET.
Nethereum.Signer.EIP712
using System.Numerics;
using Nethereum.ABI.EIP712;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Signer;
using Nethereum.Signer.EIP712;
using Nethereum.Util;
EIP-712 signs structured data with a domain separator, producing signatures that wallets can display in human-readable form. Nethereum maps EIP-712 structs to C# classes using attributes.
Each EIP-712 struct becomes a C# class with [Struct] and [Parameter] attributes.
[Struct("Person")]
public class Person
{
[Parameter("string", "name", 1)]
public string Name { get; set; }
[Parameter("address", "wallet", 2)]
public string Wallet { get; set; }
}
[Struct("Mail")]
public class Mail
{
[Parameter("tuple", "from", 1, "Person")]
public Person From { get; set; }
[Parameter("tuple", "to", 2, "Person")]
public Person To { get; set; }
[Parameter("string", "contents", 3)]
public string Contents { get; set; }
}
Rules:
tuple as the Solidity type for nested structs, with the struct name as the fourth [Parameter] argument.[Struct("Name")] value must match the EIP-712 type name exactly.var typedData = new TypedData<Domain>
{
Domain = new Domain
{
Name = "Ether Mail",
Version = "1",
ChainId = 1,
VerifyingContract = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
Types = MemberDescriptionFactory.GetTypesMemberDescription(
typeof(Domain), typeof(Mail), typeof(Person)),
PrimaryType = nameof(Mail),
};
MemberDescriptionFactory.GetTypesMemberDescription auto-generates the type schema from C# classes. Pass typeof(Domain) plus all struct types.
Domain fields: Name, Version, ChainId, VerifyingContract, Salt (optional).
var signer = new Eip712TypedDataSigner();
var key = new EthECKey("private-key-hex");
var mail = new Mail
{
From = new Person
{
Name = "Cow",
Wallet = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
To = new Person
{
Name = "Bob",
Wallet = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
Contents = "Hello, Bob!"
};
var signature = signer.SignTypedDataV4(mail, typedData, key);
var recoveredAddress = signer.RecoverFromSignatureV4(mail, typedData, signature);
// Verify
bool isValid = key.GetPublicAddress().IsTheSameAddress(recoveredAddress);
The signature is a 0x-prefixed hex string (132 chars = 65 bytes).
When receiving EIP-712 data as JSON (e.g., from WalletConnect):
var typedDataJson = @"{
'domain': {
'chainId': 1,
'name': 'Ether Mail',
'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
'version': '1'
},
'message': {
'contents': 'Hello, Bob!',
'from': { 'name': 'Cow', 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' },
'to': { 'name': 'Bob', 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' }
},
'primaryType': 'Mail',
'types': {
'EIP712Domain': [
{ 'name': 'name', 'type': 'string' },
{ 'name': 'version', 'type': 'string' },
{ 'name': 'chainId', 'type': 'uint256' },
{ 'name': 'verifyingContract', 'type': 'address' }
],
'Mail': [
{ 'name': 'from', 'type': 'Person' },
{ 'name': 'to', 'type': 'Person' },
{ 'name': 'contents', 'type': 'string' }
],
'Person': [
{ 'name': 'name', 'type': 'string' },
{ 'name': 'wallet', 'type': 'address' }
]
}
}";
var key = new EthECKey("private-key-hex");
var signer = new Eip712TypedDataSigner();
var signature = signer.SignTypedDataV4(typedDataJson, key);
var recoveredAddress = signer.RecoverFromSignatureV4(typedDataJson, signature);
Define the Permit struct:
[Struct("Permit")]
public class Permit
{
[Parameter("address", "owner", 1)]
public string Owner { get; set; }
[Parameter("address", "spender", 2)]
public string Spender { get; set; }
[Parameter("uint256", "value", 3)]
public BigInteger Value { get; set; }
[Parameter("uint256", "nonce", 4)]
public BigInteger Nonce { get; set; }
[Parameter("uint256", "deadline", 5)]
public BigInteger Deadline { get; set; }
}
Sign a permit:
var typedData = new TypedData<Domain>
{
Domain = new Domain
{
Name = "MyToken",
Version = "1",
ChainId = 1,
VerifyingContract = "0x1234567890abcdef1234567890abcdef12345678"
},
Types = MemberDescriptionFactory.GetTypesMemberDescription(
typeof(Domain), typeof(Permit)),
PrimaryType = nameof(Permit),
};
var permit = new Permit
{
Owner = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
Spender = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
Value = BigInteger.Parse("1000000000000000000"),
Nonce = 0,
Deadline = BigInteger.Parse("1000000000000")
};
var key = new EthECKey("private-key-hex");
var signer = new Eip712TypedDataSigner();
var signature = signer.SignTypedDataV4(permit, typedData, key);
For simple structs without nesting, skip manual TypedData construction:
var domain = new Domain
{
Name = "Ether Mail",
Version = "1",
ChainId = 1,
VerifyingContract = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
};
var signer = new Eip712TypedDataSigner();
var key = new EthECKey("private-key-hex");
var signature = signer.SignTypedData<Permit, Domain>(permit, domain, "Permit", key);
Manual verification of auto-generated signatures:
var encodedData = Eip712TypedDataEncoder.Current
.EncodeTypedData(permit, domain, "Permit");
var recoveredAddress = new MessageSigner().EcRecover(
Sha3Keccack.Current.CalculateHash(encodedData), signature);
| Method | Use Case |
|---|---|
SignTypedDataV4(message, typedData, key) | Sign with C# typed structs |
RecoverFromSignatureV4(message, typedData, sig) | Recover address from typed struct signature |
SignTypedDataV4(json, key) | Sign from raw JSON |
RecoverFromSignatureV4(json, sig) | Recover address from JSON signature |
SignTypedData<T, TDomain>(msg, domain, name, key) | Sign with auto-generated schema |
var expectedAddress = "0x...";
var recoveredAddress = signer.RecoverFromSignatureV4(message, typedData, signature);
bool isValid = expectedAddress.IsTheSameAddress(recoveredAddress);
var domain = new Domain
{
Name = "My Dapp",
Version = "2",
ChainId = 137, // Polygon
VerifyingContract = "0x1111111111111111111111111111111111111111"
};
All patterns from tests/Nethereum.Signer.UnitTests/Eip712DocExampleTests.cs.