之前在的文章 Solidity 智能合約 - EIP-712 類型結構化資料雜湊和簽名,說明了智能合約如何檢驗 EIP-712 的簽章。這篇文章將說明如何使用 Web3.js 來進行 EIP-712 簽章。本篇文章使用 Web3.js 4.1.1 版本。

簽章

瀏覽器錢包簽章

Web3.js 可以使用下面函式來進行 EIP-712 的簽章:

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

瀏覽器錢包可以使用上面的函式,但 Web3.js 沒有內建私鑰的簽署方式,所以後面會介紹另一個套件來處理。

web3.eth.signTypedData

這個函式在 4.1.0 加入

1
web3.eth.signTypedData(address: string, typedData: Eip712TypedData): Promise<string>
  • address: 簽署人地址。
  • typedData: 簽署的內容,包含 EIP-712 的 domain、類別定義和訊息內容,直接參考下面範例說明。
  • 回傳值: 為 Promise 物件,內容為簽章。

使用之前的範例合約來說明

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);
}
}

對應的 Web3.js 用法如下

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
// 簽署人地址,例如
const address = '0xDD980c315dFA75682F04381E98ea38BD2A151540';

// 填入實際的 chainId,例如主網 1
const chainId = 1;

// 實際部署上鏈的合約地址
const verifyingContract = '0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8';

// 另外填入合約上填寫的 name 和 version
const domain = {
chainId,
verifyingContract,
name: 'Example',
version: '1'
};

const types = {
// EIP712Domain 固定寫這個內容
EIP712Domain: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'chainId',
type: 'uint256',
},
{
name: 'verifyingContract',
type: 'address',
}
],

// 以下是自定義的結構型別
Mail: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'contents', type: 'string' }
]
};

// 實際的資料內容
const message = {
from: '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8',
to: '0xDA9dfA130Df4dE4673b89022EE50ff26f6EA73Cf',
contents: 'Hello'
};

// 最後把全部內容組起來
const typedData = {
types,
domain,

// 指定使用的是哪個型別
primaryType: 'Mail',
message
};

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

最後會得到沒有切開的簽章,可以參考之前的文章使用 Web3.js 簽章與檢驗 來處理。

web3.currentProvider.request

這是通用的呼叫 RPC 函式,在比較舊的 Web3.js 版本,沒有上面的 signTypedData 可以使用,可以直接呼叫 RPC method。

1
web3.currentProvider.request(args: Web3APIPayload): Promise<string>
  • args: RPC 內容,參考下面範例。
  • 回傳值: 為 Promise 物件,內容為 RPC 執行結果,這裡的結果為簽章。

上面的範例前面的部分都一樣,只要把最後一行的

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

改成

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,
}
);

私鑰錢包簽署

如果要使用私鑰錢包簽署,可以使用 Metamask 提供的套件 @metamask/eth-sig-util

1
signTypedData({ data: TypedMessage, privateKey: Buffer, version: SignTypedDataVersion }): string
  • data: 簽署內容,和上面的 typedData 一樣。
  • privateKey: 私鑰,Buffer 型別。
  • version: EIP-712 版本。
  • 回傳值: 簽章。

安裝完後首先要引用套件

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

接著一樣只需要把最後簽署的那行改掉

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

其他範例

之前的結構陣列範例為例

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 和資料內容修改如下

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'
};

檢驗簽章

如果想在後端程式檢驗簽章,作為登入之類的用途,同樣使用 @metamask/eth-sig-util 套件:

1
recoverTypedSignature({ data: TypedMessage, signature: string, version: SignTypedDataVersion }): string
  • data: 簽署內容。
  • signature: 未切開的簽章。
  • version: EIP-712 版本。
  • 回傳值: 簽署人地址。

範例如下

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
});

總結

  1. 瀏覽器錢包使用 Web3.js v4.1.0 以上用 web3.eth.signTypedData
  2. 瀏覽器錢包使用 Web3.js 舊版本用 web3.currentProvider.request
  3. 私鑰簽署使用 @metamask/eth-sig-utilsignTypedData
  4. 檢驗簽章使用 @metamask/eth-sig-utilrecoverTypedSignature

延伸閱讀

Solidity 智能合約 - EIP-712 類型結構化資料雜湊和簽名
使用 Ethers.js 進行 EIP-712 類型結構化資料簽名
Solidity 智能合約 - ecrecover 簽章檢驗
使用 Web3.js 簽章與檢驗