Solidity - EIP-712 Typed Structured Data Hashing and Signing explains how smart contracts verify the signature of EIP-712. This article explains how to use Web3.js for EIP-712 signing. This article uses Web3.js version 4.1.1.

Sign

Browser Wallet

Web3.js can use the following functions to sign EIP-712:

  1. web3.eth.signTypedData
  2. web3.currentProvider.request

Browser wallets can use the above functions, but Web3.js does not have a private key signing method, so another library will be introduced later to handle it.

web3.eth.signTypedData

This function was added since 4.1.0

1
web3.eth.signTypedData(address: string, typedData: Eip712TypedData): Promise<string>
  • address: Address of the signer.
  • typedData: The content to sign, includes the domain, struct definition and message content of EIP-712. Please refer to the example below directly.
  • Return: A Promise object, and the content is a signature.

Use the previous example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract Example is EIP712 {
struct Mail {
address from;
address to;
string contents;
}

// 0x536e54c54e6699204b424f41f6dea846ee38ac369afec3e7c141d2c92c65e67f
bytes32 private constant MAIL_TYPEHASH = keccak256(
"Mail(address from,address to,string contents)"
);

constructor() EIP712("Example", "1") {
}

function hashStruct(Mail memory mail) public pure returns (bytes32) {
return keccak256(
abi.encode(
MAIL_TYPEHASH,
mail.from,
mail.to,
keccak256(abi.encodePacked(mail.contents))
)
);
}

function recover(Mail memory mail, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
bytes32 structHash = hashStruct(mail);
bytes32 hash = _hashTypedDataV4(structHash);
return ecrecover(hash , v, r, s);
}
}

The corresponding Web3.js usage is as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Signer address, e.g.
const address = '0xDD980c315dFA75682F04381E98ea38BD2A151540';

// Use actual chainId. For example, mainnet is 1
const chainId = 1;

// Deployed contract address
const verifyingContract = '0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8';

// Also fill in the name and version in the contract
const domain = {
chainId,
verifyingContract,
name: 'Example',
version: '1'
};

const types = {
// EIP712Domain fixed data
EIP712Domain: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'chainId',
type: 'uint256',
},
{
name: 'verifyingContract',
type: 'address',
}
],

// The following is custom struct
Mail: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'contents', type: 'string' }
]
};

// actual content
const message = {
from: '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8',
to: '0xDA9dfA130Df4dE4673b89022EE50ff26f6EA73Cf',
contents: 'Hello'
};

// entire data
const typedData = {
types,
domain,

// Specify which type to use
primaryType: 'Mail',
message
};

// 0x78c03866a5efa253157e62596194631b4c9757d38b3556994d6b879d79d713e666f9c0a3e21e48fd061c5cf5217fd7e3c3ced521aa39650fda179ce61e6763b11c
const signature = await web3.eth.signTypedData(address, typedData);

We will get a not spiltted signature. We can refer to the previous article Using Web3.js to Sign and Recover to handle it.

web3.currentProvider.request

This is a general function for calling RPC. In the older version of Web3.js, the above signTypedData is not available, so you can call the RPC method directly.

1
web3.currentProvider.request(args: Web3APIPayload): Promise<string>
  • args: RPC content, refer to the example below.
  • Return: A Promise object, and the content is the RPC execution result. The result here is the signature.

The first part of the above example is the same, just change the last line:

1
const signature = await web3.eth.signTypedData(address, typedData);

Change to

1
2
3
4
5
6
7
8
const params = [address, typedData];
const signature = await web3.currentProvider.request(
{
method: 'eth_signTypedData_v4',
params,
from: address,
}
);

Private Key Wallet

If you want to use a private key wallet to sign, you can use the library provided by Metamask @metamask/eth-sig-util.

1
signTypedData({ data: TypedMessage, privateKey: Buffer, version: SignTypedDataVersion }): string
  • data: Content to sign. The same as typedData above.
  • privateKey: Private key, Buffer type.
  • version: EIP-712 version.
  • Return: signature.

After installation, you must first import the library

1
import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util';

Then just change the last line

1
2
3
4
5
6
const privatekey = '0x...';
const signature = signTypedData({
privateKey: Buffer.from(privatekey, 'hex'),
data: typedData as any,
version: SignTypedDataVersion.V4
});

Other Examples

struct array

1
2
3
4
5
6
7
8
9
10
struct Mail {
Person from;
Person[] to;
string contents;
}

struct Person {
address wallet;
string name;
}

types and message change to the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const types = {
EIP712Domain: [
// ...
],

Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' }
],
Person: [
{ name: 'wallet', type: 'address' },
{ name: 'name', type: 'string' }
]
};

const message = {
from: {
wallet: '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8',
name: 'Binance 7'
},
to: [
{
wallet: '0xDA9dfA130Df4dE4673b89022EE50ff26f6EA73Cf',
name: 'Kraken 13'
}
],
contents: 'Hello'
};

Recover Signature

If you want to recover and verify the signature in the backend for login purposes, you can also use the @metamask/eth-sig-util package:

1
recoverTypedSignature({ data: TypedMessage, signature: string, version: SignTypedDataVersion }): string
  • data: Content to sign. The same as typedData above.
  • signature: Not splitted signature.
  • version: EIP-712 version.
  • Return: Signer address.

For example:

1
2
3
4
5
6
7
8
9
10
import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util';

const signature = '0x78c03866a5efa253157e62596194631b4c9757d38b3556994d6b879d79d713e666f9c0a3e21e48fd061c5cf5217fd7e3c3ced521aa39650fda179ce61e6763b11c';

// 0xdd980c315dfa75682f04381e98ea38bd2a151540
const signer = recoverTypedSignature({
data: typedData,
signature,
version: SignTypedDataVersion.V4
});

Conclusion

  1. The browser wallet uses web3.eth.signTypedData to sign if Web3.js is v4.1.0 or above.
  2. The browser wallet uses web3.currentProvider.request to sign if Web3.js is old version.
  3. Private key signing uses signTypedData of @metamask/eth-sig-util to sign.
  4. Use recoverTypedSignature of @metamask/eth-sig-util to recover and verify the signature.

Further Reading

Solidity - EIP-712 Typed Structured Data Hashing and Signing
Using Ethers.js to Sign and Recover EIP-712 Typed Structured Data
Solidity - ecrecover
Using Web3.js to Sign and Recover